Skip to content

Commit e1e0ae4

Browse files
committed
Use last modified year as end of copyright year range
Use the year of the commit in which a file was most recently modified in Git history for the end of the copyright year range. If the file hasn't been committed yet, the current calendar year is used.
1 parent 6fa172e commit e1e0ae4

File tree

2 files changed

+138
-18
lines changed

2 files changed

+138
-18
lines changed

wpiformat/test/test_licenseupdate.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
11
from datetime import date
22
import os
3+
from pathlib import Path
4+
import shutil
5+
import subprocess
6+
import tempfile
37

48
from test.tasktest import *
59
from wpiformat.config import Config
610
from wpiformat.licenseupdate import LicenseUpdate
711

812

13+
class OpenTemporaryDirectory():
14+
15+
def __init__(self):
16+
self.prev_dir = os.getcwd()
17+
18+
def __enter__(self):
19+
self.temp_dir = tempfile.TemporaryDirectory()
20+
os.chdir(self.temp_dir.name)
21+
return self.temp_dir
22+
23+
def __exit__(self, type, value, traceback):
24+
os.chdir(self.prev_dir)
25+
26+
927
def test_licenseupdate():
1028
year = str(date.today().year)
1129

@@ -173,4 +191,83 @@ def test_licenseupdate():
173191
config_file = Config(os.path.abspath(os.getcwd()), ".styleguide")
174192
assert not task.should_process_file(config_file, "./Excluded.h")
175193

194+
# Create git repo to test license years for commits
195+
with OpenTemporaryDirectory():
196+
subprocess.run(["git", "init", "-q"])
197+
198+
# Add base files
199+
with open(".styleguide-license", "w") as file:
200+
file.write("// Copyright (c) {year}")
201+
with open(".styleguide", "w") as file:
202+
file.write("cppSrcFileInclude {\n" + r"\.cpp$")
203+
subprocess.run(["git", "add", ".styleguide-license"])
204+
subprocess.run(["git", "add", ".styleguide"])
205+
subprocess.run(["git", "commit", "-q", "-m", "\"Initial commit\""])
206+
207+
# Add file with commit date of last year and range through this year
208+
with open("last-year.cpp", "w") as file:
209+
file.write(f"// Copyright (c) 2017-{year}")
210+
subprocess.run(["git", "add", "last-year.cpp"])
211+
subprocess.run(["git", "commit", "-q", "-m", "\"Last year\""])
212+
last_iso_year = f"{int(year) - 1}-01-01T00:00:00"
213+
subprocess.Popen([
214+
"git", "commit", "-q", "--amend", "--no-edit",
215+
f"--date={last_iso_year}"
216+
],
217+
env={
218+
**os.environ, "GIT_COMMITTER_DATE": last_iso_year
219+
}).wait()
220+
221+
# Add file with commit date of this year and range through this year
222+
with open("this-year.cpp", "w") as file:
223+
file.write(f"// Copyright (c) 2017-{year}")
224+
subprocess.run(["git", "add", "this-year.cpp"])
225+
subprocess.run(["git", "commit", "-q", "-m", "\"This year\""])
226+
227+
# Add file with commit date of next year and range through this year
228+
with open("next-year.cpp", "w") as file:
229+
file.write(f"// Copyright (c) 2017-{year}")
230+
subprocess.run(["git", "add", "next-year.cpp"])
231+
subprocess.run(["git", "commit", "-q", "-m", "\"Next year\""])
232+
next_iso_year = f"{int(year) + 1}-01-01T00:00:00"
233+
subprocess.Popen([
234+
"git", "commit", "-q", "--amend", "--no-edit",
235+
f"--date={next_iso_year}"
236+
],
237+
env={
238+
**os.environ, "GIT_COMMITTER_DATE": next_iso_year
239+
}).wait()
240+
241+
# Create uncommitted file with no year
242+
Path("no-year.cpp").touch()
243+
244+
# Run wpiformat on last-year.cpp
245+
with open("last-year.cpp", "r") as input:
246+
lines = input.read()
247+
output, changed, success = task.run_pipeline(config_file,
248+
"last-year.cpp", lines)
249+
assert output == f"// Copyright (c) 2017-{int(year) - 1}\n\n"
250+
251+
# Run wpiformat on this-year.cpp
252+
with open("last-year.cpp", "r") as input:
253+
lines = input.read()
254+
output, changed, success = task.run_pipeline(config_file,
255+
"this-year.cpp", lines)
256+
assert output == f"// Copyright (c) 2017-{year}\n\n"
257+
258+
# Run wpiformat on next-year.cpp
259+
with open("next-year.cpp", "r") as input:
260+
lines = input.read()
261+
output, changed, success = task.run_pipeline(config_file,
262+
"next-year.cpp", lines)
263+
assert output == f"// Copyright (c) 2017-{int(year) + 1}\n\n"
264+
265+
# Run wpiformat on no-year.cpp
266+
# Should have current calendar year
267+
with open("no-year.cpp", "r") as input:
268+
lines = input.read()
269+
output, changed, success = task.run_pipeline(config_file, "no-year.cpp",
270+
lines)
271+
assert output == f"// Copyright (c) {year}\n\n"
272+
176273
test.run(OutputType.FILE)

wpiformat/wpiformat/licenseupdate.py

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44
import regex
5+
import subprocess
56
import sys
67

78
from wpiformat.config import Config
@@ -26,15 +27,17 @@ def should_process_file(self, config_file, name):
2627
return (config_file.is_c_file(name) or config_file.is_cpp_file(name) or
2728
name.endswith(".java")) and not license_regex.search(name)
2829

29-
def __try_regex(self, lines, license_template):
30+
def __try_regex(self, lines, last_year, license_template):
3031
"""Try finding license with regex of license template.
3132
3233
Keyword arguments:
3334
lines -- lines of file
35+
last_year -- last year in copyright range
3436
license_template -- license_template string
3537
3638
Returns:
37-
Tuple of whether license was found, year, and file contents after license.
39+
Tuple of whether license was found, first year in copyright range, and
40+
file contents after license.
3841
"""
3942
linesep = Task.get_linesep(lines)
4043

@@ -47,28 +50,29 @@ def __try_regex(self, lines, license_template):
4750
license_rgx = regex.compile(license_rgxstr, regex.M)
4851

4952
# Compare license
50-
year = self.__current_year
5153
match = license_rgx.search(lines)
5254
if match:
5355
try:
54-
year = match.group("year")
56+
first_year = match.group("year")
5557
except IndexError:
5658
pass
5759

5860
# If comment at beginning of file is non-empty license, update it
59-
return (True, year, linesep + lines[match.end():].lstrip())
61+
return (True, first_year, linesep + lines[match.end():].lstrip())
6062
else:
61-
return (False, year, lines)
63+
return (False, last_year, lines)
6264

63-
def __try_string_search(self, lines, license_template):
65+
def __try_string_search(self, lines, last_year, license_template):
6466
"""Try finding license with string search.
6567
6668
Keyword arguments:
6769
lines -- lines of file
70+
last_year -- last year in copyright range
6871
license_template -- license_template string
6972
7073
Returns:
71-
Tuple of whether license was found, year, and file contents after license.
74+
Tuple of whether license was found, first year in copyright range, and
75+
file contents after license.
7276
"""
7377
linesep = Task.get_linesep(lines)
7478

@@ -107,8 +111,9 @@ def __try_string_search(self, lines, license_template):
107111
else:
108112
license_end += 1
109113

114+
first_year = last_year
115+
110116
# If comment at beginning of file is non-empty license, update it
111-
year = self.__current_year
112117
if first_comment_is_license and license_end > 0:
113118
license_part = linesep.join(stripped_lines[0:license_end])
114119
appendix_part = \
@@ -119,33 +124,51 @@ def __try_string_search(self, lines, license_template):
119124
match = year_regex.search(line)
120125
# If license contains copyright pattern, extract the first year
121126
if match:
122-
year = match.group(1)
127+
first_year = match.group(1)
123128
break
124129

125-
return (True, year, appendix_part)
130+
return (True, first_year, appendix_part)
126131
else:
127-
return (False, year, linesep + lines.lstrip())
132+
return (False, first_year, linesep + lines.lstrip())
128133

129134
def run_pipeline(self, config_file, name, lines):
130135
linesep = Task.get_linesep(lines)
131136

132137
license_template = Config.read_file(
133138
os.path.dirname(os.path.abspath(name)), ".styleguide-license")
134139

135-
success, year, appendix = self.__try_regex(lines, license_template)
140+
# Get year when file was most recently modified in Git history
141+
#
142+
# Committer date is used instead of author date (the one shown by "git
143+
# log" because the year the file was last modified in the history should
144+
# be used. Author dates can be older than this or even out of order in
145+
# the log.
146+
cmd = ["git", "log", "-n", "1", "--format=%ci", "--", name]
147+
last_year = subprocess.run(cmd,
148+
stdout=subprocess.PIPE).stdout.decode()[:4]
149+
150+
# If file hasn't been committed yet, use current calendar year as end of
151+
# copyright year range
152+
if last_year == "":
153+
last_year = self.__current_year
154+
155+
success, first_year, appendix = self.__try_regex(
156+
lines, last_year, license_template)
136157
if not success:
137-
success, year, appendix = self.__try_string_search(
138-
lines, license_template)
158+
success, first_year, appendix = self.__try_string_search(
159+
lines, last_year, license_template)
139160

140161
output = ""
141162

142163
# Determine copyright range and trailing padding
143-
if year != self.__current_year:
144-
year = year + "-" + self.__current_year
164+
if first_year != last_year:
165+
year_range = first_year + "-" + last_year
166+
else:
167+
year_range = first_year
145168

146169
for line in license_template:
147170
# Insert copyright year range
148-
line = line.replace("{year}", year)
171+
line = line.replace("{year}", year_range)
149172

150173
# Insert padding which expands to the 80th column. If there is more
151174
# than one padding token, the line may contain fewer than 80

0 commit comments

Comments
 (0)