1 | #! /usr/bin/env python
|
---|
2 |
|
---|
3 | """Consolidate a bunch of CVS or RCS logs read from stdin.
|
---|
4 |
|
---|
5 | Input should be the output of a CVS or RCS logging command, e.g.
|
---|
6 |
|
---|
7 | cvs log -rrelease14:
|
---|
8 |
|
---|
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!
|
---|
12 |
|
---|
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.
|
---|
16 |
|
---|
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.
|
---|
21 |
|
---|
22 | The -r option reverses the output (oldest first; the default is oldest
|
---|
23 | last).
|
---|
24 |
|
---|
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.)
|
---|
30 |
|
---|
31 | -h prints this message and exits.
|
---|
32 |
|
---|
33 | XXX This code was created by reverse engineering CVS 1.9 and RCS 5.7
|
---|
34 | from their output.
|
---|
35 | """
|
---|
36 |
|
---|
37 | import sys, errno, getopt, re
|
---|
38 |
|
---|
39 | sep1 = '='*77 + '\n' # file separator
|
---|
40 | sep2 = '-'*28 + '\n' # revision separator
|
---|
41 |
|
---|
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)
|
---|
71 |
|
---|
72 | def read_chunk(fp):
|
---|
73 | """Read a chunk -- data for one file, ending with sep1.
|
---|
74 |
|
---|
75 | Split the chunk in parts separated by sep2.
|
---|
76 |
|
---|
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
|
---|
95 |
|
---|
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
|
---|
164 |
|
---|
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
|
---|
179 |
|
---|
180 | if __name__ == '__main__':
|
---|
181 | try:
|
---|
182 | main()
|
---|
183 | except IOError, e:
|
---|
184 | if e.errno != errno.EPIPE:
|
---|
185 | raise
|
---|