forked from f4exb/sdrangel
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added script to update copyright notices from git history. Part of f4…
- Loading branch information
Showing
1 changed file
with
298 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,298 @@ | ||
#!/usr/bin/python | ||
""" | ||
Utility to generate copyright notices from the git history of the source file | ||
Inspired by: https://0pointer.net/blog/projects/copyright.html | ||
""" | ||
import sys | ||
import os | ||
import functools | ||
from subprocess import * | ||
from datetime import * | ||
from optparse import OptionParser | ||
|
||
AUTHOR_SUBSTITUTES = { | ||
"ZigaS": "Ziga S", | ||
"hexameron": "John Greb", | ||
"srcejon": "Jon Beniston, M7RCE", | ||
"Jon Beniston": "Jon Beniston, M7RCE", | ||
"f4exb": "Edouard Griffiths, F4EXB", | ||
"Edouard Griffiths": "Edouard Griffiths, F4EXB" | ||
} | ||
|
||
# Commits rewriting the copyright notices | ||
EXCLUDE_HASHES = { | ||
"c9e13c336", | ||
"e61317ef0", | ||
"65842d9b5", | ||
"00b041d76", | ||
"3a944fa20", | ||
"b6c4d10b6", | ||
"869f1a419", | ||
"743260db9", | ||
"9ce0810a2", | ||
"3596fe431" | ||
} | ||
|
||
# ====================================================================== | ||
def getInputOptions(): | ||
parser = OptionParser(usage="usage: %%prog options\n\n%s") | ||
parser.add_option("-f", "--file", dest="file", help="File to process", metavar="FILE", type="str") | ||
parser.add_option("-d", "--directory", dest="directory", help="Directory to process", metavar="DIRECTORY", type="str") | ||
parser.add_option("-e", "--extension", dest="extensions", help="Filter by this extension (includes dot)", metavar="EXTENSION", type="str", action="append") | ||
parser.add_option("-l", "--list-authors", dest="list_authors", help="List authors", metavar="LIST", action="store_true", default=False) | ||
parser.add_option("-r", "--remove-original", dest="remove_original", help="Remove original copyright notices", metavar="REMOVE", action="store_true", default=False) | ||
parser.add_option("-n", "--dry-run", dest="dry_run", help="Print new headers instead of overwriting the files", metavar="DRY_RUN", action="store_true", default=False) | ||
(options, args) = parser.parse_args() | ||
return options | ||
|
||
# ====================================================================== | ||
def validate_options(options): | ||
if options.file is None and options.directory is None: | ||
print("At least a file (-f) or a directory (-d) must be specified") | ||
return False | ||
elif options.file is not None and options.directory is not None: | ||
print("Specify either a file (-f) or a directory (-d) but not both") | ||
return False | ||
if not options.extensions: | ||
options.extensions = [".h", ".cpp"] | ||
return True | ||
|
||
# ====================================================================== | ||
def pretty_years(s): | ||
|
||
l = list(s) | ||
l.sort() | ||
|
||
start = None | ||
prev = None | ||
r = [] | ||
|
||
for x in l: | ||
if prev is None: | ||
start = x | ||
prev = x | ||
continue | ||
|
||
if x == prev + 1: | ||
prev = x | ||
continue | ||
|
||
if prev == start: | ||
r.append("%i" % prev) | ||
else: | ||
r.append("%i-%i" % (start, prev)) | ||
|
||
start = x | ||
prev = x | ||
|
||
if not prev is None: | ||
if prev == start: | ||
r.append("%i" % prev) | ||
else: | ||
r.append("%i-%i" % (start, prev)) | ||
|
||
return ", ".join(r) | ||
|
||
# ====================================================================== | ||
def order_by_year(a, b): | ||
|
||
la = list(a[2]) | ||
la.sort() | ||
|
||
lb = list(b[2]) | ||
lb.sort() | ||
|
||
if la[0] < lb[0]: | ||
return -1 | ||
elif la[0] > lb[0]: | ||
return 1 | ||
else: | ||
return 0 | ||
|
||
# ====================================================================== | ||
def analyze(f): | ||
print(f"File: {f}") | ||
|
||
commits = [] | ||
data = {} | ||
|
||
for ln in Popen(["git", "log", "--follow", "--all", "--date=format:'%Y-%m-%d %H:%M:%S,%z'", "--pretty=format:'%an,%ae,%ad,%h'", f], stdout=PIPE).stdout: | ||
ls = ln.decode().strip() | ||
le = ls.split(',') # Line elements (comma separated) | ||
lh = le[4].rstrip("\'") | ||
if lh in EXCLUDE_HASHES: | ||
continue | ||
dt = datetime.strptime(le[2].lstrip("\'"), '%Y-%m-%d %H:%M:%S') | ||
tz = le[3].rstrip("\'") | ||
data = { | ||
"author": le[0].lstrip("\'"), | ||
"author-mail": le[1], | ||
"author-time": int(datetime.timestamp(dt)), | ||
"author-tz": tz | ||
} | ||
if data["author"] == "Hexameron": | ||
data["author-time"] = int(datetime.timestamp(datetime(2012, 1, 1))) | ||
if data["author"] in AUTHOR_SUBSTITUTES: | ||
data["author"] = AUTHOR_SUBSTITUTES[data["author"]] | ||
commits.append(data) | ||
|
||
by_author = {} | ||
|
||
for c in commits: | ||
try: | ||
n = by_author[c["author"]] | ||
except KeyError: | ||
n = (c["author"], c["author-mail"], set()) | ||
by_author[c["author"]] = n | ||
|
||
# FIXME: Handle time zones properly | ||
year = datetime.fromtimestamp(int(c["author-time"])).year | ||
|
||
n[2].add(year) | ||
|
||
for an, a in list(by_author.items()): | ||
for bn, b in list(by_author.items()): | ||
if a is b: | ||
continue | ||
|
||
if a[1] == b[1]: | ||
a[2].update(b[2]) | ||
|
||
if an in by_author and bn in by_author: | ||
del by_author[bn] | ||
|
||
copyrite = list(by_author.values()) | ||
copyrite.sort(key=functools.cmp_to_key(order_by_year)) | ||
return copyrite | ||
|
||
# ====================================================================== | ||
def get_files(options): | ||
files = [] | ||
dirs = os.walk(options.directory) | ||
for dirspec in dirs: | ||
for f in dirspec[2]: | ||
filepath = os.path.join(dirspec[0], f) | ||
ext = os.path.splitext(filepath)[1] | ||
if ext in options.extensions: | ||
files.append(filepath) | ||
return files | ||
|
||
# ====================================================================== | ||
def list_authors(options): | ||
authors = set() | ||
if options.directory is not None: | ||
files = get_files(options) | ||
for f in files: | ||
copyrite = analyze(f) | ||
for c in copyrite: | ||
authors.add(c[0]) | ||
for author in authors: | ||
print(author) | ||
return | ||
copyrite = analyze(options.file) | ||
for c in copyrite: | ||
print(c[0]) | ||
|
||
# ====================================================================== | ||
def remove_line(line): | ||
if "Copyright (C)" in line: | ||
return True | ||
if "Copyright (c)" in line: | ||
return True | ||
if line.startswith("// written by"): | ||
return True | ||
|
||
# ====================================================================== | ||
def get_header_lines(): | ||
return [ | ||
"", | ||
"This program is free software; you can redistribute it and/or modify", | ||
"it under the terms of the GNU General Public License as published by", | ||
"the Free Software Foundation as version 3 of the License, or", | ||
"(at your option) any later version.", | ||
"", | ||
"This program is distributed in the hope that it will be useful,", | ||
"but WITHOUT ANY WARRANTY; without even the implied warranty of", | ||
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the", | ||
"GNU General Public License V3 for more details.", | ||
"", | ||
"You should have received a copy of the GNU General Public License", | ||
"along with this program. If not, see <http://www.gnu.org/licenses/>." | ||
] | ||
|
||
# ====================================================================== | ||
def process_file(f, options): | ||
with open(f) as ff: | ||
lines_in = [line.rstrip() for line in ff] | ||
lines_out = [] | ||
cr = analyze(f) | ||
header = False | ||
header_start = 0 | ||
for iline, line in enumerate(lines_in): | ||
if line.startswith("////////"): | ||
header= True | ||
header_start = iline | ||
break | ||
if header: | ||
lines_out = lines_in[:header_start+1] | ||
else: | ||
lines_out = ["///////////////////////////////////////////////////////////////////////////////////////"] | ||
width = len(lines_out[header_start]) - 6 | ||
for name, mail, years in cr: | ||
if name == "Hexameron": | ||
lines_out.append("// {0:{1}} //".format("Copyright (C) 2012 maintech GmbH, Otto-Hahn-Str. 15, 97204 Hoechberg, Germany", width)) | ||
lines_out.append("// {0:{1}} //".format("written by Christian Daniel", width)) | ||
else: | ||
cr_string = f"Copyright (C) {pretty_years(years)} {name} <{mail}>" | ||
lines_out.append(f"// {cr_string:{width}} //") | ||
if not header: | ||
for hline in get_header_lines(): | ||
lines_out.append("// {0:{1}} //".format(hline, width)) | ||
lines_out.append(lines_out[0]) | ||
elif not options.remove_original: | ||
lines_out.append("") | ||
in_header = header | ||
header_stop = len(lines_out) | ||
iread = header_start+1 if header else 0 | ||
for iline, line in enumerate(lines_in[iread:]): | ||
if in_header and options.remove_original and remove_line(line): | ||
continue | ||
if line.startswith("////////"): | ||
in_header = False | ||
header_stop += iline | ||
lines_out.append(line) | ||
if options.dry_run: | ||
for line_out in lines_out[header_start:header_stop]: | ||
print(line_out) | ||
else: | ||
with open(f, "w") as ff: | ||
for line in lines_out: | ||
ff.write(f"{line}\n") | ||
|
||
|
||
# ====================================================================== | ||
def process_directory(options): | ||
files = get_files(options) | ||
for f in files: | ||
process_file(f, options) | ||
|
||
|
||
# ====================================================================== | ||
def main(): | ||
try: | ||
options = getInputOptions() | ||
if not validate_options(options): | ||
sys.exit(-1) | ||
if options.list_authors: | ||
list_authors(options) | ||
elif options.file: | ||
process_file(options.file, options) | ||
else: | ||
process_directory(options) | ||
except KeyboardInterrupt: | ||
print("Keyboard interrupt. Exiting") | ||
|
||
|
||
# ====================================================================== | ||
if __name__ == '__main__': | ||
main() |