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