mirror of
				https://github.com/f4exb/sdrangel.git
				synced 2025-10-31 13:00:26 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			299 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			299 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/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()
 |