update_copyright_header.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # This Source Code Form is subject to the terms of the Mozilla Public
  4. # License, v. 2.0. If a copy of the MPL was not distributed with this
  5. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6. # It is based on the idea of http://0pointer.net/blog/projects/copyright.html
  7. import os
  8. import re
  9. import io
  10. import sys
  11. from git import *
  12. from shutil import move
  13. try:
  14. from StringIO import StringIO
  15. except ImportError:
  16. from io import StringIO
  17. if sys.version_info[0] >= 3:
  18. # strings are already parsed to unicode
  19. def unicode(s):
  20. return s
  21. # Replace the name by another value, i.e. add affiliation or replace user name by full name
  22. # only use lower case name
  23. authorFullName = {
  24. 'staldert': 'Thomas Stalder, Blue Time Concept SA',
  25. 'mark giraud': 'Mark Giraud, Fraunhofer IOSB',
  26. 'julius pfrommer': 'Julius Pfrommer, Fraunhofer IOSB',
  27. 'stefan profanter': 'Stefan Profanter, fortiss GmbH',
  28. }
  29. # Skip commits with the following authors, since they are not valid names
  30. # and come from an invalid git config
  31. skipNames = ['=', 'open62541', 'opcua']
  32. def compactYears(yearList):
  33. current = None
  34. last = None
  35. result = []
  36. for yStr in yearList:
  37. y = int(yStr)
  38. if last is None:
  39. current = y
  40. last = y
  41. continue
  42. if y == last + 1:
  43. last = y
  44. continue
  45. if last == current:
  46. result.append("%i" % last)
  47. else:
  48. result.append("%i-%i" % (current, last))
  49. current = y
  50. last = y
  51. if not last is None:
  52. if last == current:
  53. result.append("%i" % last)
  54. else:
  55. result.append("%i-%i" % (current, last))
  56. return ", ".join(result)
  57. fileAuthorStats = dict()
  58. def insertCopyrightAuthors(file, authorsList):
  59. copyrightEntries = list()
  60. for author in authorsList:
  61. copyrightEntries.append(unicode("Copyright {} (c) {}").format(compactYears(author['years']), author['author']))
  62. copyrightAdded = False
  63. commentPattern = re.compile(r"(.*)\*/$")
  64. tmpName = file + ".new"
  65. tempFile = io.open(tmpName, mode="w", encoding="utf-8")
  66. with io.open(file, mode="r", encoding="utf-8") as f:
  67. for line in f:
  68. if copyrightAdded or not commentPattern.match(line):
  69. tempFile.write(line)
  70. else:
  71. tempFile.write(commentPattern.match(line).group(1) + "\n *\n")
  72. for e in copyrightEntries:
  73. tempFile.write(unicode(" * {}\n").format(e))
  74. tempFile.write(unicode(" */\n"))
  75. copyrightAdded = True
  76. tempFile.close()
  77. os.unlink(file)
  78. move(tmpName, file)
  79. def updateCopyright(repo, file):
  80. print("Checking file {}".format(file))
  81. # Build the info on how many lines every author commited every year
  82. relativeFilePath = file[len(repo.working_dir)+1:].replace("\\","/")
  83. if not relativeFilePath in fileAuthorStats:
  84. print("File not found in list: {}".format(relativeFilePath))
  85. return
  86. stats = fileAuthorStats[relativeFilePath]
  87. # Now create a sorted list and filter out small contributions
  88. authorList = list()
  89. for author in stats:
  90. if author in skipNames:
  91. continue
  92. authorYears = list()
  93. for year in stats[author]['years']:
  94. if stats[author]['years'][year] < 10:
  95. # ignore contributions for this year if less than 10 lines changed
  96. continue
  97. authorYears.append(year)
  98. if len(authorYears) == 0:
  99. continue
  100. authorYears.sort()
  101. if author.lower() in authorFullName:
  102. authorName = authorFullName[author.lower()]
  103. else:
  104. authorName = author
  105. authorList.append({
  106. 'author': authorName,
  107. 'years': authorYears,
  108. 'first_commit': stats[author]['first_commit']
  109. })
  110. # Sort the authors list first by year, and then by name
  111. authorListSorted = sorted(authorList, key=lambda a: a['first_commit'])
  112. insertCopyrightAuthors(file, authorListSorted)
  113. # This is required since some commits use different author names for the same person
  114. assumeSameAuthor = {
  115. 'Mark': u'Mark Giraud',
  116. 'Infinity95': u'Mark Giraud',
  117. 'janitza-thbe': u'Thomas Bender',
  118. 'Stasik0': u'Sten Grüner',
  119. 'Sten': u'Sten Grüner',
  120. 'Frank Meerkoetter': u'Frank Meerkötter',
  121. 'ichrispa': u'Chris Iatrou',
  122. 'Chris Paul Iatrou': u'Chris Iatrou',
  123. 'Torben-D': u'TorbenD',
  124. 'FlorianPalm': u'Florian Palm',
  125. 'ChristianFimmers': u'Christian Fimmers'
  126. }
  127. def buildFileStats(repo):
  128. fileRenameMap = dict()
  129. renamePattern = re.compile(r"(.*){(.*) => (.*)}(.*)")
  130. cnt = 0
  131. for commit in repo.iter_commits():
  132. cnt += 1
  133. curr = 0
  134. for commit in repo.iter_commits():
  135. curr += 1
  136. print("Checking commit {}/{} -> {}".format(curr, cnt, commit.hexsha))
  137. for objpath, stats in commit.stats.files.items():
  138. match = renamePattern.match(objpath)
  139. if match:
  140. # the file was renamed, store the rename to follow up later
  141. oldFile = (match.group(1) + match.group(2) + match.group(4)).replace("//", "/")
  142. newFile = (match.group(1) + match.group(3) + match.group(4)).replace("//", "/")
  143. while newFile in fileRenameMap:
  144. newFile = fileRenameMap[newFile]
  145. if oldFile != newFile:
  146. fileRenameMap[oldFile] = newFile
  147. else:
  148. newFile = fileRenameMap[objpath] if objpath in fileRenameMap else objpath
  149. if stats['insertions'] > 0:
  150. if not newFile in fileAuthorStats:
  151. fileAuthorStats[newFile] = dict()
  152. authorName = unicode(commit.author.name)
  153. if authorName in assumeSameAuthor:
  154. authorName = assumeSameAuthor[authorName]
  155. if not authorName in fileAuthorStats[newFile]:
  156. fileAuthorStats[newFile][authorName] = {
  157. 'years': dict(),
  158. 'first_commit': commit.committed_datetime
  159. }
  160. elif commit.committed_datetime < fileAuthorStats[newFile][authorName]['first_commit']:
  161. fileAuthorStats[newFile][authorName]['first_commit'] = commit.committed_datetime
  162. if not commit.committed_datetime.year in fileAuthorStats[newFile][authorName]['years']:
  163. fileAuthorStats[newFile][authorName]['years'][commit.committed_datetime.year] = 0
  164. fileAuthorStats[newFile][authorName]['years'][commit.committed_datetime.year] += stats['insertions']
  165. def walkFiles(repo, folder, pattern):
  166. patternCompiled = re.compile(pattern)
  167. for root, subdirs, files in os.walk(folder):
  168. for f in files:
  169. if patternCompiled.match(f):
  170. fname = os.path.join(root,f)
  171. updateCopyright(repo, fname)
  172. if __name__ == '__main__':
  173. baseDir = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir))
  174. repo = Repo(baseDir)
  175. assert not repo.bare
  176. buildFileStats(repo)
  177. dirs = ['src', 'plugins', 'include']
  178. for dir in dirs:
  179. walkFiles(repo, os.path.join(baseDir, dir), r"(.*\.c|.*\.h)$")