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