| 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
|
|---|