update_copyright_header.py 7.2 KB

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