|
@@ -0,0 +1,234 @@
|
|
|
+#!/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
|
|
|
+
|
|
|
+try:
|
|
|
+ from StringIO import StringIO
|
|
|
+except ImportError:
|
|
|
+ from io import StringIO
|
|
|
+
|
|
|
+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',
|
|
|
+ '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)$")
|