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.
9 which dumps all log messages from release1.4 upwards (assuming that
10 release 1.4 was tagged with tag 'release14'). Note the trailing
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
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
31 -h prints this message and exits.
33 XXX This code was created by reverse engineering CVS 1.9 and RCS 5.7
37 import sys
, errno
, getopt
, re
39 sep1
= '='*77 + '\n' # file separator
40 sep2
= '-'*28 + '\n' # revision separator
47 opts
, args
= getopt
.getopt(sys
.argv
[1:], "trb:h")
60 chunk
= read_chunk(sys
.stdin
)
63 records
= digest_chunk(chunk
, branch
)
66 database
[len(database
):] = records
70 format_output(database
)
73 """Read a chunk -- data for one file, ending with sep1.
75 Split the chunk in parts separated by sep2.
96 def digest_chunk(chunk
, branch
=None):
97 """Digest a chunk -- extract working file name and revisions"""
102 if line
[:keylen
] == key
:
103 working_file
= line
[keylen
:].strip()
109 elif branch
== "HEAD":
110 branch
= re
.compile(r
"^\d+\.\d+$")
113 key
= 'symbolic names:\n'
120 tag
, rev
= line
.split()
126 rev
= revisions
.get(branch
)
127 branch
= re
.compile(r
"^<>$") # <> to force a mismatch by default
129 if rev
.find('.0.') >= 0:
130 rev
= rev
.replace('.0.', '.')
131 branch
= re
.compile(r
"^" + re
.escape(rev
) + r
"\.\d+$")
133 for lines
in chunk
[1:]:
137 words
= dateline
.split()
139 if len(words
) >= 3 and words
[0] == 'date:':
142 if timeword
[-1:] == ';':
143 timeword
= timeword
[:-1]
144 date
= dateword
+ ' ' + timeword
145 if len(words
) >= 5 and words
[3] == 'author:':
147 if author
[-1:] == ';':
151 text
.insert(0, revline
)
152 words
= revline
.split()
153 if len(words
) >= 2 and words
[0] == 'revision':
156 # No 'revision' line -- weird...
158 text
.insert(0, revline
)
160 if rev
is None or not branch
.match(rev
):
162 records
.append((date
, working_file
, rev
, author
, text
))
165 def format_output(database
):
168 database
.append((None, None, None, None, None)) # Sentinel
169 for (date
, working_file
, rev
, author
, text
) in database
:
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
)
177 prev
.append((date
, working_file
, rev
, author
))
180 if __name__
== '__main__':
184 if e
.errno
!= errno
.EPIPE
: