2 exec perl -w -x $0 ${1+"$@"} # -*- mode: perl; perl-indent-level: 2; -*-
6 ##############################################################
8 ### cvs2cl.pl: produce ChangeLog(s) from `cvs log` output. ###
10 ##############################################################
13 ## $Date: 2005/10/29 14:58:02 $
19 use File::Basename qw( fileparse );
20 use Getopt::Long qw( GetOptions );
22 use Time::Local qw( timegm );
23 use User::pwent qw( getpwnam );
27 # Read in the logs for multiple files, spit out a nice ChangeLog that
28 # mirrors the information entered during `cvs commit'.
30 # The problem presents some challenges. In an ideal world, we could
31 # detect files with the same author, log message, and checkin time --
32 # each <filelist, author, time, logmessage> would be a changelog entry.
33 # We'd sort them; and spit them out. Unfortunately, CVS is *not atomic*
34 # so checkins can span a range of times. Also, the directory structure
35 # could be hierarchical.
37 # Another question is whether we really want to have the ChangeLog
38 # exactly reflect commits. An author could issue two related commits,
39 # with different log entries, reflecting a single logical change to the
40 # source. GNU style ChangeLogs group these under a single author/date.
41 # We try to do the same.
43 # So, we parse the output of `cvs log', storing log messages in a
44 # multilevel hash that stores the mapping:
45 # directory => author => time => message => filelist
46 # As we go, we notice "nearby" commit times and store them together
47 # (i.e., under the same timestamp), so they appear in the same log
50 # When we've read all the logs, we twist this mapping into
51 # a time => author => message => filelist mapping for each directory.
53 # If we're not using the `--distributed' flag, the directory is always
54 # considered to be `./', even as descend into subdirectories.
58 # name number of lines (10.xii.03)
61 # +-maybe_grab_accumulation_date 38
62 # +-read_changelog 277
63 # +-maybe_read_user_map_file 94
66 # +-read_symbolic_name 43
68 # +-read_date_author_and_state 25
69 # +-parse_date_author_and_state 20
71 # +-output_changelog 424
72 # +-pretty_file_list 290
73 # +-common_path_prefix 35
74 # +-preprocess_msg_text 30
78 # +-wrap_log_entry 177
88 # -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*- -*-
90 # Note about a bug-slash-opportunity:
91 # -----------------------------------
93 # There's a bug in Text::Wrap, which affects cvs2cl. This script
101 # "This script demonstrates a bug in Text::Wrap. The very long line
102 # following this paragraph will be relocated relative to the surrounding
105 # ====================================================================
107 # See? When the bug happens, we'll get the line of equal signs below
108 # this paragraph, even though it should be above.";
111 # # Print out the test text with no wrapping:
112 # print "$test_text";
116 # # Now print it out wrapped, and see the bug:
117 # print wrap ("\t", " ", "$test_text");
121 # If the line of equal signs were one shorter, then the bug doesn't
122 # happen. Interesting.
124 # Anyway, rather than fix this in Text::Wrap, we might as well write a
125 # new wrap() which has the following much-needed features:
127 # * initial indentation, like current Text::Wrap()
128 # * subsequent line indentation, like current Text::Wrap()
129 # * user chooses among: force-break long words, leave them alone, or die()?
130 # * preserve existing indentation: chopped chunks from an indented line
131 # are indented by same (like this line, not counting the asterisk!)
132 # * optional list of things to preserve on line starts, default ">"
134 # Note that the last two are essentially the same concept, so unify in
135 # implementation and give a good interface to controlling them.
139 # Optionally, when encounter a line pre-indented by same as previous
140 # line, then strip the newline and refill, but indent by the same.
143 # Globals --------------------------------------------------------------------
145 # In case we have to print it out:
146 my $VERSION = '$Revision: 1.1 $';
147 $VERSION =~ s/\S+\s+(\S+)\s+\S+/$1/;
149 ## Vars set by options:
151 # Print debugging messages?
154 # Just show version and exit?
155 my $Print_Version = 0;
157 # Just print usage message and exit?
160 # What file should we generate (defaults to "ChangeLog")?
161 my $Log_File_Name = "ChangeLog";
163 # Grab most recent entry date from existing ChangeLog file, just add
167 # `cvs log -d`, this will repeat the last entry in the old log. This is OK,
168 # as it guarantees at least one entry in the update changelog, which means
169 # that there will always be a date to extract for the next update. The repeat
170 # entry can be removed in postprocessing, if necessary.
173 # I don't think this actually does anything useful
176 # Expand usernames to email addresses based on a map file?
177 my $User_Map_File = '';
178 my $User_Passwd_File;
181 # Output log in chronological order? [default is reverse chronological order]
182 my $Chronological_Order = 0;
184 # Grab user details via gecos
187 # User domain for gecos email addresses
190 # Output to a file or to stdout?
191 my $Output_To_Stdout = 0;
193 # Eliminate empty log messages?
194 my $Prune_Empty_Msgs = 0;
196 # Tags of which not to output
199 # Show only revisions with Tags
202 # Don't call Text::Wrap on the body of the message
205 # Indentation of log messages
208 # Don't do any pretty print processing
211 # Separates header from log message. Code assumes it is either " " or
212 # "\n\n", so if there's ever an option to set it to something else,
213 # make sure to go through all conditionals that use this var.
214 my $After_Header = " ";
217 my $XML_Encoding = '';
219 # Format more for programs than for humans.
221 my $No_XML_Namespace = 0;
222 my $No_XML_ISO_Date = 0;
224 # Do some special tweaks for log data that was written in FSF
228 # Show times in UTC instead of local time
231 # Show times in output?
234 # Show day of week in output?
235 my $Show_Day_Of_Week = 0;
237 # Show revision numbers in output?
238 my $Show_Revisions = 0;
240 # Show dead files in output?
243 # Hide dead trunk files which were created as a result of additions on a
245 my $Hide_Branch_Additions = 1;
247 # Show tags (symbolic names) in output?
250 # Show tags separately in output?
251 my $Show_Tag_Dates = 0;
253 # Show branches by symbolic name in output?
254 my $Show_Branches = 0;
256 # Show only revisions on these branches or their ancestors.
258 # Show only revisions on these branches or their ancestors; ignore descendent
262 # Don't bother with files matching this regexp.
265 # How exactly we match entries. We definitely want "o",
266 # and user might add "i" by using --case-insensitive option.
267 my $Case_Insensitive = 0;
269 # Maybe only show log messages matching a certain regular expression.
270 my $Regexp_Gate = '';
272 # Pass this global option string along to cvs, to the left of `log':
273 my $Global_Opts = '';
275 # Pass this option string along to the cvs log subcommand:
276 my $Command_Opts = '';
278 # Read log output from stdin instead of invoking cvs log?
279 my $Input_From_Stdin = 0;
281 # Don't show filenames in output.
282 my $Hide_Filenames = 0;
284 # Don't shorten directory names from filenames.
287 # Max checkin duration. CVS checkin is not atomic, so we may have checkin
288 # times that span a range of time. We assume that checkins will last no
289 # longer than $Max_Checkin_Duration seconds, and that similarly, no
290 # checkins will happen from the same users with the same message less
291 # than $Max_Checkin_Duration seconds apart.
292 my $Max_Checkin_Duration = 180;
294 # What to put at the front of [each] ChangeLog.
295 my $ChangeLog_Header = '';
297 # Whether to enable 'delta' mode, and for what start/end tags.
304 # Whether to parse filenames from the RCS filename, and if so what
308 # Whether to output information on the # of lines added and removed
309 # by each file modification.
310 my $Show_Lines_Modified = 0;
312 ## end vars set by options.
314 # latest observed times for the start/end tags in delta mode
315 my $Delta_StartTime = 0;
316 my $Delta_EndTime = 0;
318 my $No_Ancestors = 0;
320 my $No_Extra_Indent = 0;
322 my $GroupWithinDate = 0;
324 # ----------------------------------------------------------------------------
326 package CVS::Utils::ChangeLog::EntrySet;
331 bless \%self, $class;
334 # -------------------------------------
336 sub output_changelog {
337 my $output_type = $XML_Output ? 'XML' : 'Text';
338 my $output_class = "CVS::Utils::ChangeLog::EntrySet::Output::${output_type}";
339 my $output = $output_class->new(follow_branches => \@Follow_Branches,
340 follow_only => \@Follow_Only,
341 ignore_tags => \%ignore_tags,
342 show_tags => \%show_tags,
344 $output->output_changelog(@_);
347 # -------------------------------------
350 my ($self, $file_full_path, $time, $revision, $state, $lines,
351 $branch_names, $branch_roots, $branch_numbers,
352 $symbolic_names, $author, $msg_txt) = @_;
355 CVS::Utils::ChangeLog::FileEntry->new($file_full_path, $time, $revision,
357 $branch_names, $branch_roots,
361 # We might be including revision numbers and/or tags and/or
362 # branch names in the output. Most of the code from here to
363 # loop-end deals with organizing these in qunk.
365 unless ( $Hide_Branch_Additions
367 $msg_txt =~ /file .+ was initially added on branch \S+./ ) {
368 # Add this file to the list
369 # (We use many spoonfuls of autovivication magic. Hashes and arrays
370 # will spring into existence if they aren't there already.)
372 &main::debug ("(pushing log msg for ". $qunk->dir_key . $qunk->filename . ")\n");
374 # Store with the files in this commit. Later we'll loop through
375 # again, making sure that revisions with the same log message
376 # and nearby commit times are grouped together as one commit.
377 $self->{$qunk->dir_key}{$author}{$time}{$msg_txt} =
378 CVS::Utils::ChangeLog::Message->new($msg_txt)
379 unless exists $self->{$qunk->dir_key}{$author}{$time}{$msg_txt};
380 $self->{$qunk->dir_key}{$author}{$time}{$msg_txt}->add_fileentry($qunk);
385 # ----------------------------------------------------------------------------
387 package CVS::Utils::ChangeLog::EntrySet::Output::Text;
389 use base qw( CVS::Utils::ChangeLog::EntrySet::Output );
391 use File::Basename qw( fileparse );
395 my $self = $class->SUPER::new(@_);
398 # -------------------------------------
401 my $self = shift; my $class = ref $self;
404 return $Show_Day_Of_Week ? ' ' . $class->weekday_en($wday) : '';
407 # -------------------------------------
411 my ($time, $author, $lastdate) = @_;
413 my $header_line = '';
415 my (undef,$min,$hour,$mday,$mon,$year,$wday)
416 = $UTC_Times ? gmtime($time) : localtime($time);
418 my $date = $self->fdatetime($time);
422 sprintf "%s %s\n\n", $date, $author;
424 if ( ! defined $lastdate or $date ne $lastdate or ! $GroupWithinDate ) {
425 if ( $GroupWithinDate ) {
426 $header_line = "$date\n\n";
428 $header_line = "$date $author\n\n";
436 # -------------------------------------
438 sub preprocess_msg_text {
442 $text = $self->SUPER::preprocess_msg_text($text);
444 unless ( $No_Wrap ) {
445 # Strip off lone newlines, but only for lines that don't begin with
446 # whitespace or a mail-quoting character, since we want to preserve
447 # that kind of formatting. Also don't strip newlines that follow a
448 # period; we handle those specially next. And don't strip
449 # newlines that precede an open paren.
450 1 while $text =~ s/(^|\n)([^>\s].*[^.\n])\n([^>\n])/$1$2 $3/g;
452 # If a newline follows a period, make sure that when we bring up the
453 # bottom sentence, it begins with two spaces.
454 1 while $text =~ s/(^|\n)([^>\s].*)\n([^>\n])/$1$2 $3/g;
460 # -------------------------------------
462 # Here we take a bunch of qunks and convert them into printed
463 # summary that will include all the information the user asked for.
464 sub pretty_file_list {
470 my $qunksref = shift;
473 my $beauty = ''; # The accumulating header string for this entry.
474 my %non_unanimous_tags; # Tags found in a proper subset of qunks
475 my %unanimous_tags; # Tags found in all qunks
476 my %all_branches; # Branches found in any qunk
477 my $fbegun = 0; # Did we begin printing filenames yet?
479 my ($common_dir, $qunkrefs) =
480 $self->_pretty_file_list(\(%unanimous_tags, %non_unanimous_tags, %all_branches), $qunksref);
482 my @qunkrefs = @$qunkrefs;
484 # Not XML output, so complexly compactify for chordate consumption. At this
485 # point we have enough global information about all the qunks to organize
486 # them non-redundantly for output.
489 # Note that $common_dir still has its trailing slash
490 $beauty .= "$common_dir: ";
495 # For trailing revision numbers.
498 foreach my $branch (keys (%all_branches))
500 foreach my $qunkref (@qunkrefs)
502 if ((defined ($qunkref->branch))
503 and ($qunkref->branch eq $branch))
506 # kff todo: comma-delimited in XML too? Sure.
512 my $fname = substr ($qunkref->filename, length ($common_dir));
514 $qunkref->{'printed'} = 1; # Just setting a mark bit, basically
516 if ( $Show_Tags and defined $qunkref->tags ) {
517 my @tags = grep ($non_unanimous_tags{$_}, @{$qunkref->tags});
520 $beauty .= " (tags: ";
521 $beauty .= join (', ', @tags);
526 if ($Show_Revisions) {
527 # Collect the revision numbers' last components, but don't
528 # print them -- they'll get printed with the branch name
530 $qunkref->revision =~ /.+\.([\d]+)$/;
531 push (@brevisions, $1);
533 # todo: we're still collecting branch roots, but we're not
534 # showing them anywhere. If we do show them, it would be
535 # nifty to just call them revision "0" on a the branch.
536 # Yeah, that's the ticket.
540 $beauty .= " ($branch";
542 if ((scalar (@brevisions)) > 1) {
544 $beauty .= (join (',', @brevisions));
548 # Square brackets are spurious here, since there's no range to
550 $beauty .= ".$brevisions[0]";
557 # Okay; any qunks that were done according to branch are taken care
558 # of, and marked as printed. Now print everyone else.
560 my %fileinfo_printed;
561 foreach my $qunkref (@qunkrefs)
563 next if (defined ($qunkref->{'printed'})); # skip if already printed
565 my $b = substr ($qunkref->filename, length ($common_dir));
566 # todo: Shlomo's change was this:
567 # $beauty .= substr ($qunkref->filename,
568 # (($common_dir eq "./") ? '' : length ($common_dir)));
569 $qunkref->{'printed'} = 1; # Set a mark bit.
571 if ($Show_Revisions || $Show_Tags || $Show_Dead)
573 my $started_addendum = 0;
575 if ($Show_Revisions) {
576 $started_addendum = 1;
578 $b .= $qunkref->revision;
580 if ($Show_Dead && $qunkref->state =~ /dead/)
582 # Deliberately not using $started_addendum. Keeping it simple.
585 if ($Show_Tags && (defined $qunkref->tags)) {
586 my @tags = grep ($non_unanimous_tags{$_}, @{$qunkref->tags});
587 if ((scalar (@tags)) > 0) {
588 if ($started_addendum) {
594 $b .= join (', ', @tags);
595 $started_addendum = 1;
598 if ($started_addendum) {
603 unless ( exists $fileinfo_printed{$b} ) {
609 $beauty .= $b, $fileinfo_printed{$b} = 1;
613 # Unanimous tags always come last.
614 if ($Show_Tags && %unanimous_tags)
616 $beauty .= " (utags: ";
617 $beauty .= join (', ', sort keys (%unanimous_tags));
621 # todo: still have to take care of branch_roots?
623 $beauty = "$beauty:";
628 # -------------------------------------
632 my ($fh, $time, $tag) = @_;
634 my $fdatetime = $self->fdatetime($time);
635 print $fh "$fdatetime tag $tag\n\n";
639 # -------------------------------------
643 my ($msg, $files, $qunklist) = @_;
647 if ( $No_Wrap and ! $Summary ) {
648 $msg = $self->preprocess_msg_text($msg);
649 $files = $self->mywrap("\t", "\t ", "* $files");
650 $msg =~ s/\n(.+)/\n$Indent$1/g;
651 unless ($After_Header eq " ") {
652 $msg =~ s/^(.+)/$Indent$1/g;
654 if ( $Hide_Filenames ) {
655 $body = $After_Header . $msg;
657 $body = $files . $After_Header . $msg;
659 } elsif ( $Summary ) {
660 my ($filelist, $qunk);
661 my (@DeletedQunks, @AddedQunks, @ChangedQunks);
663 $msg = $self->preprocess_msg_text($msg);
665 # Sort the files (qunks) according to the operation that was
666 # performed. Files which were added have no line change
667 # indicator, whereas deleted files have state dead.
669 foreach $qunk ( @$qunklist ) {
670 if ( "dead" eq $qunk->state) {
671 push @DeletedQunks, $qunk;
672 } elsif ( ! defined $qunk->lines ) {
673 push @AddedQunks, $qunk;
675 push @ChangedQunks, $qunk;
679 # The qunks list was originally in tree search order. Let's
680 # get that back. The lists, if they exist, will be reversed upon
685 # Now write the three sections onto $filelist
687 if ( @DeletedQunks ) {
688 $filelist .= "\tDeleted:\n";
689 foreach $qunk ( @DeletedQunks ) {
690 $filelist .= "\t\t" . $qunk->filename;
691 $filelist .= " (" . $qunk->revision . ")";
698 $filelist .= "\tAdded:\n";
699 foreach $qunk (@AddedQunks) {
700 $filelist .= "\t\t" . $qunk->filename;
701 $filelist .= " (" . $qunk->revision . ")";
707 if ( @ChangedQunks ) {
708 $filelist .= "\tChanged:\n";
709 foreach $qunk (@ChangedQunks) {
710 $filelist .= "\t\t" . $qunk->filename;
711 $filelist .= " (" . $qunk->revision . ")";
712 $filelist .= ", \"" . $qunk->state . "\"";
713 $filelist .= ", lines: " . $qunk->lines;
721 if ( $Hide_Filenames ) {
725 $msg =~ s/\n(.*)/\n$Indent$1/g;
726 unless ( $After_Header eq " " or $FSF_Style ) {
727 $msg =~ s/^(.*)/$Indent$1/g;
730 unless ( $No_Wrap ) {
732 $msg = $self->wrap_log_entry($msg, '', 69, 69);
736 $msg = $self->mywrap('', $Indent, "$msg");
737 $msg =~ s/[ \t]+\n/\n/g;
741 $body = $filelist . $After_Header . $msg;
742 } else { # do wrapping, either FSF-style or regular
743 my $latter_wrap = $No_Extra_Indent ? $Indent : "$Indent ";
746 $files = $self->mywrap($Indent, $latter_wrap, "* $files");
748 my $files_last_line_len = 0;
749 if ( $After_Header eq " " ) {
750 $files_last_line_len = $self->last_line_len($files);
751 $files_last_line_len += 1; # for $After_Header
754 $msg = $self->wrap_log_entry($msg, $latter_wrap, 69-$files_last_line_len, 69);
755 $body = $files . $After_Header . $msg;
756 } else { # not FSF-style
757 $msg = $self->preprocess_msg_text($msg);
758 $body = $files . $After_Header . $msg;
759 $body = $self->mywrap($Indent, $latter_wrap, "* $body");
760 $body =~ s/[ \t]+\n/\n/g;
767 # ----------------------------------------------------------------------------
769 package CVS::Utils::ChangeLog::EntrySet::Output::XML;
771 use base qw( CVS::Utils::ChangeLog::EntrySet::Output );
773 use File::Basename qw( fileparse );
777 my $self = $class->SUPER::new(@_);
780 # -------------------------------------
784 my ($time, $author, $lastdate) = @_;
786 my $header_line = '';
790 my ($y, $m, $d, $H, $M, $S) = (gmtime($time))[5,4,3,2,1,0];
792 # Ideally, this would honor $UTC_Times and use +HH:MM syntax
793 $isoDate = sprintf("%04d-%02d-%02dT%02d:%02d:%02dZ",
794 $y + 1900, $m + 1, $d, $H, $M, $S);
796 my (undef,$min,$hour,$mday,$mon,$year,$wday)
797 = $UTC_Times ? gmtime($time) : localtime($time);
799 my $date = $self->fdatetime($time);
800 $wday = $self->wday($wday);
803 sprintf ("<date>%4u-%02u-%02u</date>\n${wday}<time>%02u:%02u</time>\n",
804 $year+1900, $mon+1, $mday, $hour, $min);
805 $header_line .= "<isoDate>$isoDate</isoDate>\n"
806 unless $No_XML_ISO_Date;
807 $header_line .= sprintf("<author>%s</author>\n" , $author);
810 # -------------------------------------
813 my $self = shift; my $class = ref $self;
816 return '<weekday>' . $class->weekday_en($wday) . "</weekday>\n";
819 # -------------------------------------
831 # -------------------------------------
838 length $XML_Encoding ? qq'encoding="$XML_Encoding"' : '';
839 my $version = 'version="1.0"';
841 sprintf '<?xml %s?>', join ' ', grep length, $version, $encoding;
845 '<changelog xmlns="http://www.red-bean.com/xmlns/cvs2cl/">';
846 print $fh "$declaration\n\n$root\n\n";
849 # -------------------------------------
855 print $fh "</changelog>\n";
858 # -------------------------------------
860 sub preprocess_msg_text {
864 $text = $self->SUPER::preprocess_msg_text($text);
866 $text = $self->escape($text);
868 $text = "<msg>${text}</msg>\n";
873 # -------------------------------------
875 # Here we take a bunch of qunks and convert them into a printed
876 # summary that will include all the information the user asked for.
877 sub pretty_file_list {
881 my $beauty = ''; # The accumulating header string for this entry.
882 my %non_unanimous_tags; # Tags found in a proper subset of qunks
883 my %unanimous_tags; # Tags found in all qunks
884 my %all_branches; # Branches found in any qunk
885 my $fbegun = 0; # Did we begin printing filenames yet?
887 my ($common_dir, $qunkrefs) =
888 $self->_pretty_file_list(\(%unanimous_tags, %non_unanimous_tags, %all_branches),
891 my @qunkrefs = @$qunkrefs;
893 # If outputting XML, then our task is pretty simple, because we
894 # don't have to detect common dir, common tags, branch prefixing,
895 # etc. We just output exactly what we have, and don't worry about
896 # redundancy or readability.
898 foreach my $qunkref (@qunkrefs)
900 my $filename = $qunkref->filename;
901 my $state = $qunkref->state;
902 my $revision = $qunkref->revision;
903 my $tags = $qunkref->tags;
904 my $branch = $qunkref->branch;
905 my $branchroots = $qunkref->roots;
906 my $lines = $qunkref->lines;
908 $filename = $self->escape($filename); # probably paranoia
909 $revision = $self->escape($revision); # definitely paranoia
911 $beauty .= "<file>\n";
912 $beauty .= "<name>${filename}</name>\n";
913 $beauty .= "<cvsstate>${state}</cvsstate>\n";
914 $beauty .= "<revision>${revision}</revision>\n";
916 if ($Show_Lines_Modified
917 && $lines && $lines =~ m/\+(\d+)\s+-(\d+)/) {
918 $beauty .= "<linesadded>$1</linesadded>\n";
919 $beauty .= "<linesremoved>$2</linesremoved>\n";
923 $branch = $self->escape($branch); # more paranoia
924 $beauty .= "<branch>${branch}</branch>\n";
926 foreach my $tag (@$tags) {
927 $tag = $self->escape($tag); # by now you're used to the paranoia
928 $beauty .= "<tag>${tag}</tag>\n";
930 foreach my $root (@$branchroots) {
931 $root = $self->escape($root); # which is good, because it will continue
932 $beauty .= "<branchroot>${root}</branchroot>\n";
934 $beauty .= "</file>\n";
937 # Theoretically, we could go home now. But as long as we're here,
938 # let's print out the common_dir and utags, as a convenience to
939 # the receiver (after all, earlier code calculated that stuff
940 # anyway, so we might as well take advantage of it).
942 if ((scalar (keys (%unanimous_tags))) > 1) {
943 foreach my $utag ((keys (%unanimous_tags))) {
944 $utag = $self->escape($utag); # the usual paranoia
945 $beauty .= "<utag>${utag}</utag>\n";
949 $common_dir = $self->escape($common_dir);
950 $beauty .= "<commondir>${common_dir}</commondir>\n";
953 # That's enough for XML, time to go home:
957 # -------------------------------------
961 my ($fh, $time, $tag) = @_;
963 my ($y, $m, $d, $H, $M, $S) = (gmtime($time))[5,4,3,2,1,0];
965 # Ideally, this would honor $UTC_Times and use +HH:MM syntax
966 my $isoDate = sprintf("%04d-%02d-%02dT%02d:%02d:%02dZ",
967 $y + 1900, $m + 1, $d, $H, $M, $S);
969 print $fh "<tagdate>\n";
970 print $fh "<tagisodate>$isoDate</tagisodate>\n";
971 print $fh "<tagdatetag>$tag</tagdatetag>\n";
972 print $fh "</tagdate>\n\n";
976 # -------------------------------------
980 my ($fh, $entry) = @_;
981 print $fh "<entry>\n$entry</entry>\n\n";
984 # -------------------------------------
988 my ($msg, $files, $qunklist) = @_;
990 $msg = $self->preprocess_msg_text($msg);
991 return $files . $msg;
994 # ----------------------------------------------------------------------------
996 package CVS::Utils::ChangeLog::EntrySet::Output;
998 use Carp qw( croak );
999 use File::Basename qw( fileparse );
1001 # Class Utility Functions -------------
1005 my @weekdays = (qw(Sunday Monday Tuesday Wednesday Thursday Friday Saturday));
1008 return $weekdays[$_[0]];
1013 # -------------------------------------
1016 my ($proto, %args) = @_;
1017 my $class = ref $proto || $proto;
1019 my $follow_branches = delete $args{follow_branches};
1020 my $follow_only = delete $args{follow_only};
1021 my $ignore_tags = delete $args{ignore_tags};
1022 my $show_tags = delete $args{show_tags};
1023 die "Unrecognized arg to EntrySet::Output::new: '$_'\n"
1026 bless +{follow_branches => $follow_branches,
1027 follow_only => $follow_only,
1028 show_tags => $show_tags,
1029 ignore_tags => $ignore_tags,
1033 # Abstract Subrs ----------------------
1035 sub wday { croak "Whoops. Abtract method call (wday).\n" }
1036 sub pretty_file_list { croak "Whoops. Abtract method call (pretty_file_list).\n" }
1037 sub output_tagdate { croak "Whoops. Abtract method call (output_tagdate).\n" }
1038 sub header_line { croak "Whoops. Abtract method call (header_line).\n" }
1040 # Instance Subrs ----------------------
1042 sub output_header { }
1044 # -------------------------------------
1048 my ($fh, $entry) = @_;
1049 print $fh "$entry\n";
1052 # -------------------------------------
1054 sub output_footer { }
1056 # -------------------------------------
1058 sub escape { return $_[1] }
1060 # -------------------------------------
1062 sub _revision_is_wanted {
1063 my ($self, $qunk) = @_;
1065 my ($revision, $branch_numbers) = @{$qunk}{qw( revision branch_numbers )};
1066 my $follow_branches = $self->{follow_branches};
1067 my $follow_only = $self->{follow_only};
1069 for my $ignore_tag (keys %{$self->{ignore_tags}}) {
1071 if defined $qunk->{tags} and grep $_ eq $ignore_tag, @{$qunk->{tags}};
1074 if ( keys %{$self->{show_tags}} ) {
1075 for my $show_tag (keys %{$self->{show_tags}}) {
1077 if ! defined $qunk->{tags} or ! grep $_ eq $show_tag, @{$qunk->{tags}};
1082 unless @$follow_branches + @$follow_only; # no follow is follow all
1084 for my $x (map([$_, 1], @$follow_branches),
1085 map([$_, 0], @$follow_only )) {
1086 my ($branch, $followsub) = @$x;
1088 # Special case for following trunk revisions
1090 if $branch =~ /^trunk$/i and $revision =~ /^[0-9]+\.[0-9]+$/;
1092 if ( my $branch_number = $branch_numbers->{$branch} ) {
1093 # Are we on one of the follow branches or an ancestor of same?
1095 # If this revision is a prefix of the branch number, or possibly is less
1096 # in the minormost number, OR if this branch number is a prefix of the
1097 # revision, then yes. Otherwise, no.
1099 # So below, we determine if any of those conditions are met.
1101 # Trivial case: is this revision on the branch? (Compare this way to
1102 # avoid regexps that screw up Emacs indentation, argh.)
1103 if ( substr($revision, 0, (length($branch_number) + 1))
1105 ($branch_number . ".") ) {
1108 # } elsif ( length($revision) == length($branch_number)+2 ) {
1109 } elsif ( substr($revision, length($branch_number)+1) =~ /^\d+$/ ) {
1112 } elsif ( length($branch_number) > length($revision)
1115 # Non-trivial case: check if rev is ancestral to branch
1117 # r_left still has the trailing "."
1118 my ($r_left, $r_end) = ($revision =~ /^((?:\d+\.)+)(\d+)$/);
1120 # b_left still has trailing "."
1121 # b_mid has no trailing "."
1122 my ($b_left, $b_mid) = ($branch_number =~ /^((?:\d+\.)+)(\d+)\.\d+$/);
1124 if $r_left eq $b_left and $r_end <= $b_mid;
1132 # -------------------------------------
1134 sub output_changelog {
1135 my $self = shift; my $class = ref $self;
1136 my ($grand_poobah) = @_;
1137 ### Process each ChangeLog
1139 while (my ($dir,$authorhash) = each %$grand_poobah)
1141 &main::debug ("DOING DIR: $dir\n");
1143 # Here we twist our hash around, from being
1144 # author => time => message => filelist
1145 # in %$authorhash to
1146 # time => author => message => filelist
1149 # This is also where we merge entries. The algorithm proceeds
1150 # through the timeline of the changelog with a sliding window of
1151 # $Max_Checkin_Duration seconds; within that window, entries that
1152 # have the same log message are merged.
1154 # (To save space, we zap %$authorhash after we've copied
1155 # everything out of it.)
1158 while (my ($author,$timehash) = each %$authorhash)
1161 foreach my $time (sort {$a <=> $b} (keys %$timehash))
1163 my $msghash = $timehash->{$time};
1164 while (my ($msg,$qunklist) = each %$msghash)
1166 my $stamptime = $stamptime{$msg};
1167 if ((defined $stamptime)
1168 and (($time - $stamptime) < $Max_Checkin_Duration)
1169 and (defined $changelog{$stamptime}{$author}{$msg}))
1171 push(@{$changelog{$stamptime}{$author}{$msg}}, $qunklist->files);
1174 $changelog{$time}{$author}{$msg} = $qunklist->files;
1175 $stamptime{$msg} = $time;
1180 undef (%$authorhash);
1182 ### Now we can write out the ChangeLog!
1184 my ($logfile_here, $logfile_bak, $tmpfile);
1187 if (! $Output_To_Stdout) {
1188 $logfile_here = $dir . $Log_File_Name;
1189 $logfile_here =~ s/^\.\/\//\//; # fix any leading ".//" problem
1190 $tmpfile = "${logfile_here}.cvs2cl$$.tmp";
1191 $logfile_bak = "${logfile_here}.bak";
1193 open (LOG_OUT, ">$tmpfile") or die "Unable to open \"$tmpfile\"";
1196 open (LOG_OUT, ">-") or die "Unable to open stdout for writing";
1199 print LOG_OUT $ChangeLog_Header;
1201 my %tag_date_printed;
1203 $self->output_header(\*LOG_OUT);
1206 if($Chronological_Order) {
1207 @key_list = sort {$a <=> $b} (keys %changelog);
1209 @key_list = sort {$b <=> $a} (keys %changelog);
1211 foreach my $time (@key_list)
1213 next if ($Delta_Mode &&
1214 (($time <= $Delta_StartTime) ||
1215 ($time > $Delta_EndTime && $Delta_EndTime)));
1217 # Set up the date/author line.
1218 # kff todo: do some more XML munging here, on the header
1219 # part of the entry:
1220 my (undef,$min,$hour,$mday,$mon,$year,$wday)
1221 = $UTC_Times ? gmtime($time) : localtime($time);
1223 $wday = $self->wday($wday);
1224 # XML output includes everything else, we might as well make
1225 # it always include Day Of Week too, for consistency.
1226 my $authorhash = $changelog{$time};
1227 if ( $Show_Tag_Dates || $XML_Output ) {
1229 while (my ($author,$mesghash) = each %$authorhash) {
1230 while (my ($msg,$qunk) = each %$mesghash) {
1231 for my $qunkref2 (@$qunk) {
1232 if (defined ($qunkref2->tags)) {
1233 for my $tag (@{$qunkref2->tags}) {
1240 # Sort here for determinism to ease testing
1241 foreach my $tag (sort keys %tags) {
1242 if ( ! defined $tag_date_printed{$tag} ) {
1243 $tag_date_printed{$tag} = $time;
1244 $self->output_tagdate(\*LOG_OUT, $time, $tag);
1248 while (my ($author,$mesghash) = each %$authorhash)
1250 # If XML, escape in outer loop to avoid compound quoting:
1251 $author = $self->escape($author);
1254 # We sort here to enable predictable ordering for the testing porpoises
1255 for my $msg (sort keys %$mesghash)
1257 my $qunklist = $mesghash->{$msg};
1260 grep $self->_revision_is_wanted($_), @$qunklist;
1262 next FOOBIE unless @qunklist;
1264 my $files = $self->pretty_file_list(\@qunklist);
1265 my $header_line; # date and author
1266 my $wholething; # $header_line + $body
1268 my $date = $self->fdatetime($time);
1269 $header_line = $self->header_line($time, $author, $lastdate);
1272 $Text::Wrap::huge = 'overflow'
1273 if $Text::Wrap::VERSION >= 2001.0130;
1274 # Reshape the body according to user preferences.
1275 my $body = $self->format_body($msg, $files, \@qunklist);
1277 $body =~ s/[ \t]+\n/\n/g;
1278 $wholething = $header_line . $body;
1280 # One last check: make sure it passes the regexp test, if the
1281 # user asked for that. We have to do it here, so that the
1282 # test can match against information in the header as well
1283 # as in the text of the log message.
1285 # How annoying to duplicate so much code just because I
1286 # can't figure out a way to evaluate scalars on the trailing
1287 # operator portion of a regular expression. Grrr.
1288 if ($Case_Insensitive) {
1289 unless ( $Regexp_Gate and ( $wholething !~ /$Regexp_Gate/oi ) ) {
1290 $self->output_entry(\*LOG_OUT, $wholething);
1294 unless ( $Regexp_Gate and ( $wholething !~ /$Regexp_Gate/o ) ) {
1295 $self->output_entry(\*LOG_OUT, $wholething);
1302 $self->output_footer(\*LOG_OUT);
1306 if ( ! $Output_To_Stdout ) {
1307 # If accumulating, append old data to new before renaming. But
1308 # don't append the most recent entry, since it's already in the
1309 # new log due to CVS's idiosyncratic interpretation of "log -d".
1310 if ($Cumulative && -f $logfile_here) {
1311 open NEW_LOG, ">>$tmpfile"
1312 or die "trouble appending to $tmpfile ($!)";
1314 open OLD_LOG, "<$logfile_here"
1315 or die "trouble reading from $logfile_here ($!)";
1317 my $started_first_entry = 0;
1318 my $passed_first_entry = 0;
1320 if ( ! $passed_first_entry ) {
1321 if ( ( ! $started_first_entry )
1322 and /^(\d\d\d\d-\d\d-\d\d\s+(\w+\s+)?\d\d:\d\d)/ ) {
1323 $started_first_entry = 1;
1324 } elsif ( /^(\d\d\d\d-\d\d-\d\d\s+(\w+\s+)?\d\d:\d\d)/ ) {
1325 $passed_first_entry = 1;
1337 if ( -f $logfile_here ) {
1338 rename $logfile_here, $logfile_bak;
1340 rename $tmpfile, $logfile_here;
1345 # -------------------------------------
1347 # Don't call this wrap, because with 5.5.3, that clashes with the
1348 # (unconditional :-( ) export of wrap() from Text::Wrap
1351 my ($indent1, $indent2, @text) = @_;
1352 # If incoming text looks preformatted, don't get clever
1353 my $text = Text::Wrap::wrap($indent1, $indent2, @text);
1354 if ( grep /^\s+/m, @text ) {
1357 my @lines = split /\n/, $text;
1358 $indent2 =~ s!^((?: {8})+)!"\t" x (length($1)/8)!e;
1359 $lines[0] =~ s/^$indent1\s+/$indent1/;
1360 s/^$indent2\s+/$indent2/
1361 for @lines[1..$#lines];
1362 my $newtext = join "\n", @lines;
1364 if substr($text, -1) eq "\n";
1368 # -------------------------------------
1370 sub preprocess_msg_text {
1374 # Strip out carriage returns (as they probably result from DOSsy editors).
1375 $text =~ s/\r\n/\n/g;
1376 # If it *looks* like two newlines, make it *be* two newlines:
1377 $text =~ s/\n\s*\n/\n\n/g;
1382 # -------------------------------------
1387 my $files_list = shift;
1388 my @lines = split (/\n/, $files_list);
1389 my $last_line = pop (@lines);
1390 return length ($last_line);
1393 # -------------------------------------
1395 # A custom wrap function, sensitive to some common constructs used in
1397 sub wrap_log_entry {
1400 my $text = shift; # The text to wrap.
1401 my $left_pad_str = shift; # String to pad with on the left.
1403 # These do NOT take left_pad_str into account:
1404 my $length_remaining = shift; # Amount left on current line.
1405 my $max_line_length = shift; # Amount left for a blank line.
1407 my $wrapped_text = ''; # The accumulating wrapped entry.
1408 my $user_indent = ''; # Inherited user_indent from prev line.
1410 my $first_time = 1; # First iteration of the loop?
1411 my $suppress_line_start_match = 0; # Set to disable line start checks.
1413 my @lines = split (/\n/, $text);
1414 while (@lines) # Don't use `foreach' here, it won't work.
1416 my $this_line = shift (@lines);
1419 if ($this_line =~ /^(\s+)/) {
1426 # If it matches any of the line-start regexps, print a newline now...
1427 if ($suppress_line_start_match)
1429 $suppress_line_start_match = 0;
1431 elsif (($this_line =~ /^(\s*)\*\s+[a-zA-Z0-9]/)
1432 || ($this_line =~ /^(\s*)\* [a-zA-Z0-9_\.\/\+-]+/)
1433 || ($this_line =~ /^(\s*)\([a-zA-Z0-9_\.\/\+-]+(\)|,\s*)/)
1434 || ($this_line =~ /^(\s+)(\S+)/)
1435 || ($this_line =~ /^(\s*)- +/)
1436 || ($this_line =~ /^()\s*$/)
1437 || ($this_line =~ /^(\s*)\*\) +/)
1438 || ($this_line =~ /^(\s*)[a-zA-Z0-9](\)|\.|\:) +/))
1440 # Make a line break immediately, unless header separator is set
1441 # and this line is the first line in the entry, in which case
1442 # we're getting the blank line for free already and shouldn't
1444 unless (($After_Header ne " ") and ($first_time))
1446 if ($this_line =~ /^()\s*$/) {
1447 $suppress_line_start_match = 1;
1448 $wrapped_text .= "\n${left_pad_str}";
1451 $wrapped_text .= "\n${left_pad_str}";
1454 $length_remaining = $max_line_length - (length ($user_indent));
1457 # Now that any user_indent has been preserved, strip off leading
1458 # whitespace, so up-folding has no ugly side-effects.
1459 $this_line =~ s/^\s*//;
1461 # Accumulate the line, and adjust parameters for next line.
1462 my $this_len = length ($this_line);
1465 # Blank lines should cancel any user_indent level.
1467 $length_remaining = $max_line_length;
1469 elsif ($this_len >= $length_remaining) # Line too long, try breaking it.
1471 # Walk backwards from the end. At first acceptable spot, break
1473 my $idx = $length_remaining - 1;
1474 if ($idx < 0) { $idx = 0 };
1477 if (substr ($this_line, $idx, 1) =~ /\s/)
1479 my $line_now = substr ($this_line, 0, $idx);
1480 my $next_line = substr ($this_line, $idx);
1481 $this_line = $line_now;
1483 # Clean whitespace off the end.
1486 # The current line is ready to be printed.
1487 $this_line .= "\n${left_pad_str}";
1489 # Make sure the next line is allowed full room.
1490 $length_remaining = $max_line_length - (length ($user_indent));
1492 # Strip next_line, but then preserve any user_indent.
1493 $next_line =~ s/^\s*//;
1495 # Sneak a peek at the user_indent of the upcoming line, so
1496 # $next_line (which will now precede it) can inherit that
1497 # indent level. Otherwise, use whatever user_indent level
1498 # we currently have, which might be none.
1499 my $next_next_line = shift (@lines);
1500 if ((defined ($next_next_line)) && ($next_next_line =~ /^(\s+)/)) {
1501 $next_line = $1 . $next_line if (defined ($1));
1502 # $length_remaining = $max_line_length - (length ($1));
1503 $next_next_line =~ s/^\s*//;
1506 $next_line = $user_indent . $next_line;
1508 if (defined ($next_next_line)) {
1509 unshift (@lines, $next_next_line);
1511 unshift (@lines, $next_line);
1513 # Our new next line might, coincidentally, begin with one of
1514 # the line-start regexps, so we temporarily turn off
1515 # sensitivity to that until we're past the line.
1516 $suppress_line_start_match = 1;
1528 # We bottomed out because the line is longer than the
1529 # available space. But that could be because the space is
1530 # small, or because the line is longer than even the maximum
1531 # possible space. Handle both cases below.
1533 if ($length_remaining == ($max_line_length - (length ($user_indent))))
1535 # The line is simply too long -- there is no hope of ever
1536 # breaking it nicely, so just insert it verbatim, with
1537 # appropriate padding.
1538 $this_line = "\n${left_pad_str}${this_line}";
1542 # Can't break it here, but may be able to on the next round...
1543 unshift (@lines, $this_line);
1544 $length_remaining = $max_line_length - (length ($user_indent));
1545 $this_line = "\n${left_pad_str}";
1549 else # $this_len < $length_remaining, so tack on what we can.
1551 # Leave a note for the next iteration.
1552 $length_remaining = $length_remaining - $this_len;
1554 if ($this_line =~ /\.$/)
1557 $length_remaining -= 2;
1559 else # not a sentence end
1562 $length_remaining -= 1;
1566 # Unconditionally indicate that loop has run at least once.
1569 $wrapped_text .= "${user_indent}${this_line}";
1572 # One last bit of padding.
1573 $wrapped_text .= "\n";
1575 return $wrapped_text;
1578 # -------------------------------------
1580 sub _pretty_file_list {
1583 my ($unanimous_tags, $non_unanimous_tags, $all_branches, $qunksref) = @_;
1586 grep +( ( ! $_->tags_exists
1588 ! grep exists $ignore_tags{$_}, @{$_->tags})
1594 grep exists $show_tags{$_}, @{$_->tags} )
1599 my $common_dir; # Dir prefix common to all files ('' if none)
1601 # First, loop over the qunks gathering all the tag/branch names.
1602 # We'll put them all in non_unanimous_tags, and take out the
1603 # unanimous ones later.
1605 foreach my $qunkref (@qunkrefs)
1607 # Keep track of whether all the files in this commit were in the
1608 # same directory, and memorize it if so. We can make the output a
1609 # little more compact by mentioning the directory only once.
1610 if ($Common_Dir && (scalar (@qunkrefs)) > 1)
1612 if (! (defined ($common_dir)))
1615 ($base, $dir, undef) = fileparse ($qunkref->filename);
1617 if ((! (defined ($dir))) # this first case is sheer paranoia
1629 elsif ($common_dir ne '')
1631 # Already have a common dir prefix, so how much of it can we preserve?
1632 $common_dir = &main::common_path_prefix ($qunkref->filename, $common_dir);
1635 else # only one file in this entry anyway, so common dir not an issue
1640 if (defined ($qunkref->branch)) {
1641 $all_branches->{$qunkref->branch} = 1;
1643 if (defined ($qunkref->tags)) {
1644 foreach my $tag (@{$qunkref->tags}) {
1645 $non_unanimous_tags->{$tag} = 1;
1650 # Any tag held by all qunks will be printed specially... but only if
1651 # there are multiple qunks in the first place!
1652 if ((scalar (@qunkrefs)) > 1) {
1653 foreach my $tag (keys (%$non_unanimous_tags)) {
1654 my $everyone_has_this_tag = 1;
1655 foreach my $qunkref (@qunkrefs) {
1656 if ((! (defined ($qunkref->tags)))
1657 or (! (grep ($_ eq $tag, @{$qunkref->tags})))) {
1658 $everyone_has_this_tag = 0;
1661 if ($everyone_has_this_tag) {
1662 $unanimous_tags->{$tag} = 1;
1663 delete $non_unanimous_tags->{$tag};
1668 return $common_dir, \@qunkrefs;
1671 # -------------------------------------
1676 my ($year, $mday, $mon, $wday, $hour, $min);
1679 ($year, $mday, $mon, $wday, $hour, $min) = @_;
1682 (undef, $min, $hour, $mday, $mon, $year, $wday) =
1683 $UTC_Times ? gmtime($time) : localtime($time);
1687 $wday = $self->wday($wday);
1690 my $fdate = $self->fdate($year, $mon, $mday, $wday);
1693 my $ftime = $self->ftime($hour, $min);
1694 return "$fdate $ftime";
1700 # -------------------------------------
1705 my ($year, $mday, $mon, $wday);
1708 ($year, $mon, $mday, $wday) = @_;
1711 (undef, undef, undef, $mday, $mon, $year, $wday) =
1712 $UTC_Times ? gmtime($time) : localtime($time);
1716 $wday = $self->wday($wday);
1719 return sprintf '%4u-%02u-%02u%s', $year, $mon, $mday, $wday;
1722 # -------------------------------------
1733 (undef, $min, $hour) = $UTC_Times ? gmtime($time) : localtime($time);
1736 return sprintf '%02u:%02u', $hour, $min;
1739 # ----------------------------------------------------------------------------
1741 package CVS::Utils::ChangeLog::Message;
1747 my %self = (msg => $msg, files => []);
1749 bless \%self, $class;
1754 my ($fileentry) = @_;
1756 die "Not a fileentry: $fileentry"
1757 unless $fileentry->isa('CVS::Utils::ChangeLog::FileEntry');
1759 push @{$self->{files}}, $fileentry;
1762 sub files { wantarray ? @{$_[0]->{files}} : $_[0]->{files} }
1764 # ----------------------------------------------------------------------------
1766 package CVS::Utils::ChangeLog::FileEntry;
1768 use File::Basename qw( fileparse );
1770 # Each revision of a file has a little data structure (a `qunk')
1771 # associated with it. That data structure holds not only the
1772 # file's name, but any additional information about the file
1773 # that might be needed in the output, such as the revision
1774 # number, tags, branches, etc. The reason to have these things
1775 # arranged in a data structure, instead of just appending them
1776 # textually to the file's name, is that we may want to do a
1777 # little rearranging later as we write the output. For example,
1778 # all the files on a given tag/branch will go together, followed
1779 # by the tag in parentheses (so trunk or otherwise non-tagged
1780 # files would go at the end of the file list for a given log
1781 # message). This rearrangement is a lot easier to do if we
1782 # don't have to reparse the text.
1784 # A qunk looks like this:
1787 # filename => "hello.c",
1788 # revision => "1.4.3.2",
1789 # time => a timegm() return value (moment of commit)
1790 # tags => [ "tag1", "tag2", ... ],
1791 # branch => "branchname" # There should be only one, right?
1792 # roots => [ "branchtag1", "branchtag2", ... ]
1793 # lines => "+x -y" # or undefined; x and y are integers
1796 # Single top-level ChangeLog, or one per subdirectory?
1798 sub distributed { $#_ ? ($distributed = $_[1]) : $distributed; }
1802 my ($path, $time, $revision, $state, $lines,
1803 $branch_names, $branch_roots, $branch_numbers, $symbolic_names) = @_;
1805 my %self = (time => $time,
1806 revision => $revision,
1809 branch_numbers => $branch_numbers,
1812 if ( $distributed ) {
1813 @self{qw(filename dir_key)} = fileparse($path);
1815 @self{qw(filename dir_key)} = ($path, './');
1818 { # Scope for $branch_prefix
1819 (my ($branch_prefix) = ($revision =~ /((?:\d+\.)+)\d+/));
1820 $branch_prefix =~ s/\.$//;
1821 if ( $branch_names->{$branch_prefix} ) {
1822 my $branch_name = $branch_names->{$branch_prefix};
1823 $self{branch} = $branch_name;
1824 $self{branches} = [$branch_name];
1826 while ( $branch_prefix =~ s/^(\d+(?:\.\d+\.\d+)+)\.\d+\.\d+$/$1/ ) {
1827 push @{$self{branches}}, $branch_names->{$branch_prefix}
1828 if exists $branch_names->{$branch_prefix};
1832 # If there's anything in the @branch_roots array, then this
1833 # revision is the root of at least one branch. We'll display
1834 # them as branch names instead of revision numbers, the
1835 # substitution for which is done directly in the array:
1836 $self{'roots'} = [ map { $branch_names->{$_} } @$branch_roots ]
1839 if ( exists $symbolic_names->{$revision} ) {
1840 $self{tags} = delete $symbolic_names->{$revision};
1841 &main::delta_check($time, $self{tags});
1844 bless \%self, $class;
1847 sub filename { $_[0]->{filename} }
1848 sub dir_key { $_[0]->{dir_key} }
1849 sub revision { $_[0]->{revision} }
1850 sub branch { $_[0]->{branch} }
1851 sub state { $_[0]->{state} }
1852 sub lines { $_[0]->{lines} }
1853 sub roots { $_[0]->{roots} }
1854 sub branch_numbers { $_[0]->{branch_numbers} }
1856 sub tags { $_[0]->{tags} }
1858 exists $_[0]->{tags};
1861 # This may someday be used in a more sophisticated calculation of what other
1862 # files are involved in this commit. For now, we don't use it much except for
1863 # delta mode, because the common-commit-detection algorithm is hypothesized to
1864 # be "good enough" as it stands.
1865 sub time { $_[0]->{time} }
1867 # ----------------------------------------------------------------------------
1869 package CVS::Utils::ChangeLog::EntrySetBuilder;
1871 use File::Basename qw( fileparse );
1872 use Time::Local qw( timegm );
1874 use constant MAILNAME => "/etc/mailname";
1876 # In 'cvs log' output, one long unbroken line of equal signs separates files:
1877 use constant FILE_SEPARATOR => '=' x 77;# . "\n";
1878 # In 'cvs log' output, a shorter line of dashes separates log messages within
1880 use constant REV_SEPARATOR => '-' x 28;# . "\n";
1882 use constant EMPTY_LOG_MESSAGE => '*** empty log message ***';
1884 # -------------------------------------
1888 my $class = ref $proto || $proto;
1890 my $poobah = CVS::Utils::ChangeLog::EntrySet->new;
1891 my $self = bless +{ grand_poobah => $poobah }, $class;
1894 $self->maybe_read_user_map_file;
1898 # -------------------------------------
1903 # Make way for the next message
1904 undef $self->{rev_msg};
1905 undef $self->{rev_time};
1906 undef $self->{rev_revision};
1907 undef $self->{rev_author};
1908 undef $self->{rev_state};
1909 undef $self->{lines};
1910 $self->{rev_branch_roots} = []; # For showing which files are branch
1912 $self->{collecting_symbolic_names} = 0;
1915 # -------------------------------------
1921 undef $self->{filename};
1922 $self->{branch_names} = +{}; # We'll grab branch names while we're
1924 $self->{branch_numbers} = +{}; # Save some revisions for
1926 $self->{symbolic_names} = +{}; # Where tag names get stored.
1929 # -------------------------------------
1931 sub grand_poobah { $_[0]->{grand_poobah} }
1933 # -------------------------------------
1935 sub read_changelog {
1936 my ($self, $command) = @_;
1938 local (*READER, *WRITER);
1940 if (! $Input_From_Stdin) {
1941 pipe(READER, WRITER)
1942 or die "Couldn't form pipe: $!\n";
1944 die "Couldn't fork: $!\n"
1946 if ( ! $pid ) { # child
1947 open STDOUT, '>&=' . fileno WRITER
1948 or die "Couldn't dup stderr to ", fileno WRITER, "\n";
1949 # strangely, some perls give spurious warnings about STDIN being opened
1950 # for output only these close calls precede the STDOUT reopen above.
1951 # I think they must be reusing fd 1.
1960 &main::debug ("(run \"@$command\")\n");
1963 open READER, '-' or die "unable to open stdin for reading";
1973 # If on a new file and don't see filename, skip until we find it, and
1974 # when we find it, grab it.
1975 if ( ! defined $self->{filename} ) {
1976 $self->read_file_path($_);
1977 } elsif ( /^symbolic names:$/ ) {
1978 $self->{collecting_symbolic_names} = 1;
1979 } elsif ( $self->{collecting_symbolic_names} ) {
1980 $self->read_symbolic_name($_);
1981 } elsif ( $_ eq FILE_SEPARATOR and ! defined $self->{rev_revision} ) {
1983 } elsif ( ! defined $self->{rev_revision} ) {
1984 # If have file name, but not revision, and see revision, then grab
1985 # it. (We collect unconditionally, even though we may or may not
1987 $self->read_revision($_);
1988 } elsif ( ! defined $self->{rev_time} ) { # and /^date: /) {
1989 $self->read_date_author_and_state($_);
1990 } elsif ( /^branches:\s+(.*);$/ ) {
1991 $self->read_branches($1);
1992 } elsif ( ! ( $_ eq FILE_SEPARATOR or $_ eq REV_SEPARATOR ) ) {
1993 # If have file name, time, and author, then we're just grabbing
1994 # log message texts:
1995 $self->{rev_msg} .= $_ . "\n"; # Normally, just accumulate the message...
1998 if ( ! $self->{rev_msg}
1999 or $self->{rev_msg} =~ /^\s*(\.\s*)?$/
2000 or index($self->{rev_msg}, EMPTY_LOG_MESSAGE) > -1 ) {
2001 # ... until a msg separator is encountered:
2002 # Ensure the message contains something:
2003 $self->clear_msg, $noadd = 1
2004 if $Prune_Empty_Msgs;
2005 $self->{rev_msg} = "[no log message]\n";
2008 $self->add_file_entry
2011 if ( $_ eq FILE_SEPARATOR ) {
2020 or die "Couldn't close pipe reader: $!\n";
2021 if ( defined $pid ) {
2025 or $!=1, die sprintf("Problem reading log input (pid/exit/signal/core: %d/%d/%d/%d)\n",
2026 $pid, $? >> 8, $? & 127, $? & 128);
2031 # -------------------------------------
2033 sub add_file_entry {
2034 $_[0]->grand_poobah->add_fileentry(@{$_[0]}{qw(filename rev_time rev_revision
2035 rev_state lines branch_names
2039 rev_author rev_msg)});
2042 # -------------------------------------
2044 sub maybe_read_user_map_file {
2052 if ( $User_Map_File =~ m{^([-\w\@+=.,\/]+):([-\w\@+=.,\/:]+)} and
2053 !-f $User_Map_File )
2055 my $rsh = (exists $ENV{'CVS_RSH'} ? $ENV{'CVS_RSH'} : 'ssh');
2056 $User_Map_Input = "$rsh $1 'cat $2' |";
2057 &main::debug ("(run \"${User_Map_Input}\")\n");
2061 $User_Map_Input = "<$User_Map_File";
2064 open (MAPFILE, $User_Map_Input)
2065 or die ("Unable to open $User_Map_File ($!)");
2069 next if /^\s*#/; # Skip comment lines.
2070 next if not /:/; # Skip lines without colons.
2072 # It is now safe to split on ':'.
2073 my ($username, $expansion) = split ':';
2075 $expansion =~ s/^'(.*)'$/$1/;
2076 $expansion =~ s/^"(.*)"$/$1/;
2078 # If it looks like the expansion has a real name already, then
2079 # we toss the username we got from CVS log. Otherwise, keep
2080 # it to use in combination with the email address.
2082 if ($expansion =~ /^\s*<{0,1}\S+@.*/) {
2083 # Also, add angle brackets if none present
2084 if (! ($expansion =~ /<\S+@\S+>/)) {
2085 $expansions{$username} = "$username <$expansion>";
2088 $expansions{$username} = "$username $expansion";
2092 $expansions{$username} = $expansion;
2094 } # fi ($User_Map_File)
2099 if (defined $User_Passwd_File)
2101 if ( ! defined $Domain ) {
2102 if ( -e MAILNAME ) {
2103 chomp($Domain = slurp_file(MAILNAME));
2106 for ([qw(hostname -d)], 'dnsdomainname', 'domainname') {
2107 my ($text, $exit, $sig, $core) = run_ext($_);
2108 if ( $exit == 0 && $sig == 0 && $core == 0 ) {
2110 if ( length $text ) {
2112 last MAILDOMAIN_CMD;
2119 die "No mail domain found\n"
2120 unless defined $Domain;
2122 open (MAPFILE, "<$User_Passwd_File")
2123 or die ("Unable to open $User_Passwd_File ($!)");
2126 # all lines are valid
2127 my ($username, $pw, $uid, $gid, $gecos, $homedir, $shell) = split ':';
2129 ($expansion) = split (',', $gecos)
2130 if defined $gecos && length $gecos;
2132 my $mailname = $Domain eq '' ? $username : "$username\@$Domain";
2133 $expansions{$username} = "$expansion <$mailname>";
2138 $self->{usermap} = \%expansions;
2141 # -------------------------------------
2143 sub read_file_path {
2144 my ($self, $line) = @_;
2148 if ( $line =~ /^Working file: (.*)/ ) {
2150 } elsif ( defined $RCS_Root
2152 $line =~ m|^RCS file: $RCS_Root[/\\](.*),v$| ) {
2154 $path =~ s!Attic/!!;
2159 if ( @Ignore_Files ) {
2161 ($base, undef, undef) = fileparse($path);
2163 my $xpath = $Case_Insensitive ? lc($path) : $path;
2165 if grep $path =~ /$_/, @Ignore_Files;
2168 $self->{filename} = $path;
2172 # -------------------------------------
2174 sub read_symbolic_name {
2175 my ($self, $line) = @_;
2177 # All tag names are listed with whitespace in front in cvs log
2178 # output; so if see non-whitespace, then we're done collecting.
2180 $self->{collecting_symbolic_names} = 0;
2183 # we're looking at a tag name, so parse & store it
2185 # According to the Cederqvist manual, in node "Tags", tag names must start
2186 # with an uppercase or lowercase letter and can contain uppercase and
2187 # lowercase letters, digits, `-', and `_'. However, it's not our place to
2188 # enforce that, so we'll allow anything CVS hands us to be a tag:
2189 my ($tag_name, $tag_rev) = ($line =~ /^\s+([^:]+): ([\d.]+)$/);
2191 # A branch number either has an odd number of digit sections
2192 # (and hence an even number of dots), or has ".0." as the
2193 # second-to-last digit section. Test for these conditions.
2194 my $real_branch_rev = '';
2195 if ( $tag_rev =~ /^(\d+\.\d+\.)+\d+$/ # Even number of dots...
2197 $tag_rev !~ /^(1\.)+1$/ ) { # ...but not "1.[1.]1"
2198 $real_branch_rev = $tag_rev;
2199 } elsif ($tag_rev =~ /(\d+\.(\d+\.)+)0.(\d+)/) { # Has ".0."
2200 $real_branch_rev = $1 . $3;
2203 # If we got a branch, record its number.
2204 if ( $real_branch_rev ) {
2205 $self->{branch_names}->{$real_branch_rev} = $tag_name;
2206 $self->{branch_numbers}->{$tag_name} = $real_branch_rev;
2208 # Else it's just a regular (non-branch) tag.
2209 push @{$self->{symbolic_names}->{$tag_rev}}, $tag_name;
2213 $self->{collecting_symbolic_names} = 1;
2217 # -------------------------------------
2220 my ($self, $line) = @_;
2222 my ($revision) = ( $line =~ /^revision (\d+\.[\d.]+)/ );
2227 $self->{rev_revision} = $revision;
2231 # -------------------------------------
2233 { # Closure over %gecos_warned
2235 sub read_date_author_and_state {
2236 my ($self, $line) = @_;
2238 my ($time, $author, $state) = $self->parse_date_author_and_state($line);
2240 if ( defined($self->{usermap}->{$author}) and $self->{usermap}->{$author} ) {
2241 $author = $self->{usermap}->{$author};
2242 } elsif ( defined $Domain or $Gecos == 1 ) {
2243 my $email = $author;
2244 $email = $author."@".$Domain
2245 if defined $Domain && $Domain ne '';
2247 my $pw = getpwnam($author);
2248 my ($fullname, $office, $workphone, $homephone, $gcos);
2249 if ( defined $pw ) {
2250 $gcos = (getpwnam($author))[6];
2251 ($fullname, $office, $workphone, $homephone) =
2252 split /\s*,\s*/, $gcos;
2254 warn "Couldn't find gecos info for author '$author'\n"
2255 unless $gecos_warned{$author}++;
2258 for (grep defined, $fullname, $office, $workphone, $homephone) {
2259 s/&/ucfirst(lc($pw->name))/ge;
2261 $author = $fullname . " <" . $email . ">"
2265 $self->{rev_state} = $state;
2266 $self->{rev_time} = $time;
2267 $self->{rev_author} = $author;
2272 # -------------------------------------
2275 # A "branches: ..." line here indicates that one or more branches
2276 # are rooted at this revision. If we're showing branches, then we
2277 # want to show that fact as well, so we collect all the branches
2278 # that this is the latest ancestor of and store them in
2279 # $self->[rev_branch_roots}. Just for reference, the format of the
2280 # line we're seeing at this point is:
2282 # branches: 1.5.2; 1.5.4; ...;
2285 my ($self, $line) = @_;
2287 # Ugh. This really bothers me. Suppose we see a log entry
2290 # ----------------------------
2292 # date: 1999/10/17 03:07:38; author: jrandom; state: Exp;
2294 # Intended first line of log message begins here.
2295 # ----------------------------
2297 # The question is, how we can tell the difference between that
2298 # log message and a *two*-line log message whose first line is
2300 # "branches: 1.1.2;"
2302 # See the problem? The output of "cvs log" is inherently
2305 # For now, we punt: we liberally assume that people don't
2306 # write log messages like that, and just toss a "branches:"
2307 # line if we see it but are not showing branches. I hope no
2308 # one ever loses real log data because of this.
2309 if ( $Show_Branches ) {
2310 $line =~ s/(1\.)+1;|(1\.)+1$//; # ignore the trivial branch 1.1.1
2311 $self->{rev_branch_roots} = [split /;\s+/, $line]
2316 # -------------------------------------
2318 sub parse_date_author_and_state {
2319 my ($self, $line) = @_;
2320 # Parses the date/time and author out of a line like:
2322 # date: 1999/02/19 23:29:05; author: apharris; state: Exp;
2324 # or, in CVS 1.12.9:
2326 # date: 2004-06-05 16:10:32 +0000; author: somebody; state: Exp;
2328 my ($year, $mon, $mday, $hours, $min, $secs, $utcOffset, $author, $state, $rest) =
2330 m!(\d+)[-/](\d+)[-/](\d+)\s+(\d+):(\d+):(\d+)(\s+[+-]\d{4})?;\s+
2331 author:\s+([^;]+);\s+state:\s+([^;]+);(.*)!x
2332 or die "Couldn't parse date ``$line''";
2333 die "Bad date or Y2K issues"
2334 unless $year > 1969 and $year < 2258;
2335 # Kinda arbitrary, but useful as a sanity check
2336 my $time = timegm($secs, $min, $hours, $mday, $mon-1, $year-1900);
2337 if ( defined $utcOffset ) {
2338 my ($plusminus, $hour, $minute) = ($utcOffset =~ m/([+-])(\d\d)(\d\d)/);
2339 my $offset = (($hour * 60) + $minute) * 60 * ($plusminus eq '+' ? -1 : 1);
2342 if ( $rest =~ m!\s+lines:\s+(.*)! ) {
2343 $self->{lines} = $1;
2346 return $time, $author, $state;
2349 # Subrs ----------------------------------------------------------------------
2354 my ($time, $tags) = @_;
2356 # If we're in 'delta' mode, update the latest observed times for the
2357 # beginning and ending tags, and when we get around to printing output, we
2358 # will simply restrict ourselves to that timeframe...
2362 $Delta_StartTime = $time
2363 if $time > $Delta_StartTime and grep { $_ eq $Delta_From } @$tags;
2365 $Delta_EndTime = $time
2366 if $time > $Delta_EndTime and grep { $_ eq $Delta_To } @$tags;
2374 my $out = qx"@$cmd 2>&1";
2376 my ($sig, $core, $exit) = ($? & 127, $? & 128, $? >> 8);
2377 return $out, $exit, $sig, $core;
2380 # -------------------------------------
2382 # If accumulating, grab the boundary date from pre-existing ChangeLog.
2383 sub maybe_grab_accumulation_date {
2384 if (! $Cumulative || $Update) {
2390 open (LOG, "$Log_File_Name")
2391 or die ("trouble opening $Log_File_Name for reading ($!)");
2396 if (/^(\d\d\d\d-\d\d-\d\d\s+(\w+\s+)?\d\d:\d\d)/)
2398 $boundary_date = "$1";
2405 # convert time from utc to local timezone if the ChangeLog has
2406 # dates/times in utc
2407 if ($UTC_Times && $boundary_date)
2409 # convert the utc time to a time value
2410 my ($year,$mon,$mday,$hour,$min) = $boundary_date =~
2411 m#(\d+)-(\d+)-(\d+)\s+(\d+):(\d+)#;
2412 my $time = timegm(0,$min,$hour,$mday,$mon-1,$year-1900);
2413 # print the timevalue in the local timezone
2415 ($ignore,$min,$hour,$mday,$mon,$year,$wday) = localtime($time);
2416 $boundary_date=sprintf ("%4u-%02u-%02u %02u:%02u",
2417 $year+1900,$mon+1,$mday,$hour,$min);
2420 return $boundary_date;
2423 # -------------------------------------
2425 # Fills up a ChangeLog structure in the current directory.
2426 sub derive_changelog {
2429 # See "The Plan" above for a full explanation.
2431 # Might be adding to an existing ChangeLog
2432 my $accumulation_date = maybe_grab_accumulation_date;
2433 if ($accumulation_date) {
2434 # Insert -d immediately after 'cvs log'
2435 my $Log_Date_Command = "-d>${accumulation_date}";
2437 my ($log_index) = grep $command->[$_] eq 'log', 0..$#$command;
2438 splice @$command, $log_index+1, 0, $Log_Date_Command;
2439 &debug ("(adding log msg starting from $accumulation_date)\n");
2442 # output_changelog(read_changelog($command));
2443 my $builder = CVS::Utils::ChangeLog::EntrySetBuilder->new;
2444 $builder->read_changelog($command);
2445 $builder->grand_poobah->output_changelog;
2448 # -------------------------------------
2450 sub min { $_[0] < $_[1] ? $_[0] : $_[1] }
2452 # -------------------------------------
2454 sub common_path_prefix {
2455 my ($path1, $path2) = @_;
2457 # For compatibility (with older versions of cvs2cl.pl), we think in UN*X
2458 # terms, and mould windoze filenames to match. Is this really appropriate?
2459 # If a file is checked in under UN*X, and cvs log run on windoze, which way
2460 # do the path separators slope? Can we use fileparse as per the local
2461 # conventions? If so, we should probably have a user option to specify an
2462 # OS to emulate to handle stdin-fed logs. If we did this, we could avoid
2463 # the nasty \-/ transmogrification below.
2465 my ($dir1, $dir2) = map +(fileparse($_))[1], $path1, $path2;
2467 # Transmogrify Windows filenames to look like Unix.
2468 # (It is far more likely that someone is running cvs2cl.pl under
2469 # Windows than that they would genuinely have backslashes in their
2474 my ($accum1, $accum2, $last_common_prefix) = ('') x 3;
2476 my @path1 = grep length($_), split qr!/!, $dir1;
2477 my @path2 = grep length($_), split qr!/!, $dir2;
2480 for (0..min($#path1,$#path2)) {
2481 if ( $path1[$_] eq $path2[$_]) {
2482 push @common_path, $path1[$_];
2488 return join '', map "$_/", @common_path;
2491 # -------------------------------------
2494 # Check this internally before setting the global variable.
2497 # If this gets set, we encountered unknown options and will exit at
2498 # the end of this subroutine.
2499 my $exit_with_admonishment = 0;
2501 # command to generate the log
2502 my @log_source_command = qw( cvs log );
2504 my (@Global_Opts, @Local_Opts);
2506 Getopt::Long::Configure(qw( bundling permute no_getopt_compat
2507 pass_through no_ignore_case ));
2508 GetOptions('help|usage|h' => \$Print_Usage,
2509 'debug' => \$Debug, # unadvertised option, heh
2510 'version' => \$Print_Version,
2512 'file|f=s' => \$output_file,
2513 'accum' => \$Cumulative,
2514 'update' => \$Update,
2515 'fsf' => \$FSF_Style,
2516 'rcs=s' => \$RCS_Root,
2517 'usermap|U=s' => \$User_Map_File,
2519 'domain=s' => \$Domain,
2520 'passwd=s' => \$User_Passwd_File,
2521 'window|W=i' => \$Max_Checkin_Duration,
2522 'chrono' => \$Chronological_Order,
2523 'ignore|I=s' => \@Ignore_Files,
2524 'case-insensitive|C' => \$Case_Insensitive,
2525 'regexp|R=s' => \$Regexp_Gate,
2526 'stdin' => \$Input_From_Stdin,
2527 'stdout' => \$Output_To_Stdout,
2528 'distributed|d' => sub { CVS::Utils::ChangeLog::FileEntry->distributed(1) },
2529 'prune|P' => \$Prune_Empty_Msgs,
2530 'no-wrap' => \$No_Wrap,
2531 'gmt|utc' => \$UTC_Times,
2532 'day-of-week|w' => \$Show_Day_Of_Week,
2533 'revisions|r' => \$Show_Revisions,
2534 'show-dead' => \$Show_Dead,
2535 'tags|t' => \$Show_Tags,
2536 'tagdates|T' => \$Show_Tag_Dates,
2537 'branches|b' => \$Show_Branches,
2538 'follow|F=s' => \@Follow_Branches,
2539 'follow-only=s' => \@Follow_Only,
2540 'xml-encoding=s' => \$XML_Encoding,
2541 'xml' => \$XML_Output,
2542 'noxmlns' => \$No_XML_Namespace,
2543 'no-xml-iso-date' => \$No_XML_ISO_Date,
2544 'no-ancestors' => \$No_Ancestors,
2545 'lines-modified' => \$Show_Lines_Modified,
2547 'no-indent' => sub {
2553 $After_Header = "\n\n"; # Summary implies --separate-header
2560 'no-hide-branch-additions' => sub {
2561 $Hide_Branch_Additions = 0;
2564 'no-common-dir' => sub {
2568 'ignore-tag=s' => sub {
2569 $ignore_tags{$_[1]} = 1;
2572 'show-tag=s' => sub {
2573 $show_tags{$_[1]} = 1;
2576 # Deliberately undocumented. This is not a public interface, and
2577 # may change/disappear at any time.
2578 'test-code=s' => \$TestCode,
2583 /^([A-Za-z][A-Za-z0-9_\-\]\[]*):([A-Za-z][A-Za-z0-9_\-\]\[]*)$/ ) {
2588 die "--delta FROM_TAG:TO_TAG is what you meant to say.\n";
2595 $No_Extra_Indent = 1;
2601 $ChangeLog_Header = &slurp_file ($narg);
2602 if (! defined ($ChangeLog_Header)) {
2603 $ChangeLog_Header = '';
2607 'global-opts|g=s' => sub {
2609 push @Global_Opts, $narg;
2610 splice @log_source_command, 1, 0, $narg;
2613 'log-opts|l=s' => sub {
2615 push @Local_Opts, $narg;
2616 push @log_source_command, $narg;
2619 'mailname=s' => sub {
2621 warn "--mailname is deprecated; please use --domain instead\n";
2625 'separate-header|S' => sub {
2626 $After_Header = "\n\n";
2627 $No_Extra_Indent = 1;
2630 'group-within-date' => sub {
2631 $GroupWithinDate = 1;
2635 'hide-filenames' => sub {
2636 $Hide_Filenames = 1;
2640 or die "options parsing failed\n";
2642 push @log_source_command, map "$_", @ARGV;
2644 ## Check for contradictions...
2646 if ($Output_To_Stdout && CVS::Utils::ChangeLog::FileEntry->distributed) {
2647 print STDERR "cannot pass both --stdout and --distributed\n";
2648 $exit_with_admonishment = 1;
2651 if ($Output_To_Stdout && $output_file) {
2652 print STDERR "cannot pass both --stdout and --file\n";
2653 $exit_with_admonishment = 1;
2656 if ($Input_From_Stdin && @Global_Opts) {
2657 print STDERR "cannot pass both --stdin and -g\n";
2658 $exit_with_admonishment = 1;
2661 if ($Input_From_Stdin && @Local_Opts) {
2662 print STDERR "cannot pass both --stdin and -l\n";
2663 $exit_with_admonishment = 1;
2666 if ($XML_Output && $Cumulative) {
2667 print STDERR "cannot pass both --xml and --accum\n";
2668 $exit_with_admonishment = 1;
2671 # Other consistency checks and option-driven logic
2673 # Bleargh. Compensate for a deficiency of custom wrapping.
2674 if ( ($After_Header ne " ") and $FSF_Style ) {
2675 $After_Header .= "\t";
2678 @Ignore_Files = map lc, @Ignore_Files
2679 if $Case_Insensitive;
2681 # Or if any other error message has already been printed out, we
2683 if ($exit_with_admonishment) {
2687 elsif ($Print_Usage) {
2691 elsif ($Print_Version) {
2696 ## Else no problems, so proceed.
2699 $Log_File_Name = $output_file;
2702 return \@log_source_command;
2705 # -------------------------------------
2708 my $filename = shift || die ("no filename passed to slurp_file()");
2711 open (SLURPEE, "<${filename}") or die ("unable to open $filename ($!)");
2713 $retstr = <SLURPEE>;
2718 # -------------------------------------
2727 # -------------------------------------
2730 print "cvs2cl.pl version ${VERSION}; distributed under the GNU GPL.\n";
2733 # -------------------------------------
2738 eval "use Pod::Usage qw( pod2usage )";
2743 * Pod::Usage was not found. The formatting may be suboptimal. Consider
2744 upgrading your Perl --- Pod::Usage is standard from 5.6 onwards, and
2745 versions of perl prior to 5.6 are getting rather rusty, now. Alternatively,
2746 install Pod::Usage direct from CPAN.
2750 my $message = <DATA>;
2751 $message =~ s/^=(head1|item) //gm;
2752 $message =~ s/^=(over|back).*\n//gm;
2753 $message =~ s/\n{3,}/\n\n/g;
2757 pod2usage( -exitval => 'NOEXIT',
2759 -output => \*STDOUT,
2766 # Main -----------------------------------------------------------------------
2768 my $log_source_command = parse_options;
2769 if ( defined $TestCode ) {
2771 die "Eval failed: '$@'\n"
2774 derive_changelog($log_source_command);
2781 cvs2cl.pl - convert cvs log messages to changelogs
2785 B<cvs2cl> [I<options>] [I<FILE1> [I<FILE2> ...]]
2789 cvs2cl produces a GNU-style ChangeLog for CVS-controlled sources by
2790 running "cvs log" and parsing the output. Duplicate log messages get
2791 unified in the Right Way.
2793 The default output of cvs2cl is designed to be compact, formally unambiguous,
2794 but still easy for humans to read. It should be largely self-explanatory; the
2795 one abbreviation that might not be obvious is "utags". That stands for
2796 "universal tags" -- a universal tag is one held by all the files in a given
2799 If you need output that's easy for a program to parse, use the B<--xml> option.
2800 Note that with XML output, just about all available information is included
2801 with each change entry, whether you asked for it or not, on the theory that
2802 your parser can ignore anything it's not looking for.
2804 If filenames are given as arguments cvs2cl only shows log information for the
2811 =item B<-h>, B<-help>, B<--help>, B<-?>
2813 Show a short help and exit.
2817 Show version and exit.
2819 =item B<-r>, B<--revisions>
2821 Show revision numbers in output.
2823 =item B<-b>, B<--branches>
2825 Show branch names in revisions when possible.
2827 =item B<-t>, B<--tags>
2829 Show tags (symbolic names) in output.
2831 =item B<-T>, B<--tagdates>
2833 Show tags in output on their first occurance.
2835 =item B<--show-dead>
2841 Read from stdin, don't run cvs log.
2845 Output to stdout not to ChangeLog.
2847 =item B<-d>, B<--distributed>
2849 Put ChangeLogs in subdirs.
2851 =item B<-f> I<FILE>, B<--file> I<FILE>
2853 Write to I<FILE> instead of ChangeLog.
2857 Use this if log data is in FSF ChangeLog style.
2861 Attempt strict FSF-standard compatible output.
2863 =item B<-W> I<SECS>, B<--window> I<SECS>
2865 Window of time within which log entries unify.
2867 =item -B<U> I<UFILE>, B<--usermap> I<UFILE>
2869 Expand usernames to email addresses from I<UFILE>.
2871 =item B<--passwd> I<PASSWORDFILE>
2873 Use system passwd file for user name expansion. If no mail domain is provided
2874 (via B<--domain>), it tries to read one from B</etc/mailname>, output of B<hostname
2875 -d>, B<dnsdomainname>, or B<domain-name>. cvs2cl exits with an error if none of
2876 those options is successful. Use a domain of '' to prevent the addition of a
2879 =item B<--domain> I<DOMAIN>
2881 Domain to build email addresses from.
2885 Get user information from GECOS data.
2887 =item B<-R> I<REGEXP>, B<--regexp> I<REGEXP>
2889 Include only entries that match I<REGEXP>. This option may be used multiple
2892 =item B<-I> I<REGEXP>, B<--ignore> I<REGEXP>
2894 Ignore files whose names match I<REGEXP>. This option may be used multiple
2895 times. The regexp is a perl regular expression. It is matched as is; you may
2896 want to prefix with a ^ or suffix with a $ to anchor the match.
2898 =item B<-C>, B<--case-insensitive>
2900 Any regexp matching is done case-insensitively.
2902 =item B<-F> I<BRANCH>, B<--follow> I<BRANCH>
2904 Show only revisions on or ancestral to I<BRANCH>.
2906 =item B<--follow-only> I<BRANCH>
2908 Like --follow, but sub-branches are not followed.
2910 =item B<--no-ancestors>
2912 When using B<-F>, only track changes since the I<BRANCH> started.
2914 =item B<--no-hide-branch-additions>
2916 By default, entries generated by cvs for a file added on a branch (a dead 1.1
2917 entry) are not shown. This flag reverses that action.
2919 =item B<-S>, B<--separate-header>
2921 Blank line between each header and log message.
2925 Add CVS change summary information.
2929 Don't auto-wrap log message (recommend B<-S> also).
2931 =item B<--no-indent>
2933 Don't indent log message
2935 =item B<--gmt>, B<--utc>
2937 Show times in GMT/UTC instead of local time.
2941 Add to an existing ChangeLog (incompatible with B<--xml>).
2943 =item B<-w>, B<--day-of-week>
2949 Don't show times in output.
2953 Output log in chronological order (default is reverse chronological order).
2955 =item B<--header> I<FILE>
2957 Get ChangeLog header from I<FILE> ("B<->" means stdin).
2961 Output XML instead of ChangeLog format.
2963 =item B<--xml-encoding> I<ENCODING.>
2965 Insert encoding clause in XML header.
2969 Don't include xmlns= attribute in root element.
2971 =item B<--hide-filenames>
2973 Don't show filenames (ignored for XML output).
2975 =item B<--no-common-dir>
2977 Don't shorten directory names from filenames.
2979 =item B<--rcs> I<CVSROOT>
2981 Handle filenames from raw RCS, for instance those produced by "cvs rlog"
2982 output, stripping the prefix I<CVSROOT>.
2984 =item B<-P>, B<--prune>
2986 Don't show empty log messages.
2988 =item B<--lines-modified>
2990 Output the number of lines added and the number of lines removed for
2991 each checkin (if applicable). At the moment, this only affects the
2994 =item B<--ignore-tag> I<TAG>
2996 Ignore individual changes that are associated with a given tag.
2997 May be repeated, if so, changes that are associated with any of
2998 the given tags are ignored.
3000 =item B<--show-tag> I<TAG>
3002 Log only individual changes that are associated with a given
3003 tag. May be repeated, if so, changes that are associated with
3004 any of the given tags are logged.
3006 =item B<--delta> I<FROM_TAG>B<:>I<TO_TAG>
3008 Attempt a delta between two tags (since I<FROM_TAG> up to and
3009 including I<TO_TAG>). The algorithm is a simple date-based one
3010 (this is a hard problem) so results are imperfect.
3012 =item B<-g> I<OPTS>, B<--global-opts> I<OPTS>
3014 Pass I<OPTS> to cvs like in "cvs I<OPTS> log ...".
3016 =item B<-l> I<OPTS>, B<--log-opts> I<OPTS>
3018 Pass I<OPTS> to cvs log like in "cvs ... log I<OPTS>".
3022 Notes about the options and arguments:
3028 The B<-I> and B<-F> options may appear multiple times.
3032 To follow trunk revisions, use "B<-F trunk>" ("B<-F TRUNK>" also works). This is
3033 okay because no would ever, ever be crazy enough to name a branch "trunk",
3038 For the B<-U> option, the I<UFILE> should be formatted like CVSROOT/users. That is,
3039 each line of I<UFILE> looks like this:
3041 jrandom:jrandom@red-bean.com
3043 or maybe even like this
3045 jrandom:'Jesse Q. Random <jrandom@red-bean.com>'
3047 Don't forget to quote the portion after the colon if necessary.
3051 Many people want to filter by date. To do so, invoke cvs2cl.pl like this:
3053 cvs2cl.pl -l "-d'DATESPEC'"
3055 where DATESPEC is any date specification valid for "cvs log -d". (Note that
3056 CVS 1.10.7 and below requires there be no space between -d and its argument).
3060 Dates/times are interpreted in the local time zone.
3064 Remember to quote the argument to `B<-l>' so that your shell doesn't interpret
3065 spaces as argument separators.
3069 See the 'Common Options' section of the cvs manual ('info cvs' on UNIX-like
3070 systems) for more information.
3074 Note that the rules for quoting under windows shells are different.
3078 To run in an automated environment such as CGI or PHP, suidperl may be needed
3079 in order to execute as the correct user to enable /cvsroot read lock files to
3080 be written for the 'cvs log' command. This is likely just a case of changing
3081 the /usr/bin/perl command to /usr/bin/suidperl, and explicitly declaring the
3088 Some examples (working on UNIX shells):
3090 # logs after 6th March, 2003 (inclusive)
3091 cvs2cl.pl -l "-d'>2003-03-06'"
3092 # logs after 4:34PM 6th March, 2003 (inclusive)
3093 cvs2cl.pl -l "-d'>2003-03-06 16:34'"
3094 # logs between 4:46PM 6th March, 2003 (exclusive) and
3095 # 4:34PM 6th March, 2003 (inclusive)
3096 cvs2cl.pl -l "-d'2003-03-06 16:46>2003-03-06 16:34'"
3098 Some examples (on non-UNIX shells):
3100 # Reported to work on windows xp/2000
3101 cvs2cl.pl -l "-d"">2003-10-18;today<"""
3109 =item Melissa O'Neill
3111 =item Martyn J. Pearce
3123 =item Richard Broberg
3127 =item Oswald Buddenhagen
3131 =item Arthur de Jong
3133 =item Mark W. Eichin
3139 =item Simon Josefsson
3141 =item Robin Hugh Johnson
3153 =item Richard Laager
3157 =item Karl-Heinz Marbaise
3159 =item Mitsuaki Masuhara
3161 =item Henrik Nordstrom
3165 =item Peter Palfrader
3167 =item Thomas Parmelan
3169 =item Jordan Russell
3171 =item Jacek Sliwerski
3173 =item Johannes Stezenbach
3183 Please report bugs to C<bug-cvs2cl@red-bean.com>.
3185 =head1 PREREQUISITES
3187 This script requires C<Text::Wrap>, C<Time::Local>, and C<File::Basename>. It
3188 also seems to require C<Perl 5.004_04> or higher.
3190 =head1 OPERATING SYSTEM COMPATIBILITY
3192 Should work on any OS.
3194 =head1 SCRIPT CATEGORIES
3200 (C) 2001,2002,2003,2004 Martyn J. Pearce <fluffy@cpan.org>, under the GNU GPL.
3202 (C) 1999 Karl Fogel <kfogel@red-bean.com>, under the GNU GPL.
3204 cvs2cl.pl is free software; you can redistribute it and/or modify
3205 it under the terms of the GNU General Public License as published by
3206 the Free Software Foundation; either version 2, or (at your option)
3209 cvs2cl.pl is distributed in the hope that it will be useful,
3210 but WITHOUT ANY WARRANTY; without even the implied warranty of
3211 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3212 GNU General Public License for more details.
3214 You may have received a copy of the GNU General Public License
3215 along with cvs2cl.pl; see the file COPYING. If not, write to the
3216 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
3217 Boston, MA 02111-1307, USA.