logmerge.py revision ed5b3d8b3c8077c8c57c8a825cae06e319b0994d
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 22XXX This code was created by reverse engineering CVS 1.9 and RCS 5.7 23from their output. 24 25""" 26 27import os, sys, getopt, string, re 28 29sep1 = '='*77 + '\n' # file separator 30sep2 = '-'*28 + '\n' # revision separator 31 32def main(): 33 """Main program""" 34 truncate_last = 0 35 opts, args = getopt.getopt(sys.argv[1:], "-t") 36 for o, a in opts: 37 if o == '-t': 38 truncate_last = 1 39 database = [] 40 while 1: 41 chunk = read_chunk(sys.stdin) 42 if not chunk: 43 break 44 records = digest_chunk(chunk) 45 if truncate_last: 46 del records[-1] 47 database[len(database):] = records 48 database.sort() 49 database.reverse() 50 format_output(database) 51 52def read_chunk(fp): 53 """Read a chunk -- data for one file, ending with sep1. 54 55 Split the chunk in parts separated by sep2. 56 57 """ 58 chunk = [] 59 lines = [] 60 while 1: 61 line = fp.readline() 62 if not line: 63 break 64 if line == sep1: 65 if lines: 66 chunk.append(lines) 67 break 68 if line == sep2: 69 if lines: 70 chunk.append(lines) 71 lines = [] 72 else: 73 lines.append(line) 74 return chunk 75 76def digest_chunk(chunk): 77 """Digest a chunk -- extrach working file name and revisions""" 78 lines = chunk[0] 79 key = 'Working file:' 80 keylen = len(key) 81 for line in lines: 82 if line[:keylen] == key: 83 working_file = string.strip(line[keylen:]) 84 break 85 else: 86 working_file = None 87 records = [] 88 for lines in chunk[1:]: 89 revline = lines[0] 90 dateline = lines[1] 91 text = lines[2:] 92 words = string.split(dateline) 93 author = None 94 if len(words) >= 3 and words[0] == 'date:': 95 dateword = words[1] 96 timeword = words[2] 97 if timeword[-1:] == ';': 98 timeword = timeword[:-1] 99 date = dateword + ' ' + timeword 100 if len(words) >= 5 and words[3] == 'author:': 101 author = words[4] 102 if author[-1:] == ';': 103 author = author[:-1] 104 else: 105 date = None 106 text.insert(0, revline) 107 words = string.split(revline) 108 if len(words) >= 2 and words[0] == 'revision': 109 rev = words[1] 110 else: 111 rev = None 112 text.insert(0, revline) 113 records.append((date, working_file, rev, author, text)) 114 return records 115 116def format_output(database): 117 prevtext = None 118 prev = [] 119 database.append((None, None, None, None, None)) # Sentinel 120 for (date, working_file, rev, author, text) in database: 121 if text != prevtext: 122 if prev: 123 print sep2, 124 for (p_date, p_working_file, p_rev, p_author) in prev: 125 print p_date, p_author, p_working_file 126 sys.stdout.writelines(prevtext) 127 prev = [] 128 prev.append((date, working_file, rev, author)) 129 prevtext = text 130 131main() 132