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