/Tools/scripts/logmerge.py

http://unladen-swallow.googlecode.com/ · Python · 185 lines · 176 code · 1 blank · 8 comment · 0 complexity · 670d51d5f4f84738664d785704fb5127 MD5 · raw file

  1. #! /usr/bin/env python
  2. """Consolidate a bunch of CVS or RCS logs read from stdin.
  3. Input should be the output of a CVS or RCS logging command, e.g.
  4. cvs log -rrelease14:
  5. which dumps all log messages from release1.4 upwards (assuming that
  6. release 1.4 was tagged with tag 'release14'). Note the trailing
  7. colon!
  8. This collects all the revision records and outputs them sorted by date
  9. rather than by file, collapsing duplicate revision record, i.e.,
  10. records with the same message for different files.
  11. The -t option causes it to truncate (discard) the last revision log
  12. entry; this is useful when using something like the above cvs log
  13. command, which shows the revisions including the given tag, while you
  14. probably want everything *since* that tag.
  15. The -r option reverses the output (oldest first; the default is oldest
  16. last).
  17. The -b tag option restricts the output to *only* checkin messages
  18. belonging to the given branch tag. The form -b HEAD restricts the
  19. output to checkin messages belonging to the CVS head (trunk). (It
  20. produces some output if tag is a non-branch tag, but this output is
  21. not very useful.)
  22. -h prints this message and exits.
  23. XXX This code was created by reverse engineering CVS 1.9 and RCS 5.7
  24. from their output.
  25. """
  26. import sys, errno, getopt, re
  27. sep1 = '='*77 + '\n' # file separator
  28. sep2 = '-'*28 + '\n' # revision separator
  29. def main():
  30. """Main program"""
  31. truncate_last = 0
  32. reverse = 0
  33. branch = None
  34. opts, args = getopt.getopt(sys.argv[1:], "trb:h")
  35. for o, a in opts:
  36. if o == '-t':
  37. truncate_last = 1
  38. elif o == '-r':
  39. reverse = 1
  40. elif o == '-b':
  41. branch = a
  42. elif o == '-h':
  43. print __doc__
  44. sys.exit(0)
  45. database = []
  46. while 1:
  47. chunk = read_chunk(sys.stdin)
  48. if not chunk:
  49. break
  50. records = digest_chunk(chunk, branch)
  51. if truncate_last:
  52. del records[-1]
  53. database[len(database):] = records
  54. database.sort()
  55. if not reverse:
  56. database.reverse()
  57. format_output(database)
  58. def read_chunk(fp):
  59. """Read a chunk -- data for one file, ending with sep1.
  60. Split the chunk in parts separated by sep2.
  61. """
  62. chunk = []
  63. lines = []
  64. while 1:
  65. line = fp.readline()
  66. if not line:
  67. break
  68. if line == sep1:
  69. if lines:
  70. chunk.append(lines)
  71. break
  72. if line == sep2:
  73. if lines:
  74. chunk.append(lines)
  75. lines = []
  76. else:
  77. lines.append(line)
  78. return chunk
  79. def digest_chunk(chunk, branch=None):
  80. """Digest a chunk -- extract working file name and revisions"""
  81. lines = chunk[0]
  82. key = 'Working file:'
  83. keylen = len(key)
  84. for line in lines:
  85. if line[:keylen] == key:
  86. working_file = line[keylen:].strip()
  87. break
  88. else:
  89. working_file = None
  90. if branch is None:
  91. pass
  92. elif branch == "HEAD":
  93. branch = re.compile(r"^\d+\.\d+$")
  94. else:
  95. revisions = {}
  96. key = 'symbolic names:\n'
  97. found = 0
  98. for line in lines:
  99. if line == key:
  100. found = 1
  101. elif found:
  102. if line[0] in '\t ':
  103. tag, rev = line.split()
  104. if tag[-1] == ':':
  105. tag = tag[:-1]
  106. revisions[tag] = rev
  107. else:
  108. found = 0
  109. rev = revisions.get(branch)
  110. branch = re.compile(r"^<>$") # <> to force a mismatch by default
  111. if rev:
  112. if rev.find('.0.') >= 0:
  113. rev = rev.replace('.0.', '.')
  114. branch = re.compile(r"^" + re.escape(rev) + r"\.\d+$")
  115. records = []
  116. for lines in chunk[1:]:
  117. revline = lines[0]
  118. dateline = lines[1]
  119. text = lines[2:]
  120. words = dateline.split()
  121. author = None
  122. if len(words) >= 3 and words[0] == 'date:':
  123. dateword = words[1]
  124. timeword = words[2]
  125. if timeword[-1:] == ';':
  126. timeword = timeword[:-1]
  127. date = dateword + ' ' + timeword
  128. if len(words) >= 5 and words[3] == 'author:':
  129. author = words[4]
  130. if author[-1:] == ';':
  131. author = author[:-1]
  132. else:
  133. date = None
  134. text.insert(0, revline)
  135. words = revline.split()
  136. if len(words) >= 2 and words[0] == 'revision':
  137. rev = words[1]
  138. else:
  139. # No 'revision' line -- weird...
  140. rev = None
  141. text.insert(0, revline)
  142. if branch:
  143. if rev is None or not branch.match(rev):
  144. continue
  145. records.append((date, working_file, rev, author, text))
  146. return records
  147. def format_output(database):
  148. prevtext = None
  149. prev = []
  150. database.append((None, None, None, None, None)) # Sentinel
  151. for (date, working_file, rev, author, text) in database:
  152. if text != prevtext:
  153. if prev:
  154. print sep2,
  155. for (p_date, p_working_file, p_rev, p_author) in prev:
  156. print p_date, p_author, p_working_file, p_rev
  157. sys.stdout.writelines(prevtext)
  158. prev = []
  159. prev.append((date, working_file, rev, author))
  160. prevtext = text
  161. if __name__ == '__main__':
  162. try:
  163. main()
  164. except IOError, e:
  165. if e.errno != errno.EPIPE:
  166. raise