mailmapper 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. #!/usr/bin/env python
  2. #
  3. # Copyright (C) 2014, Masahiro Yamada <yamada.m@jp.panasonic.com>
  4. #
  5. # SPDX-License-Identifier: GPL-2.0+
  6. #
  7. '''
  8. A tool to create/update the mailmap file
  9. The command 'git shortlog' summarizes git log output in a format suitable
  10. for inclusion in release announcements. Each commit will be grouped by
  11. author and title.
  12. One problem is that the authors' name and/or email address is sometimes
  13. spelled differently. The .mailmap feature can be used to coalesce together
  14. commits by the same persion.
  15. (See 'man git-shortlog' for furthur information of this feature.)
  16. This tool helps to create/update the mailmap file.
  17. It runs 'git shortlog' internally and searches differently spelled author
  18. names which share the same email address. The author name with the most
  19. commits is asuumed to be a canonical real name. If the number of commits
  20. from the cananonical name is equal to or greater than 'MIN_COMMITS',
  21. the entry for the cananical name will be output. ('MIN_COMMITS' is used
  22. here because we do not want to create a fat mailmap by adding every author
  23. with only a few commits.)
  24. If there exists a mailmap file specified by the mailmap.file configuration
  25. options or '.mailmap' at the toplevel of the repository, it is used as
  26. a base file. (The mailmap.file configuration takes precedence over the
  27. '.mailmap' file if both exist.)
  28. The base file and the newly added entries are merged together and sorted
  29. alphabetically (but the comment block is kept untouched), and then printed
  30. to standard output.
  31. Usage
  32. -----
  33. scripts/mailmapper
  34. prints the mailmapping to standard output.
  35. scripts/mailmapper > tmp; mv tmp .mailmap
  36. will be useful for updating '.mailmap' file.
  37. '''
  38. import sys
  39. import os
  40. import subprocess
  41. # The entries only for the canonical names with MIN_COMMITS or more commits.
  42. # This limitation is used so as not to create a too big mailmap file.
  43. MIN_COMMITS = 50
  44. try:
  45. toplevel = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'])
  46. except subprocess.CalledProcessError:
  47. sys.exit('Please run in a git repository.')
  48. # strip '\n'
  49. toplevel = toplevel.rstrip()
  50. # Change the current working directory to the toplevel of the respository
  51. # for our easier life.
  52. os.chdir(toplevel)
  53. # First, create 'auther name' vs 'number of commits' database.
  54. # We assume the name with the most commits as the canonical real name.
  55. shortlog = subprocess.check_output(['git', 'shortlog', '-s', '-n'])
  56. commits_per_name = {}
  57. for line in shortlog.splitlines():
  58. try:
  59. commits, name = line.split(None, 1)
  60. except ValueError:
  61. # ignore lines with an empty author name
  62. pass
  63. commits_per_name[name] = int(commits)
  64. # Next, coalesce the auther names with the same email address
  65. shortlog = subprocess.check_output(['git', 'shortlog', '-s', '-n', '-e'])
  66. mail_vs_name = {}
  67. output = {}
  68. for line in shortlog.splitlines():
  69. # tmp, mail = line.rsplit(None, 1) is not safe
  70. # because weird email addresses might include whitespaces
  71. tmp, mail = line.split('<')
  72. mail = '<' + mail.rstrip()
  73. try:
  74. _, name = tmp.rstrip().split(None, 1)
  75. except ValueError:
  76. # author name is empty
  77. name = ''
  78. if mail in mail_vs_name:
  79. # another name for the same email address
  80. prev_name = mail_vs_name[mail]
  81. # Take the name with more commits
  82. major_name = sorted([prev_name, name],
  83. key=lambda x: commits_per_name[x] if x else 0)[1]
  84. mail_vs_name[mail] = major_name
  85. if commits_per_name[major_name] > MIN_COMMITS:
  86. output[mail] = major_name
  87. else:
  88. mail_vs_name[mail] = name
  89. # [1] If there exists a mailmap file at the location pointed to
  90. # by the mailmap.file configuration option, update it.
  91. # [2] If the file .mailmap exists at the toplevel of the repository, update it.
  92. # [3] Otherwise, create a new mailmap file.
  93. mailmap_files = []
  94. try:
  95. config_mailmap = subprocess.check_output(['git', 'config', 'mailmap.file'])
  96. except subprocess.CalledProcessError:
  97. config_mailmap = ''
  98. config_mailmap = config_mailmap.rstrip()
  99. if config_mailmap:
  100. mailmap_files.append(config_mailmap)
  101. mailmap_files.append('.mailmap')
  102. infile = None
  103. for map_file in mailmap_files:
  104. try:
  105. infile = open(map_file)
  106. except:
  107. # Failed to open. Try next.
  108. continue
  109. break
  110. comment_block = []
  111. output_lines = []
  112. if infile:
  113. for line in infile:
  114. if line[0] == '#' or line[0] == '\n':
  115. comment_block.append(line)
  116. else:
  117. output_lines.append(line)
  118. break
  119. for line in infile:
  120. output_lines.append(line)
  121. infile.close()
  122. for mail, name in output.items():
  123. output_lines.append(name + ' ' + mail + '\n')
  124. output_lines.sort()
  125. sys.stdout.write(''.join(comment_block + output_lines))