1#!/usr/bin/perl
2
3# Generate a ChangeLog file from a CVS log.
4# Written by Robert Krawitz <rlk@alum.mit.edu>
5# This code is in the public domain and may be used
6# for any purpose.
7
8use Getopt::Long;
9Getopt::Long::Configure("bundling", "no_ignore_case", "pass_through");
10
11use strict;
12
13# Configuration options.
14my $emailsuffix;
15my $symbolic_name_regexp;
16my (@ignoreprefix);
17my $reverse = 0;
18my $use_rcs_filename = 0;
19my $print_each_file = 0;
20my $print_time = 0;
21
22GetOptions("e:s" => \$emailsuffix,
23	   "X=s" => \@ignoreprefix,
24	   "r!"  => \$reverse,
25	   "R!"  => \$use_rcs_filename,
26	   "v!"  => \$print_each_file,
27	   "t!"  => \$print_time,
28	   "s:s" => \$symbolic_name_regexp);
29
30my %logmsgs = ();			# Index by date, time, and author
31my %fileversions = ();
32my $skipme = 0;
33my %basenames = ();
34my %plus = ();
35my %minus = ();
36
37my @cvsdirs=`find . -type d -name CVS -print`;
38@cvsdirs = map { chomp; s,^\./,, } @cvsdirs;
39foreach my $d (@cvsdirs) {
40    if (open ENTRIES, "$d/Entries") {
41	my ($rootdir) = $d;
42	$rootdir =~ s/CVS$//;
43	while (<ENTRIES>) {
44	    my ($type, $file, $version, @junk) = split /\//;
45	    if ($type eq "") {
46		$basenames{"$file"} = "1";
47		$file = "$rootdir$file";
48		$fileversions{$file} = $version;
49	    }
50	}
51	close ENTRIES;
52    }
53}
54
55sub compare_versions($$)
56{
57    # vw: version of the working file
58    # vl: version from the log
59    # The idea is that we want versions on the current branch, on branches
60    # leading to the current branch, and on the root prior to the current
61    # branch.
62    #
63    # Example: the current file is 1.5.12.2.4.3
64    #
65    # We want versions:
66    # 1.1
67    # 1.2
68    # 1.3
69    # 1.4
70    # 1.5
71    # 1.5.12.1
72    # 1.5.12.2
73    # 1.5.12.2.4.1
74    # 1.5.12.2.4.2
75    # 1.5.12.2.4.3
76    #
77    # We look at the numbers in pairs.  The first number in each pair is
78    # the branch number; the second number is the version on the branch.
79    # The pairs are of the form (B, V).
80    #
81    # If the number of components in the log version is greater than the
82    # number of components in the working version, we aren't interested.
83    # This file cannot be a predecessor of the working version; it is
84    # either a branch off the working version, or it is an entirely different
85    # branch.
86    #
87    # We next iterate over all pairs in the log version.  The following must
88    # be true for all pairs:
89    #
90    # Bw = Bl
91    # Vw >= Vl
92    #
93    # Note that there's no problem if the number of components in the
94    # working version exceeds the number of components in the log version.
95    #
96    # There is a special case: If the working version doesn't exist at all,
97    # we return true if the log version is on the mainline.  This lets us
98    # see log messages from files that have been deleted.
99    #
100    # Return value:
101    #
102    # 4 if there is no working version and the log version is at top level
103    # 
104    # 2 if there is no working version and the log version is not at top
105    #   level
106    #
107    # 3 if the number of components in the log version exceeds the number
108    #   of components in the working version
109    #
110    # 0 if the log version is later than or on a different branch from
111    #   the working version
112    #
113    # 1 otherwise (if the log version is a predecessor of the working version)
114
115    my ($vw, $vl) = @_;
116
117    my (@vvl) = split(/\./, $vl);
118
119    if ($vw eq "") {
120	if ($#vvl < 2) {
121	    return 2;
122	} else {
123	    return 4;
124	}
125    }
126
127    my (@vvw) = split(/\./, $vw);
128    if ($#vvl > $#vvw) {
129	return 3;
130    }
131
132    my ($i);
133    for ($i = 0; $i < $#vvl; $i += 2) {
134	my ($bl) = $vvl[$i];
135	my ($vl) = $vvl[$i + 1];
136	my ($bw) = $vvw[$i];
137	my ($vw) = $vvw[$i + 1];
138	if ($bw != $bl || $vw < $vl) {
139	    return 0;
140	}
141    }
142    return 1;
143}
144
145my ($in_header) = 0;
146my ($has_rcs_file) = 0;
147my (%symbols);
148my $revision;
149my $ignore;
150my ($skipfile);
151my $currentfile;
152my $currentbasefile;
153my %symbols_printed = ();
154
155while (<>) {
156    if (/^RCS file: /) {
157	next if (! $use_rcs_filename);
158	$in_header = 1;
159	$has_rcs_file = 1;
160	chomp;
161	$currentfile = $_;
162	$currentfile =~ s,/RCS/,/,;
163	$currentfile =~ s,^RCS file: *\./,,;
164	$currentfile =~ s/,v$//;
165	$currentfile =~ s/\s/\000/g;
166	if (grep { $currentfile =~ /^$_/ } @ignoreprefix) {
167	    $skipfile = 1;
168	} else {
169	    $skipfile = 0;
170	}
171	$symbols{$currentfile} = {};
172	next;
173    } elsif (/^Working file: /) {
174	if ($has_rcs_file) {
175	    $has_rcs_file = 0;
176	    next;
177	}
178	$in_header = 1;
179	chomp;
180	($ignore, $ignore, $currentfile) = split(/\s+/, $_, 3);
181	$currentfile =~ s/\s/\000/g;
182	if (grep { $currentfile =~ /^$_/ } @ignoreprefix) {
183	    $skipfile = 1;
184	} else {
185	    $skipfile = 0;
186	}
187	$symbols{$currentfile} = {};
188	next;
189    } elsif ($in_header && $_ =~ /^symbolic names:/) {
190	while (<>) {
191	    if (/^\s/) {
192		my ($name, $revision) = split;
193		$name =~ s/:$//;
194		next if (! ($name =~ /$symbolic_name_regexp/));
195		if (! defined $symbols{$currentfile}{$revision}) {
196		    $symbols{$currentfile}{$revision} = ();
197		}
198		push @{$symbols{$currentfile}{$revision}}, $name;
199	    } else {
200		last;
201	    }
202	}
203    } elsif ($_ =~ /^----------------------------$/) {
204	$in_header = 0;
205	next;
206    } elsif (! $in_header && $_ =~ /^revision /) {
207	($ignore, $revision) = split;
208	($currentbasefile) = $currentfile;
209	$currentbasefile =~ s;.*/([^/]+);\1;;
210	my ($check) = compare_versions($fileversions{$currentfile}, $revision);
211	#
212	# Special case -- if a file is not in the current sandbox, but it
213	# has the same base name as a file in the sandbox, log it;
214	# otherwise if it is not in the current sandbox, don't log it.
215	# 
216	if (($check == 2 && (1 || (
217	     ($basenames{$currentbasefile} || !($currentfile =~ /\//)) &&
218	     ($currentfile =~ /\.[chly]$/)))) ||
219	    $check == 1) {
220	    $skipme = 0;
221	} else {
222	    # We don't want to print out any symbolic names associated with
223	    # skipped versions.
224	    map {
225		$symbols_printed{$_} = 1;
226	    } @{$symbols{$currentfile}{$revision}};
227	    $skipme = 1;
228	}
229    } elsif (! $in_header && $_ =~ /^date: /) {
230	my (@stuff) = split;
231	my ($date, $time, $author, $state, $plus, $minus);
232	if ($stuff[3] =~ /^[-+][0-9][0-9][0-9][0-9]/) {
233	    $date = $stuff[1];
234	    $time = $stuff[2];
235	    $author = $stuff[5];
236	    $state = $stuff[7];
237	    $plus = $stuff[9];
238	    $minus = $stuff[10];
239	} else {
240	    $date = $stuff[1];
241	    $time = $stuff[2];
242	    $author = $stuff[4];
243	    $state = $stuff[6];
244	    $plus = $stuff[8];
245	    $minus = $stuff[9];
246	}
247#	$time =~ s/[0-9]:[0-9][0-9];$//;
248#	$time =~ s/[0-9][0-9];$//;
249	$time =~ s/;$//;
250	$author =~ s/;$//;
251	$plus =~ s/;$//;
252	$minus =~ s/;$//;
253	my $body = "";
254	my $firstline = 1;
255	while (<>) {
256	    if ($_ =~ /^----------------------------$/) {
257		last;
258	    } elsif ($_ =~ /^=============================================================================$/) {
259		last;
260	    } elsif ($firstline && $_ =~ /^branches:([ \t]+[0-9]+(\.[0-9]+)+;)+$/) {
261		next;
262	    } else {
263		$body .= $_;
264		$firstline = 0;
265	    }
266	}
267	my $junkbody = $body;
268	$junkbody =~ s/\s//g;
269	$junkbody .= "x";
270	my $symbols;
271	if (defined $symbols{$currentfile}{$revision}) {
272	    $symbols = join " ", @{$symbols{$currentfile}{$revision}};
273	}
274	my $datetimeauthor = "$date $time $author $junkbody $currentfile $revision $symbols";
275	if ($skipfile == 0 && $skipme == 0) {
276	    $logmsgs{$datetimeauthor} = $body;
277	    if ($plus eq "") {
278		$plus{$datetimeauthor} = "added";
279		if (-f $currentfile) {
280		    my ($lines) = `wc -l \"$currentfile\" | awk '{print \$1}'`;
281		    chomp $lines;
282		    $plus{$datetimeauthor} .= " +$lines";
283		    $minus{$datetimeauthor} = "-0";
284		}
285	    } elsif ($state eq "dead;") {
286		$plus{$datetimeauthor} = "removed";
287	    } else {
288		$plus{$datetimeauthor} = $plus;
289		$minus{$datetimeauthor} = $minus;
290	    }
291	}
292    }				# Other junk we ignore
293}
294
295my $prevmsg="";
296my $prevdate="";
297my $prevtime="";
298my $prevauthor="";
299my $header="";
300my %deltainfo = ();
301my %revinfo = ();
302
303my @chlog = $reverse ? sort keys %logmsgs : reverse sort keys %logmsgs;
304my %filenames_printed = ();
305my ($date, $time, $author, $junk, $file, $revision, @symbols);
306my $filestuff;
307
308sub printmsg($$$$$\%\%)
309{
310    my ($date, $time, $author, $emailsuffix, $prevmsg, $revinfo, $deltainfo) = @_;
311    if ($print_time) {
312	$time = " $time";
313    } else {
314	$time = "";
315    }
316    print "$date$time\t<$author$emailsuffix>\n\n";
317    my $filestuff = join "", (map { "\t\t$_ ($$revinfo{$_}) ($$deltainfo{$_})\n" } sort keys %revinfo);
318    $filestuff =~ s/\000/ /g;
319    $filestuff =~ s/\t/\tFiles:/;
320    print "$filestuff\n";
321    $prevmsg =~ s/^/\t/g;
322    $prevmsg =~ s/\n/\n\t/g;
323    $prevmsg =~ s/[ \t]+\n/\n/g;
324    $prevmsg =~ s/[ \t]+$//g;
325    print "$prevmsg\n";
326}
327
328sub printsyms(@) {
329    my (@symbols_to_print) = @_;
330    print "===============================================================================\n";
331    map {
332	print "Name: $_\n";
333    } @symbols_to_print;
334    print "\n";
335}
336
337foreach (@chlog) {
338    ($date, $time, $author, $junk, $file, $revision, @symbols) = split;
339    $date =~ s,/,-,g;
340    my $msg = $logmsgs{$_};
341    my $delta;
342    if (! $minus{$_}) {
343	$delta = "$plus{$_}";
344    } else {
345	$delta = "$plus{$_} $minus{$_}";
346    }
347    if (! $print_each_file && $prevmsg eq $msg && !$filenames_printed{$file}) {
348	$deltainfo{$file} = $delta;
349	$revinfo{$file} = $revision;
350    } else {
351	if ($prevmsg ne "" || keys %deltainfo > 0) {
352	    printmsg($prevdate, $prevtime, $prevauthor, $emailsuffix, $prevmsg, %revinfo, %deltainfo);
353	    %filenames_printed = ();
354	}
355	$header = "$date\t<$author$emailsuffix>\n\n";
356	$prevmsg = $msg;
357	$prevdate = $date;
358	$prevauthor = $author;
359	$prevtime = $time;
360	%deltainfo = ();
361	%revinfo = ();
362	$deltainfo{$file} = $delta;
363	$revinfo{$file} = $revision;
364	$filenames_printed{$file} = 1;
365    }
366    my (@symbols_to_print);
367    foreach my $s (@symbols) {
368	if (! $symbols_printed{$s}) {
369	    push @symbols_to_print, $s;
370	    $symbols_printed{$s} = 1;
371	}
372    }
373    if (@symbols_to_print) {
374	printsyms(@symbols_to_print);
375    }
376}
377
378if ($prevmsg ne "" || keys %deltainfo > 0) {
379    printmsg($prevdate, $prevtime, $prevauthor, $emailsuffix, $prevmsg, %revinfo, %deltainfo);
380}
381