Home | History | Annotate | Download | only in scripts
      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
    186