• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#! /usr/bin/env python
2
3"""Consolidate a bunch of CVS or RCS logs read from stdin.
4
5Input should be the output of a CVS or RCS logging command, e.g.
6
7    cvs log -rrelease14:
8
9which dumps all log messages from release1.4 upwards (assuming that
10release 1.4 was tagged with tag 'release14').  Note the trailing
11colon!
12
13This collects all the revision records and outputs them sorted by date
14rather than by file, collapsing duplicate revision record, i.e.,
15records with the same message for different files.
16
17The -t option causes it to truncate (discard) the last revision log
18entry; this is useful when using something like the above cvs log
19command, which shows the revisions including the given tag, while you
20probably want everything *since* that tag.
21
22The -r option reverses the output (oldest first; the default is oldest
23last).
24
25The -b tag option restricts the output to *only* checkin messages
26belonging to the given branch tag.  The form -b HEAD restricts the
27output to checkin messages belonging to the CVS head (trunk).  (It
28produces some output if tag is a non-branch tag, but this output is
29not very useful.)
30
31-h prints this message and exits.
32
33XXX This code was created by reverse engineering CVS 1.9 and RCS 5.7
34from their output.
35"""
36
37import sys, errno, getopt, re
38
39sep1 = '='*77 + '\n'                    # file separator
40sep2 = '-'*28 + '\n'                    # revision separator
41
42def 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
72def 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
96def 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
165def 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
180if __name__ == '__main__':
181    try:
182        main()
183    except IOError, e:
184        if e.errno != errno.EPIPE:
185            raise
186