| #!/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)$") |