#!/usr/bin/env python # -*- coding: utf-8 -*- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # It is based on the idea of http://0pointer.net/blog/projects/copyright.html import os import re import io import sys from git import * from shutil import move if sys.version_info[0] >= 3: # strings are already parsed to unicode def unicode(s): return s # Replace the name by another value, i.e. add affiliation or replace user name by full name # only use lower case name authorFullName = { 'staldert': 'Thomas Stalder, Blue Time Concept SA', 'mark giraud': 'Mark Giraud, Fraunhofer IOSB', 'julius pfrommer': 'Julius Pfrommer, Fraunhofer IOSB', 'stefan profanter': 'Stefan Profanter, fortiss GmbH', } # Skip commits with the following authors, since they are not valid names # and come from an invalid git config skipNames = ['=', 'open62541', 'opcua'] def compactYears(yearList): current = None last = None result = [] for yStr in yearList: y = int(yStr) if last is None: current = y last = y continue if y == last + 1: last = y continue if last == current: result.append("%i" % last) else: result.append("%i-%i" % (current, last)) current = y last = y if not last is None: if last == current: result.append("%i" % last) else: result.append("%i-%i" % (current, last)) return ", ".join(result) fileAuthorStats = dict() def insertCopyrightAuthors(file, authorsList): copyrightEntries = list() for author in authorsList: copyrightEntries.append(unicode("Copyright {} (c) {}").format(compactYears(author['years']), author['author'])) copyrightAdded = False commentPattern = re.compile(r"(.*)\*/$") tmpName = file + ".new" tempFile = io.open(tmpName, mode="w", encoding="utf-8") with io.open(file, mode="r", encoding="utf-8") as f: for line in f: if copyrightAdded or not commentPattern.match(line): tempFile.write(line) else: tempFile.write(commentPattern.match(line).group(1) + "\n *\n") for e in copyrightEntries: tempFile.write(unicode(" * {}\n").format(e)) tempFile.write(unicode(" */\n")) copyrightAdded = True tempFile.close() os.unlink(file) move(tmpName, file) def updateCopyright(repo, file): print("Checking file {}".format(file)) # Build the info on how many lines every author commited every year relativeFilePath = file[len(repo.working_dir)+1:].replace("\\","/") if not relativeFilePath in fileAuthorStats: print("File not found in list: {}".format(relativeFilePath)) return stats = fileAuthorStats[relativeFilePath] # Now create a sorted list and filter out small contributions authorList = list() for author in stats: if author in skipNames: continue authorYears = list() for year in stats[author]['years']: if stats[author]['years'][year] < 10: # ignore contributions for this year if less than 10 lines changed continue authorYears.append(year) if len(authorYears) == 0: continue authorYears.sort() if author.lower() in authorFullName: authorName = authorFullName[author.lower()] else: authorName = author authorList.append({ 'author': authorName, 'years': authorYears, 'first_commit': stats[author]['first_commit'] }) # Sort the authors list first by year, and then by name authorListSorted = sorted(authorList, key=lambda a: a['first_commit']) insertCopyrightAuthors(file, authorListSorted) # This is required since some commits use different author names for the same person assumeSameAuthor = { 'Mark': u'Mark Giraud', 'Infinity95': u'Mark Giraud', 'janitza-thbe': u'Thomas Bender', 'Stasik0': u'Sten Grüner', 'Sten': u'Sten Grüner', 'Frank Meerkoetter': u'Frank Meerkötter', 'ichrispa': u'Chris Iatrou', 'Chris Paul Iatrou': u'Chris Iatrou', 'Torben-D': u'TorbenD', 'FlorianPalm': u'Florian Palm', 'ChristianFimmers': u'Christian Fimmers' } def buildFileStats(repo): fileRenameMap = dict() renamePattern = re.compile(r"(.*){(.*) => (.*)}(.*)") cnt = 0 for commit in repo.iter_commits(): cnt += 1 curr = 0 for commit in repo.iter_commits(): curr += 1 print("Checking commit {}/{} -> {}".format(curr, cnt, commit.hexsha)) for objpath, stats in commit.stats.files.items(): match = renamePattern.match(objpath) if match: # the file was renamed, store the rename to follow up later oldFile = (match.group(1) + match.group(2) + match.group(4)).replace("//", "/") newFile = (match.group(1) + match.group(3) + match.group(4)).replace("//", "/") while newFile in fileRenameMap: newFile = fileRenameMap[newFile] if oldFile != newFile: fileRenameMap[oldFile] = newFile else: newFile = fileRenameMap[objpath] if objpath in fileRenameMap else objpath if stats['insertions'] > 0: if not newFile in fileAuthorStats: fileAuthorStats[newFile] = dict() authorName = unicode(commit.author.name) if authorName in assumeSameAuthor: authorName = assumeSameAuthor[authorName] if not authorName in fileAuthorStats[newFile]: fileAuthorStats[newFile][authorName] = { 'years': dict(), 'first_commit': commit.committed_datetime } elif commit.committed_datetime < fileAuthorStats[newFile][authorName]['first_commit']: fileAuthorStats[newFile][authorName]['first_commit'] = commit.committed_datetime if not commit.committed_datetime.year in fileAuthorStats[newFile][authorName]['years']: fileAuthorStats[newFile][authorName]['years'][commit.committed_datetime.year] = 0 fileAuthorStats[newFile][authorName]['years'][commit.committed_datetime.year] += stats['insertions'] def walkFiles(repo, folder, pattern): patternCompiled = re.compile(pattern) for root, subdirs, files in os.walk(folder): for f in files: if patternCompiled.match(f): fname = os.path.join(root,f) updateCopyright(repo, fname) if __name__ == '__main__': baseDir = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir)) repo = Repo(baseDir) assert not repo.bare buildFileStats(repo) dirs = ['src', 'plugins', 'include'] for dir in dirs: walkFiles(repo, os.path.join(baseDir, dir), r"(.*\.c|.*\.h)$")