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: 2009/01/20 14:36:08 $
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 # Set iff output should be like an FSF-style ChangeLog.
231 # Show times in UTC instead of local time
234 # Show times in output?
237 # Show day of week in output?
238 my
$Show_Day_Of_Week = 0;
240 # Show revision numbers in output?
241 my
$Show_Revisions = 0;
243 # Show dead files in output?
246 # Hide dead trunk files which were created as a result of additions on a
248 my
$Hide_Branch_Additions = 1;
250 # Show tags (symbolic names) in output?
253 # Show tags separately in output?
254 my
$Show_Tag_Dates = 0;
256 # Show branches by symbolic name in output?
257 my
$Show_Branches = 0;
259 # Show only revisions on these branches or their ancestors.
261 # Show only revisions on these branches or their ancestors; ignore descendent
265 # Don't bother with files matching this regexp.
268 # How exactly we match entries. We definitely want "o",
269 # and user might add "i" by using --case-insensitive option.
270 my
$Case_Insensitive = 0;
272 # Maybe only show log messages matching a certain regular expression.
273 my
$Regexp_Gate = '';
275 # Show tags only matching certain regular expression.
278 # Pass this global option string along to cvs, to the left of `log':
279 my
$Global_Opts = '';
281 # Pass this option string along to the cvs log subcommand:
282 my
$Command_Opts = '';
284 # Read log output from stdin instead of invoking cvs log?
285 my
$Input_From_Stdin = 0;
287 # Don't show filenames in output.
288 my
$Hide_Filenames = 0;
290 # Don't shorten directory names from filenames.
293 # Max checkin duration. CVS checkin is not atomic, so we may have checkin
294 # times that span a range of time. We assume that checkins will last no
295 # longer than $Max_Checkin_Duration seconds, and that similarly, no
296 # checkins will happen from the same users with the same message less
297 # than $Max_Checkin_Duration seconds apart.
298 my
$Max_Checkin_Duration = 180;
300 # What to put at the front of [each] ChangeLog.
301 my
$ChangeLog_Header = '';
303 # Whether to enable 'delta' mode, and for what start/end tags.
310 # Whether to parse filenames from the RCS filename, and if so what
314 # Whether to output information on the # of lines added and removed
315 # by each file modification.
316 my
$Show_Lines_Modified = 0;
318 ## end vars set by options.
320 # latest observed times for the start/end tags in delta mode
321 my
$Delta_StartTime = 0;
322 my
$Delta_EndTime = 0;
324 my
$No_Ancestors = 0;
326 my
$No_Extra_Indent = 0;
329 my
$GroupByAuthor = 0;
331 # ----------------------------------------------------------------------------
333 package CVS
::Utils
::ChangeLog
::EntrySet
;
338 bless \
%self
, $class;
341 # -------------------------------------
343 sub output_changelog
{
344 my
$output_type = $XML_Output ?
'XML' : 'Text';
345 my
$output_class = "CVS::Utils::ChangeLog::EntrySet::Output::${output_type}";
346 my
$output = $output_class->new
(follow_branches
=> \@Follow_Branches
,
347 follow_only
=> \@Follow_Only
,
348 ignore_tags
=> \
%ignore_tags
,
349 show_tags
=> \
%show_tags
,
351 $output->output_changelog
(@_
);
354 # -------------------------------------
357 my
($self, $file_full_path, $time, $revision, $state, $lines,
358 $branch_names, $branch_roots, $branch_numbers,
359 $symbolic_names, $author, $msg_txt) = @_
;
362 CVS
::Utils
::ChangeLog
::FileEntry-
>new
($file_full_path, $time, $revision,
364 $branch_names, $branch_roots,
368 # We might be including revision numbers and/or tags and/or
369 # branch names in the output. Most of the code from here to
370 # loop-end deals with organizing these in qunk.
372 unless
( $Hide_Branch_Additions
374 $msg_txt =~
/file .
+ was initially added on branch \S
+.
/ ) {
375 # Add this file to the list
376 # (We use many spoonfuls of autovivication magic. Hashes and arrays
377 # will spring into existence if they aren't there already.)
379 &main
::debug
("(pushing log msg for ".
$qunk->dir_key .
$qunk->filename .
")\n");
381 # Store with the files in this commit. Later we'll loop through
382 # again, making sure that revisions with the same log message
383 # and nearby commit times are grouped together as one commit.
384 $self->{$qunk->dir_key
}{$author}{$time}{$msg_txt} =
385 CVS
::Utils
::ChangeLog
::Message-
>new
($msg_txt)
386 unless exists
$self->{$qunk->dir_key
}{$author}{$time}{$msg_txt};
387 $self->{$qunk->dir_key
}{$author}{$time}{$msg_txt}->add_fileentry
($qunk);
392 # ----------------------------------------------------------------------------
394 package CVS
::Utils
::ChangeLog
::EntrySet
::Output
::Text
;
396 use base qw
( CVS
::Utils
::ChangeLog
::EntrySet
::Output
);
398 use File
::Basename qw
( fileparse
);
402 my
$self = $class->SUPER
::new
(@_
);
405 # -------------------------------------
408 my
$self = shift; my
$class = ref
$self;
411 return $Show_Day_Of_Week ?
' ' .
$class->weekday_en
($wday) : '';
414 # -------------------------------------
418 my
($time, $author, $lastdate, $lastauthor) = @_
;
420 my
$header_line = '';
422 my
(undef
,$min,$hour,$mday,$mon,$year,$wday)
423 = $UTC_Times ? gmtime
($time) : localtime
($time);
425 my
$date = $self->fdatetime
($time);
428 $header_line = sprintf
"%s %s\n\n", $date, $author;
430 if ( $GroupByDate and
($date eq
$lastdate) and
431 ((!$GroupByAuthor) or
($author eq
$lastauthor)) ) {
434 if ( $GroupByDate and
! $GroupByAuthor ) {
435 $header_line = "$date\n\n";
437 $header_line = "$date $author\n\n";
443 # -------------------------------------
445 sub preprocess_msg_text
{
449 $text = $self->SUPER
::preprocess_msg_text
($text);
451 unless
( $No_Wrap ) {
452 # Strip off lone newlines, but only for lines that don't begin with
453 # whitespace or a mail-quoting character, since we want to preserve
454 # that kind of formatting. Also don't strip newlines that follow a
455 # period; we handle those specially next. And don't strip
456 # newlines that precede an open paren.
457 1 while $text =~ s
/(^|
\n)([^
>\s
].
*[^.
\n])\n([^
>\n])/$1$2 $3/g
;
459 # If a newline follows a period, make sure that when we bring up the
460 # bottom sentence, it begins with two spaces.
461 1 while $text =~ s
/(^|
\n)([^
>\s
].
*)\n([^
>\n])/$1$2 $3/g
;
467 # -------------------------------------
469 # Here we take a bunch of qunks and convert them into printed
470 # summary that will include all the information the user asked for.
471 sub pretty_file_list
{
477 my
$qunksref = shift;
480 my
$beauty = ''; # The accumulating header string for this entry.
481 my
%non_unanimous_tags
; # Tags found in a proper subset of qunks
482 my
%unanimous_tags
; # Tags found in all qunks
483 my
%all_branches
; # Branches found in any qunk
484 my
$fbegun = 0; # Did we begin printing filenames yet?
486 my
($common_dir, $qunkrefs) =
487 $self->_pretty_file_list
(\
(%unanimous_tags
, %non_unanimous_tags
, %all_branches
), $qunksref);
489 my @qunkrefs
= @
$qunkrefs;
491 # Not XML output, so complexly compactify for chordate consumption. At this
492 # point we have enough global information about all the qunks to organize
493 # them non-redundantly for output.
496 # Note that $common_dir still has its trailing slash
497 $beauty .
= "$common_dir: ";
502 # For trailing revision numbers.
505 foreach my
$branch (keys
(%all_branches
))
507 foreach my
$qunkref (@qunkrefs
)
509 if ((defined
($qunkref->branch
))
510 and
($qunkref->branch eq
$branch))
513 # kff todo: comma-delimited in XML too? Sure.
519 my
$fname = substr
($qunkref->filename
, length
($common_dir));
521 $qunkref->{'printed'} = 1; # Just setting a mark bit, basically
523 if ( $Show_Tags and defined
$qunkref->tags
) {
524 my @tags
= grep ($non_unanimous_tags{$_}, @
{$qunkref->tags
});
527 $beauty .
= " (tags: ";
528 $beauty .
= join (', ', @tags
);
533 if ($Show_Revisions) {
534 # Collect the revision numbers' last components, but don't
535 # print them -- they'll get printed with the branch name
537 $qunkref->revision
=~
/.
+\.
([\d
]+)$
/;
538 push
(@brevisions
, $1);
540 # todo: we're still collecting branch roots, but we're not
541 # showing them anywhere. If we do show them, it would be
542 # nifty to just call them revision "0" on a the branch.
543 # Yeah, that's the ticket.
547 $beauty .
= " ($branch";
549 if ((scalar
(@brevisions
)) > 1) {
551 $beauty .
= (join (',', @brevisions
));
555 # Square brackets are spurious here, since there's no range to
557 $beauty .
= ".$brevisions[0]";
564 # Okay; any qunks that were done according to branch are taken care
565 # of, and marked as printed. Now print everyone else.
567 my
%fileinfo_printed
;
568 foreach my
$qunkref (@qunkrefs
)
570 next
if (defined
($qunkref->{'printed'})); # skip if already printed
572 my
$b = substr
($qunkref->filename
, length
($common_dir));
573 # todo: Shlomo's change was this:
574 # $beauty .= substr ($qunkref->filename,
575 # (($common_dir eq "./") ? '' : length ($common_dir)));
576 $qunkref->{'printed'} = 1; # Set a mark bit.
578 if ($Show_Revisions ||
$Show_Tags ||
$Show_Dead)
580 my
$started_addendum = 0;
582 if ($Show_Revisions) {
583 $started_addendum = 1;
585 $b .
= $qunkref->revision
;
587 if ($Show_Dead && $qunkref->state
=~
/dead
/)
589 # Deliberately not using $started_addendum. Keeping it simple.
592 if ($Show_Tags && (defined
$qunkref->tags
)) {
593 my @tags
= grep ($non_unanimous_tags{$_}, @
{$qunkref->tags
});
594 if ((scalar
(@tags
)) > 0) {
595 if ($started_addendum) {
601 $b .
= join (', ', @tags
);
602 $started_addendum = 1;
605 if ($started_addendum) {
610 unless
( exists
$fileinfo_printed{$b} ) {
616 $beauty .
= $b, $fileinfo_printed{$b} = 1;
620 # Unanimous tags always come last.
621 if ($Show_Tags && %unanimous_tags
)
623 $beauty .
= " (utags: ";
624 $beauty .
= join (', ', sort keys
(%unanimous_tags
));
628 # todo: still have to take care of branch_roots?
630 $beauty = "$beauty:";
635 # -------------------------------------
639 my
($fh, $time, $tag) = @_
;
641 my
$fdatetime = $self->fdatetime
($time);
642 print
$fh "$fdatetime tag $tag\n\n";
646 # -------------------------------------
650 my
($msg, $files, $qunklist) = @_
;
654 if ( $No_Wrap and
! $Summary ) {
655 $msg = $self->preprocess_msg_text
($msg);
656 $files = $self->mywrap
("\t", "\t ", "* $files");
657 $msg =~ s
/\n(.
+)/\n$Indent$1/g
;
658 unless
($After_Header eq
" ") {
659 $msg =~ s
/^
(.
+)/$Indent$1/g
;
661 if ( $Hide_Filenames ) {
662 $body = $After_Header .
$msg;
664 $body = $files .
$After_Header .
$msg;
666 } elsif
( $Summary ) {
667 my
($filelist, $qunk);
668 my
(@DeletedQunks
, @AddedQunks
, @ChangedQunks
);
670 $msg = $self->preprocess_msg_text
($msg);
672 # Sort the files (qunks) according to the operation that was
673 # performed. Files which were added have no line change
674 # indicator, whereas deleted files have state dead.
676 foreach
$qunk ( @
$qunklist ) {
677 if ( "dead" eq
$qunk->state
) {
678 push @DeletedQunks
, $qunk;
679 } elsif
( ! defined
$qunk->lines
) {
680 push @AddedQunks
, $qunk;
682 push @ChangedQunks
, $qunk;
686 # The qunks list was originally in tree search order. Let's
687 # get that back. The lists, if they exist, will be reversed upon
692 # Now write the three sections onto $filelist
694 if ( @DeletedQunks
) {
695 $filelist .
= "\tDeleted:\n";
696 foreach
$qunk ( @DeletedQunks
) {
697 $filelist .
= "\t\t" .
$qunk->filename
;
698 $filelist .
= " (" .
$qunk->revision .
")";
705 $filelist .
= "\tAdded:\n";
706 foreach
$qunk (@AddedQunks
) {
707 $filelist .
= "\t\t" .
$qunk->filename
;
708 $filelist .
= " (" .
$qunk->revision .
")";
714 if ( @ChangedQunks
) {
715 $filelist .
= "\tChanged:\n";
716 foreach
$qunk (@ChangedQunks
) {
717 $filelist .
= "\t\t" .
$qunk->filename
;
718 $filelist .
= " (" .
$qunk->revision .
")";
719 $filelist .
= ", \"" .
$qunk->state .
"\"";
720 $filelist .
= ", lines: " .
$qunk->lines
;
728 if ( $Hide_Filenames ) {
732 $msg =~ s
/\n(.
*)/\n$Indent$1/g
;
733 unless
( $After_Header eq
" " or
$FSF_Style ) {
734 $msg =~ s
/^
(.
*)/$Indent$1/g
;
737 unless
( $No_Wrap ) {
739 $msg = $self->wrap_log_entry
($msg, '', 69, 69);
743 $msg = $self->mywrap
('', $Indent, "$msg");
744 $msg =~ s
/[ \t]+\n/\n/g
;
748 $body = $filelist .
$After_Header .
$msg;
749 } else { # do wrapping, either FSF-style or regular
750 my
$latter_wrap = $No_Extra_Indent ?
$Indent : "$Indent ";
753 $files = $self->mywrap
($Indent, $latter_wrap, "* $files");
755 my
$files_last_line_len = 0;
756 if ( $After_Header eq
" " ) {
757 $files_last_line_len = $self->last_line_len
($files);
758 $files_last_line_len += 1; # for $After_Header
761 $msg = $self->wrap_log_entry
($msg, $latter_wrap, 69-$files_last_line_len, 69);
762 $body = $files .
$After_Header .
$msg;
763 } else { # not FSF-style
764 $msg = $self->preprocess_msg_text
($msg);
765 $body = $files .
$After_Header .
$msg;
766 $body = $self->mywrap
($Indent, $latter_wrap, "* $body");
767 $body =~ s
/[ \t]+\n/\n/g
;
774 # ----------------------------------------------------------------------------
776 package CVS
::Utils
::ChangeLog
::EntrySet
::Output
::XML
;
778 use base qw
( CVS
::Utils
::ChangeLog
::EntrySet
::Output
);
780 use File
::Basename qw
( fileparse
);
784 my
$self = $class->SUPER
::new
(@_
);
787 # -------------------------------------
791 my
($time, $author, $lastdate) = @_
;
793 my
$header_line = '';
797 my
($y, $m, $d, $H, $M, $S) = (gmtime
($time))[5,4,3,2,1,0];
799 # Ideally, this would honor $UTC_Times and use +HH:MM syntax
800 $isoDate = sprintf
("%04d-%02d-%02dT%02d:%02d:%02dZ",
801 $y + 1900, $m + 1, $d, $H, $M, $S);
803 my
(undef
,$min,$hour,$mday,$mon,$year,$wday)
804 = $UTC_Times ? gmtime
($time) : localtime
($time);
806 my
$date = $self->fdatetime
($time);
807 $wday = $self->wday
($wday);
810 sprintf
("<date>%4u-%02u-%02u</date>\n${wday}<time>%02u:%02u</time>\n",
811 $year+1900, $mon+1, $mday, $hour, $min);
812 $header_line .
= "<isoDate>$isoDate</isoDate>\n"
813 unless
$No_XML_ISO_Date;
814 $header_line .
= sprintf
("<author>%s</author>\n" , $author);
817 # -------------------------------------
820 my
$self = shift; my
$class = ref
$self;
823 return '<weekday>' .
$class->weekday_en
($wday) .
"</weekday>\n";
826 # -------------------------------------
838 # -------------------------------------
845 length
$XML_Encoding ? qq
'encoding="$XML_Encoding"' : '';
846 my
$version = 'version="1.0"';
848 sprintf
'<?xml %s?>', join ' ', grep length
, $version, $encoding;
852 '<changelog xmlns="http://www.red-bean.com/xmlns/cvs2cl/">';
853 print
$fh "$declaration\n\n$root\n\n";
856 # -------------------------------------
862 print
$fh "</changelog>\n";
865 # -------------------------------------
867 sub preprocess_msg_text
{
871 $text = $self->SUPER
::preprocess_msg_text
($text);
873 $text = $self->escape
($text);
875 $text = "<msg>${text}</msg>\n";
880 # -------------------------------------
882 # Here we take a bunch of qunks and convert them into a printed
883 # summary that will include all the information the user asked for.
884 sub pretty_file_list
{
888 my
$beauty = ''; # The accumulating header string for this entry.
889 my
%non_unanimous_tags
; # Tags found in a proper subset of qunks
890 my
%unanimous_tags
; # Tags found in all qunks
891 my
%all_branches
; # Branches found in any qunk
892 my
$fbegun = 0; # Did we begin printing filenames yet?
894 my
($common_dir, $qunkrefs) =
895 $self->_pretty_file_list
(\
(%unanimous_tags
, %non_unanimous_tags
, %all_branches
),
898 my @qunkrefs
= @
$qunkrefs;
900 # If outputting XML, then our task is pretty simple, because we
901 # don't have to detect common dir, common tags, branch prefixing,
902 # etc. We just output exactly what we have, and don't worry about
903 # redundancy or readability.
905 foreach my
$qunkref (@qunkrefs
)
907 my
$filename = $qunkref->filename
;
908 my
$state = $qunkref->state
;
909 my
$revision = $qunkref->revision
;
910 my
$tags = $qunkref->tags
;
911 my
$branch = $qunkref->branch
;
912 my
$branchroots = $qunkref->roots
;
913 my
$lines = $qunkref->lines
;
915 $filename = $self->escape
($filename); # probably paranoia
916 $revision = $self->escape
($revision); # definitely paranoia
918 $beauty .
= "<file>\n";
919 $beauty .
= "<name>${filename}</name>\n";
920 $beauty .
= "<cvsstate>${state}</cvsstate>\n";
921 $beauty .
= "<revision>${revision}</revision>\n";
923 if ($Show_Lines_Modified
924 && $lines && $lines =~ m
/\
+(\d
+)\s
+-(\d
+)/) {
925 $beauty .
= "<linesadded>$1</linesadded>\n";
926 $beauty .
= "<linesremoved>$2</linesremoved>\n";
930 $branch = $self->escape
($branch); # more paranoia
931 $beauty .
= "<branch>${branch}</branch>\n";
933 foreach my
$tag (@
$tags) {
934 $tag = $self->escape
($tag); # by now you're used to the paranoia
935 $beauty .
= "<tag>${tag}</tag>\n";
937 foreach my
$root (@
$branchroots) {
938 $root = $self->escape
($root); # which is good, because it will continue
939 $beauty .
= "<branchroot>${root}</branchroot>\n";
941 $beauty .
= "</file>\n";
944 # Theoretically, we could go home now. But as long as we're here,
945 # let's print out the common_dir and utags, as a convenience to
946 # the receiver (after all, earlier code calculated that stuff
947 # anyway, so we might as well take advantage of it).
949 if ((scalar
(keys
(%unanimous_tags
))) > 1) {
950 foreach my
$utag ((keys
(%unanimous_tags
))) {
951 $utag = $self->escape
($utag); # the usual paranoia
952 $beauty .
= "<utag>${utag}</utag>\n";
956 $common_dir = $self->escape
($common_dir);
957 $beauty .
= "<commondir>${common_dir}</commondir>\n";
960 # That's enough for XML, time to go home:
964 # -------------------------------------
968 my
($fh, $time, $tag) = @_
;
970 my
($y, $m, $d, $H, $M, $S) = (gmtime
($time))[5,4,3,2,1,0];
972 # Ideally, this would honor $UTC_Times and use +HH:MM syntax
973 my
$isoDate = sprintf
("%04d-%02d-%02dT%02d:%02d:%02dZ",
974 $y + 1900, $m + 1, $d, $H, $M, $S);
976 print
$fh "<tagdate>\n";
977 print
$fh "<tagisodate>$isoDate</tagisodate>\n";
978 print
$fh "<tagdatetag>$tag</tagdatetag>\n";
979 print
$fh "</tagdate>\n\n";
983 # -------------------------------------
987 my
($fh, $entry) = @_
;
988 print
$fh "<entry>\n$entry</entry>\n\n";
991 # -------------------------------------
995 my
($msg, $files, $qunklist) = @_
;
997 $msg = $self->preprocess_msg_text
($msg);
998 return $files .
$msg;
1001 # ----------------------------------------------------------------------------
1003 package CVS
::Utils
::ChangeLog
::EntrySet
::Output
;
1005 use Carp qw
( croak
);
1006 use File
::Basename qw
( fileparse
);
1008 # Class Utility Functions -------------
1012 my @weekdays
= (qw
(Sunday Monday Tuesday Wednesday Thursday Friday Saturday
));
1015 return $weekdays[$_[0]];
1020 # -------------------------------------
1023 my
($proto, %args
) = @_
;
1024 my
$class = ref
$proto ||
$proto;
1026 my
$follow_branches = delete
$args{follow_branches
};
1027 my
$follow_only = delete
$args{follow_only
};
1028 my
$ignore_tags = delete
$args{ignore_tags
};
1029 my
$show_tags = delete
$args{show_tags
};
1030 die
"Unrecognized arg to EntrySet::Output::new: '$_'\n"
1033 bless
+{follow_branches
=> $follow_branches,
1034 follow_only
=> $follow_only,
1035 show_tags
=> $show_tags,
1036 ignore_tags
=> $ignore_tags,
1040 # Abstract Subrs ----------------------
1042 sub wday
{ croak
"Whoops. Abtract method call (wday).\n" }
1043 sub pretty_file_list
{ croak
"Whoops. Abtract method call (pretty_file_list).\n" }
1044 sub output_tagdate
{ croak
"Whoops. Abtract method call (output_tagdate).\n" }
1045 sub header_line
{ croak
"Whoops. Abtract method call (header_line).\n" }
1047 # Instance Subrs ----------------------
1049 sub output_header
{ }
1051 # -------------------------------------
1055 my
($fh, $entry) = @_
;
1056 print
$fh "$entry\n";
1059 # -------------------------------------
1061 sub output_footer
{ }
1063 # -------------------------------------
1065 sub escape
{ return $_[1] }
1067 # -------------------------------------
1069 sub _revision_is_wanted
{
1070 my
($self, $qunk) = @_
;
1072 my
($revision, $branch_numbers) = @
{$qunk}{qw
( revision branch_numbers
)};
1073 my
$follow_branches = $self->{follow_branches
};
1074 my
$follow_only = $self->{follow_only
};
1076 for my
$ignore_tag (keys
%{$self->{ignore_tags
}}) {
1078 if defined
$qunk->{tags
} and
grep $_ eq
$ignore_tag, @
{$qunk->{tags
}};
1081 if ( keys
%{$self->{show_tags
}} ) {
1082 for my
$show_tag (keys
%{$self->{show_tags
}}) {
1084 if ! defined
$qunk->{tags
} or
! grep $_ eq
$show_tag, @
{$qunk->{tags
}};
1089 unless @
$follow_branches + @
$follow_only; # no follow is follow all
1091 for my
$x (map
([$_, 1], @
$follow_branches),
1092 map
([$_, 0], @
$follow_only )) {
1093 my
($branch, $followsub) = @
$x;
1095 # Special case for following trunk revisions
1097 if $branch =~
/^trunk$
/i and
$revision =~
/^
[0-9]+\.
[0-9]+$
/;
1099 if ( my
$branch_number = $branch_numbers->{$branch} ) {
1100 # Are we on one of the follow branches or an ancestor of same?
1102 # If this revision is a prefix of the branch number, or possibly is less
1103 # in the minormost number, OR if this branch number is a prefix of the
1104 # revision, then yes. Otherwise, no.
1106 # So below, we determine if any of those conditions are met.
1108 # Trivial case: is this revision on the branch? (Compare this way to
1109 # avoid regexps that screw up Emacs indentation, argh.)
1110 if ( substr
($revision, 0, (length
($branch_number) + 1))
1112 ($branch_number .
".") ) {
1115 # } elsif ( length($revision) == length($branch_number)+2 ) {
1116 } elsif
( substr
($revision, length
($branch_number)+1) =~
/^\d
+$
/ ) {
1119 } elsif
( length
($branch_number) > length
($revision)
1122 # Non-trivial case: check if rev is ancestral to branch
1124 # r_left still has the trailing "."
1125 my
($r_left, $r_end) = ($revision =~
/^
((?
:\d
+\.
)+)(\d
+)$
/);
1127 # b_left still has trailing "."
1128 # b_mid has no trailing "."
1129 my
($b_left, $b_mid) = ($branch_number =~
/^
((?
:\d
+\.
)+)(\d
+)\.\d
+$
/);
1131 if $r_left eq
$b_left and
$r_end <= $b_mid;
1139 # -------------------------------------
1141 sub output_changelog
{
1142 my
$self = shift; my
$class = ref
$self;
1143 my
($grand_poobah) = @_
;
1144 ### Process each ChangeLog
1146 while (my
($dir,$authorhash) = each
%$grand_poobah)
1148 &main
::debug
("DOING DIR: $dir\n");
1150 # Here we twist our hash around, from being
1151 # author => time => message => filelist
1152 # in %$authorhash to
1153 # time => author => message => filelist
1156 # This is also where we merge entries. The algorithm proceeds
1157 # through the timeline of the changelog with a sliding window of
1158 # $Max_Checkin_Duration seconds; within that window, entries that
1159 # have the same log message are merged.
1161 # (To save space, we zap %$authorhash after we've copied
1162 # everything out of it.)
1165 while (my
($author,$timehash) = each
%$authorhash)
1168 foreach my
$time (sort {$a <=> $b} (keys
%$timehash))
1170 my
$msghash = $timehash->{$time};
1171 while (my
($msg,$qunklist) = each
%$msghash)
1173 my
$stamptime = $stamptime{$msg};
1174 if ((defined
$stamptime)
1175 and
(($time - $stamptime) < $Max_Checkin_Duration)
1176 and
(defined
$changelog{$stamptime}{$author}{$msg}))
1178 push
(@
{$changelog{$stamptime}{$author}{$msg}}, $qunklist->files
);
1181 $changelog{$time}{$author}{$msg} = $qunklist->files
;
1182 $stamptime{$msg} = $time;
1187 undef
(%$authorhash);
1189 ### Now we can write out the ChangeLog!
1191 my
($logfile_here, $logfile_bak, $tmpfile);
1193 my
$lastauthor = "";
1195 if (! $Output_To_Stdout) {
1196 $logfile_here = $dir .
$Log_File_Name;
1197 if (!$^O
=~
/Win32
/i
) {
1198 $logfile_here =~ s
/^\.\
/\
//\
//; # fix any leading ".//" problem
1201 $logfile_here =~ s
/^\.\
/+//; # remove any leading "./"
1203 $tmpfile = "${logfile_here}.cvs2cl$$.tmp";
1204 $logfile_bak = "${logfile_here}.bak";
1206 open
(LOG_OUT
, ">$tmpfile") or die
"Unable to open \"$tmpfile\"";
1209 open
(LOG_OUT
, ">-") or die
"Unable to open stdout for writing";
1212 print LOG_OUT
$ChangeLog_Header;
1214 my
%tag_date_printed
;
1216 $self->output_header
(\
*LOG_OUT
);
1219 if($Chronological_Order) {
1220 @key_list
= sort {$a <=> $b} (keys
%changelog
);
1222 @key_list
= sort {$b <=> $a} (keys
%changelog
);
1225 if ( $Show_Tag_Dates ||
$XML_Output ) {
1226 foreach my
$time (@key_list
) {
1227 my
$authorhash = $changelog{$time};
1228 while (my
($author,$mesghash) = each
%$authorhash) {
1229 while (my
($msg,$qunk) = each
%$mesghash) {
1230 my
$qunklist = $mesghash->{$msg};
1231 for my
$qunkref2 (@
$qunklist) {
1232 if (!$self->_revision_is_wanted
($qunkref2)) {
1233 if (defined
($qunkref2->tags
)) {
1234 for my
$tag (@
{$qunkref2->tags
}) {
1235 $tag_date_printed{$tag} = 1;
1245 foreach my
$time (@key_list
)
1247 next
if ($Delta_Mode &&
1248 (($time <= $Delta_StartTime) ||
1249 ($time > $Delta_EndTime && $Delta_EndTime)));
1251 # Set up the date/author line.
1252 # kff todo: do some more XML munging here, on the header
1253 # part of the entry:
1254 my
(undef
,$min,$hour,$mday,$mon,$year,$wday)
1255 = $UTC_Times ? gmtime
($time) : localtime
($time);
1257 $wday = $self->wday
($wday);
1258 # XML output includes everything else, we might as well make
1259 # it always include Day Of Week too, for consistency.
1260 my
$authorhash = $changelog{$time};
1261 if ( $Show_Tag_Dates ||
$XML_Output ) {
1263 while (my
($author,$mesghash) = each
%$authorhash) {
1264 while (my
($msg,$qunk) = each
%$mesghash) {
1265 for my
$qunkref2 (@
$qunk) {
1266 if (defined
($qunkref2->tags
)) {
1267 for my
$tag (@
{$qunkref2->tags
}) {
1274 # Sort here for determinism to ease testing
1275 foreach my
$tag (sort keys
%tags
) {
1276 if ( ! defined
$tag_date_printed{$tag} ) {
1277 $tag_date_printed{$tag} = $time;
1278 if ( (! defined
$Regexp_Tag) or
( $tag =~
/$Regexp_Tag/ ) ) {
1279 $self->output_tagdate
(\
*LOG_OUT
, $time, $tag);
1285 while (my
($author,$mesghash) = each
%$authorhash)
1287 # If XML, escape in outer loop to avoid compound quoting:
1288 $author = $self->escape
($author);
1291 # We sort here to enable predictable ordering for the testing porpoises
1292 for my
$msg (sort keys
%$mesghash)
1294 my
$qunklist = $mesghash->{$msg};
1297 grep $self->_revision_is_wanted
($_), @
$qunklist;
1299 next FOOBIE unless @qunklist
;
1301 my
$files = $self->pretty_file_list
(\@qunklist
);
1302 my
$header_line; # date and author
1303 my
$wholething; # $header_line + $body
1305 my
$date = $self->fdatetime
($time);
1306 $header_line = $self->header_line
($time, $author, $lastdate, $lastauthor);
1308 $lastauthor = $author;
1310 $Text::Wrap
::huge
= 'overflow'
1311 if $Text::Wrap
::VERSION
>= 2001.0130;
1312 # Reshape the body according to user preferences.
1313 my
$body = $self->format_body
($msg, $files, \@qunklist
);
1315 $body =~ s
/[ \t]+\n/\n/g
;
1316 $wholething = $header_line .
$body;
1318 # One last check: make sure it passes the regexp test, if the
1319 # user asked for that. We have to do it here, so that the
1320 # test can match against information in the header as well
1321 # as in the text of the log message.
1323 # How annoying to duplicate so much code just because I
1324 # can't figure out a way to evaluate scalars on the trailing
1325 # operator portion of a regular expression. Grrr.
1326 if ($Case_Insensitive) {
1327 unless
( $Regexp_Gate and
( $wholething !~
/$Regexp_Gate/oi
) ) {
1328 $self->output_entry
(\
*LOG_OUT
, $wholething);
1332 unless
( $Regexp_Gate and
( $wholething !~
/$Regexp_Gate/o
) ) {
1333 $self->output_entry
(\
*LOG_OUT
, $wholething);
1340 $self->output_footer
(\
*LOG_OUT
);
1344 if ( ! $Output_To_Stdout ) {
1345 # If accumulating, append old data to new before renaming. But
1346 # don't append the most recent entry, since it's already in the
1347 # new log due to CVS's idiosyncratic interpretation of "log -d".
1348 if ($Cumulative && -f $logfile_here) {
1349 open NEW_LOG
, ">>$tmpfile"
1350 or die
"trouble appending to $tmpfile ($!)";
1352 open OLD_LOG
, "<$logfile_here"
1353 or die
"trouble reading from $logfile_here ($!)";
1355 my
$started_first_entry = 0;
1356 my
$passed_first_entry = 0;
1358 if ( ! $passed_first_entry ) {
1359 if ( ( ! $started_first_entry )
1360 and
/^
(\d\d\d\d-\d\d-\d\d\s
+(\w
+\s
+)?\d\d
:\d\d
)/ ) {
1361 $started_first_entry = 1;
1362 } elsif
( /^
(\d\d\d\d-\d\d-\d\d\s
+(\w
+\s
+)?\d\d
:\d\d
)/ ) {
1363 $passed_first_entry = 1;
1375 if ( -f $logfile_here ) {
1376 rename
$logfile_here, $logfile_bak;
1378 rename
$tmpfile, $logfile_here;
1383 # -------------------------------------
1385 # Don't call this wrap, because with 5.5.3, that clashes with the
1386 # (unconditional :-( ) export of wrap() from Text::Wrap
1389 my
($indent1, $indent2, @text
) = @_
;
1390 # If incoming text looks preformatted, don't get clever
1391 my
$text = Text
::Wrap
::wrap
($indent1, $indent2, @text
);
1392 if ( grep /^\s
+/m
, @text
) {
1395 my @lines
= split /\n/, $text;
1396 $indent2 =~ s
!^
((?
: {8})+)!"\t" x
(length
($1)/8)!e
;
1397 $lines[0] =~ s
/^
$indent1\s
+/$indent1/;
1398 s
/^
$indent2\s
+/$indent2/
1399 for @lines
[1..
$#lines];
1400 my
$newtext = join "\n", @lines
;
1402 if substr
($text, -1) eq
"\n";
1406 # -------------------------------------
1408 sub preprocess_msg_text
{
1412 # Strip out carriage returns (as they probably result from DOSsy editors).
1413 $text =~ s
/\r\n/\n/g
;
1414 # If it *looks* like two newlines, make it *be* two newlines:
1415 $text =~ s
/\n\s
*\n/\n\n/g
;
1420 # -------------------------------------
1425 my
$files_list = shift;
1426 my @lines
= split (/\n/, $files_list);
1427 my
$last_line = pop
(@lines
);
1428 return length
($last_line);
1431 # -------------------------------------
1433 # A custom wrap function, sensitive to some common constructs used in
1435 sub wrap_log_entry
{
1438 my
$text = shift; # The text to wrap.
1439 my
$left_pad_str = shift; # String to pad with on the left.
1441 # These do NOT take left_pad_str into account:
1442 my
$length_remaining = shift; # Amount left on current line.
1443 my
$max_line_length = shift; # Amount left for a blank line.
1445 my
$wrapped_text = ''; # The accumulating wrapped entry.
1446 my
$user_indent = ''; # Inherited user_indent from prev line.
1448 my
$first_time = 1; # First iteration of the loop?
1449 my
$suppress_line_start_match = 0; # Set to disable line start checks.
1451 my @lines
= split (/\n/, $text);
1452 while (@lines
) # Don't use `foreach' here, it won't work.
1454 my
$this_line = shift (@lines
);
1457 if ($this_line =~
/^
(\s
+)/) {
1464 # If it matches any of the line-start regexps, print a newline now...
1465 if ($suppress_line_start_match)
1467 $suppress_line_start_match = 0;
1469 elsif
(($this_line =~
/^
(\s
*)\
*\s
+[a-zA-Z0-9
]/)
1470 ||
($this_line =~
/^
(\s
*)\
* [a-zA-Z0-9_\.\
/\
+-]+/)
1471 ||
($this_line =~
/^
(\s
*)\
([a-zA-Z0-9_\.\
/\
+-]+(\
)|
,\s
*)/)
1472 ||
($this_line =~
/^
(\s
+)(\S
+)/)
1473 ||
($this_line =~
/^
(\s
*)- +/)
1474 ||
($this_line =~
/^
()\s
*$
/)
1475 ||
($this_line =~
/^
(\s
*)\
*\
) +/)
1476 ||
($this_line =~
/^
(\s
*)[a-zA-Z0-9
](\
)|\.|\
:) +/))
1478 # Make a line break immediately, unless header separator is set
1479 # and this line is the first line in the entry, in which case
1480 # we're getting the blank line for free already and shouldn't
1482 unless
(($After_Header ne
" ") and
($first_time))
1484 if ($this_line =~
/^
()\s
*$
/) {
1485 $suppress_line_start_match = 1;
1486 $wrapped_text .
= "\n${left_pad_str}";
1489 $wrapped_text .
= "\n${left_pad_str}";
1492 $length_remaining = $max_line_length - (length
($user_indent));
1495 # Now that any user_indent has been preserved, strip off leading
1496 # whitespace, so up-folding has no ugly side-effects.
1497 $this_line =~ s
/^\s
*//;
1499 # Accumulate the line, and adjust parameters for next line.
1500 my
$this_len = length
($this_line);
1503 # Blank lines should cancel any user_indent level.
1505 $length_remaining = $max_line_length;
1507 elsif
($this_len >= $length_remaining) # Line too long, try breaking it.
1509 # Walk backwards from the end. At first acceptable spot, break
1511 my
$idx = $length_remaining - 1;
1512 if ($idx < 0) { $idx = 0 };
1515 if (substr
($this_line, $idx, 1) =~
/\s
/)
1517 my
$line_now = substr
($this_line, 0, $idx);
1518 my
$next_line = substr
($this_line, $idx);
1519 $this_line = $line_now;
1521 # Clean whitespace off the end.
1524 # The current line is ready to be printed.
1525 $this_line .
= "\n${left_pad_str}";
1527 # Make sure the next line is allowed full room.
1528 $length_remaining = $max_line_length - (length
($user_indent));
1530 # Strip next_line, but then preserve any user_indent.
1531 $next_line =~ s
/^\s
*//;
1533 # Sneak a peek at the user_indent of the upcoming line, so
1534 # $next_line (which will now precede it) can inherit that
1535 # indent level. Otherwise, use whatever user_indent level
1536 # we currently have, which might be none.
1537 my
$next_next_line = shift (@lines
);
1538 if ((defined
($next_next_line)) && ($next_next_line =~
/^
(\s
+)/)) {
1539 $next_line = $1 .
$next_line if (defined
($1));
1540 # $length_remaining = $max_line_length - (length ($1));
1541 $next_next_line =~ s
/^\s
*//;
1544 $next_line = $user_indent .
$next_line;
1546 if (defined
($next_next_line)) {
1547 unshift
(@lines
, $next_next_line);
1549 unshift
(@lines
, $next_line);
1551 # Our new next line might, coincidentally, begin with one of
1552 # the line-start regexps, so we temporarily turn off
1553 # sensitivity to that until we're past the line.
1554 $suppress_line_start_match = 1;
1566 # We bottomed out because the line is longer than the
1567 # available space. But that could be because the space is
1568 # small, or because the line is longer than even the maximum
1569 # possible space. Handle both cases below.
1571 if ($length_remaining == ($max_line_length - (length
($user_indent))))
1573 # The line is simply too long -- there is no hope of ever
1574 # breaking it nicely, so just insert it verbatim, with
1575 # appropriate padding.
1576 $this_line = "\n${left_pad_str}${this_line}";
1580 # Can't break it here, but may be able to on the next round...
1581 unshift
(@lines
, $this_line);
1582 $length_remaining = $max_line_length - (length
($user_indent));
1583 $this_line = "\n${left_pad_str}";
1587 else # $this_len < $length_remaining, so tack on what we can.
1589 # Leave a note for the next iteration.
1590 $length_remaining = $length_remaining - $this_len;
1592 if ($this_line =~
/\.$
/)
1595 $length_remaining -= 2;
1597 else # not a sentence end
1600 $length_remaining -= 1;
1604 # Unconditionally indicate that loop has run at least once.
1607 $wrapped_text .
= "${user_indent}${this_line}";
1610 # One last bit of padding.
1611 $wrapped_text .
= "\n";
1613 return $wrapped_text;
1616 # -------------------------------------
1618 sub _pretty_file_list
{
1621 my
($unanimous_tags, $non_unanimous_tags, $all_branches, $qunksref) = @_
;
1624 grep +( ( ! $_->tags_exists
1626 ! grep exists
$ignore_tags{$_}, @
{$_->tags
})
1632 grep exists
$show_tags{$_}, @
{$_->tags
} )
1637 my
$common_dir; # Dir prefix common to all files ('' if none)
1639 # First, loop over the qunks gathering all the tag/branch names.
1640 # We'll put them all in non_unanimous_tags, and take out the
1641 # unanimous ones later.
1643 foreach my
$qunkref (@qunkrefs
)
1645 # Keep track of whether all the files in this commit were in the
1646 # same directory, and memorize it if so. We can make the output a
1647 # little more compact by mentioning the directory only once.
1648 if ($Common_Dir && (scalar
(@qunkrefs
)) > 1)
1650 if (! (defined
($common_dir)))
1653 ($base, $dir, undef
) = fileparse
($qunkref->filename
);
1655 if ((! (defined
($dir))) # this first case is sheer paranoia
1667 elsif
($common_dir ne
'')
1669 # Already have a common dir prefix, so how much of it can we preserve?
1670 $common_dir = &main
::common_path_prefix
($qunkref->filename
, $common_dir);
1673 else # only one file in this entry anyway, so common dir not an issue
1678 if (defined
($qunkref->branch
)) {
1679 $all_branches->{$qunkref->branch
} = 1;
1681 if (defined
($qunkref->tags
)) {
1682 foreach my
$tag (@
{$qunkref->tags
}) {
1683 $non_unanimous_tags->{$tag} = 1;
1688 # Any tag held by all qunks will be printed specially... but only if
1689 # there are multiple qunks in the first place!
1690 if ((scalar
(@qunkrefs
)) > 1) {
1691 foreach my
$tag (keys
(%$non_unanimous_tags)) {
1692 my
$everyone_has_this_tag = 1;
1693 foreach my
$qunkref (@qunkrefs
) {
1694 if ((! (defined
($qunkref->tags
)))
1695 or
(! (grep ($_ eq
$tag, @
{$qunkref->tags
})))) {
1696 $everyone_has_this_tag = 0;
1699 if ($everyone_has_this_tag) {
1700 $unanimous_tags->{$tag} = 1;
1701 delete
$non_unanimous_tags->{$tag};
1706 return $common_dir, \@qunkrefs
;
1709 # -------------------------------------
1714 my
($year, $mday, $mon, $wday, $hour, $min);
1717 ($year, $mday, $mon, $wday, $hour, $min) = @_
;
1720 (undef
, $min, $hour, $mday, $mon, $year, $wday) =
1721 $UTC_Times ? gmtime
($time) : localtime
($time);
1725 $wday = $self->wday
($wday);
1728 my
$fdate = $self->fdate
($year, $mon, $mday, $wday);
1731 my
$ftime = $self->ftime
($hour, $min);
1732 return "$fdate $ftime";
1738 # -------------------------------------
1743 my
($year, $mday, $mon, $wday);
1746 ($year, $mon, $mday, $wday) = @_
;
1749 (undef
, undef
, undef
, $mday, $mon, $year, $wday) =
1750 $UTC_Times ? gmtime
($time) : localtime
($time);
1754 $wday = $self->wday
($wday);
1757 return sprintf
'%4u-%02u-%02u%s', $year, $mon, $mday, $wday;
1760 # -------------------------------------
1771 (undef
, $min, $hour) = $UTC_Times ? gmtime
($time) : localtime
($time);
1774 return sprintf
'%02u:%02u', $hour, $min;
1777 # ----------------------------------------------------------------------------
1779 package CVS
::Utils
::ChangeLog
::Message
;
1785 my
%self
= (msg
=> $msg, files
=> []);
1787 bless \
%self
, $class;
1792 my
($fileentry) = @_
;
1794 die
"Not a fileentry: $fileentry"
1795 unless
$fileentry->isa
('CVS::Utils::ChangeLog::FileEntry');
1797 push @
{$self->{files
}}, $fileentry;
1800 sub files
{ wantarray ? @
{$_[0]->{files
}} : $_[0]->{files
} }
1802 # ----------------------------------------------------------------------------
1804 package CVS
::Utils
::ChangeLog
::FileEntry
;
1806 use File
::Basename qw
( fileparse
);
1808 # Each revision of a file has a little data structure (a `qunk')
1809 # associated with it. That data structure holds not only the
1810 # file's name, but any additional information about the file
1811 # that might be needed in the output, such as the revision
1812 # number, tags, branches, etc. The reason to have these things
1813 # arranged in a data structure, instead of just appending them
1814 # textually to the file's name, is that we may want to do a
1815 # little rearranging later as we write the output. For example,
1816 # all the files on a given tag/branch will go together, followed
1817 # by the tag in parentheses (so trunk or otherwise non-tagged
1818 # files would go at the end of the file list for a given log
1819 # message). This rearrangement is a lot easier to do if we
1820 # don't have to reparse the text.
1822 # A qunk looks like this:
1825 # filename => "hello.c",
1826 # revision => "1.4.3.2",
1827 # time => a timegm() return value (moment of commit)
1828 # tags => [ "tag1", "tag2", ... ],
1829 # branch => "branchname" # There should be only one, right?
1830 # roots => [ "branchtag1", "branchtag2", ... ]
1831 # lines => "+x -y" # or undefined; x and y are integers
1834 # Single top-level ChangeLog, or one per subdirectory?
1836 sub distributed
{ $#_ ?
($distributed = $_[1]) : $distributed; }
1840 my
($path, $time, $revision, $state, $lines,
1841 $branch_names, $branch_roots, $branch_numbers, $symbolic_names) = @_
;
1843 my
%self
= (time => $time,
1844 revision
=> $revision,
1847 branch_numbers
=> $branch_numbers,
1850 if ( $distributed ) {
1851 @self
{qw
(filename dir_key
)} = fileparse
($path);
1853 @self
{qw
(filename dir_key
)} = ($path, './');
1856 { # Scope for $branch_prefix
1857 (my
($branch_prefix) = ($revision =~
/((?
:\d
+\.
)+)\d
+/));
1858 $branch_prefix =~ s
/\.$
//;
1859 if ( $branch_names->{$branch_prefix} ) {
1860 my
$branch_name = $branch_names->{$branch_prefix};
1861 $self{branch
} = $branch_name;
1862 $self{branches
} = [$branch_name];
1864 while ( $branch_prefix =~ s
/^
(\d
+(?
:\.\d
+\.\d
+)+)\.\d
+\.\d
+$
/$1/ ) {
1865 push @
{$self{branches
}}, $branch_names->{$branch_prefix}
1866 if exists
$branch_names->{$branch_prefix};
1870 # If there's anything in the @branch_roots array, then this
1871 # revision is the root of at least one branch. We'll display
1872 # them as branch names instead of revision numbers, the
1873 # substitution for which is done directly in the array:
1874 $self{'roots'} = [ map
{ $branch_names->{$_} } @
$branch_roots ]
1877 if ( exists
$symbolic_names->{$revision} ) {
1878 $self{tags
} = delete
$symbolic_names->{$revision};
1879 &main
::delta_check
($time, $self{tags
});
1882 bless \
%self
, $class;
1885 sub filename
{ $_[0]->{filename
} }
1886 sub dir_key
{ $_[0]->{dir_key
} }
1887 sub revision
{ $_[0]->{revision
} }
1888 sub branch
{ $_[0]->{branch
} }
1889 sub state
{ $_[0]->{state
} }
1890 sub lines
{ $_[0]->{lines
} }
1891 sub roots
{ $_[0]->{roots
} }
1892 sub branch_numbers
{ $_[0]->{branch_numbers
} }
1894 sub tags
{ $_[0]->{tags
} }
1896 exists
$_[0]->{tags
};
1899 # This may someday be used in a more sophisticated calculation of what other
1900 # files are involved in this commit. For now, we don't use it much except for
1901 # delta mode, because the common-commit-detection algorithm is hypothesized to
1902 # be "good enough" as it stands.
1903 sub
time { $_[0]->{time} }
1905 # ----------------------------------------------------------------------------
1907 package CVS
::Utils
::ChangeLog
::EntrySetBuilder
;
1909 use File
::Basename qw
( fileparse
);
1910 use Time
::Local qw
( timegm
);
1912 use constant MAILNAME
=> "/etc/mailname";
1914 # In 'cvs log' output, one long unbroken line of equal signs separates files:
1915 use constant FILE_SEPARATOR
=> '=' x
77;# . "\n";
1916 # In 'cvs log' output, a shorter line of dashes separates log messages within
1918 use constant REV_SEPARATOR
=> '-' x
28;# . "\n";
1920 use constant EMPTY_LOG_MESSAGE
=> '*** empty log message ***';
1922 # -------------------------------------
1926 my
$class = ref
$proto ||
$proto;
1928 my
$poobah = CVS
::Utils
::ChangeLog
::EntrySet-
>new
;
1929 my
$self = bless
+{ grand_poobah
=> $poobah }, $class;
1932 $self->maybe_read_user_map_file
;
1936 # -------------------------------------
1941 # Make way for the next message
1942 undef
$self->{rev_msg
};
1943 undef
$self->{rev_time
};
1944 undef
$self->{rev_revision
};
1945 undef
$self->{rev_author
};
1946 undef
$self->{rev_state
};
1947 undef
$self->{lines
};
1948 $self->{rev_branch_roots
} = []; # For showing which files are branch
1950 $self->{collecting_symbolic_names
} = 0;
1953 # -------------------------------------
1959 undef
$self->{filename
};
1960 $self->{branch_names
} = +{}; # We'll grab branch names while we're
1962 $self->{branch_numbers
} = +{}; # Save some revisions for
1964 $self->{symbolic_names
} = +{}; # Where tag names get stored.
1967 # -------------------------------------
1969 sub grand_poobah
{ $_[0]->{grand_poobah
} }
1971 # -------------------------------------
1973 sub read_changelog
{
1974 my
($self, $command) = @_
;
1978 if (! $Input_From_Stdin) {
1979 if ($^O
=~
/Win32
/i
) {
1980 open
(READER
, "@$command |")
1981 or die
"unable to run \"@$command\"";
1985 pipe
(READER
, WRITER
)
1986 or die
"Couldn't form pipe: $!\n";
1988 if (! defined
$pid) {
1989 die
"Couldn't fork: $!\n";
1991 if ( ! $pid ) { # child
1992 open STDOUT
, '>&=' . fileno WRITER
1993 or die
"Couldn't dup stderr to ", fileno WRITER
, "\n";
1994 # strangely, some perls give spurious warnings about STDIN being opened
1995 # for output only these close calls precede the STDOUT reopen above.
1996 # I think they must be reusing fd 1.
2006 &main
::debug
("(run \"@$command\")\n");
2009 open READER
, '-' or die
"unable to open stdin for reading";
2019 # If on a new file and don't see filename, skip until we find it, and
2020 # when we find it, grab it.
2021 if ( ! defined
$self->{filename
} ) {
2022 $self->read_file_path
($_);
2023 } elsif
( /^symbolic names
:$
/ ) {
2024 $self->{collecting_symbolic_names
} = 1;
2025 } elsif
( $self->{collecting_symbolic_names
} ) {
2026 $self->read_symbolic_name
($_);
2027 } elsif
( $_ eq FILE_SEPARATOR and
! defined
$self->{rev_revision
} ) {
2029 } elsif
( ! defined
$self->{rev_revision
} ) {
2030 # If have file name, but not revision, and see revision, then grab
2031 # it. (We collect unconditionally, even though we may or may not
2033 $self->read_revision
($_);
2034 } elsif
( ! defined
$self->{rev_time
} ) { # and /^date: /) {
2035 $self->read_date_author_and_state
($_);
2036 } elsif
( /^branches
:\s
+(.
*);$
/ ) {
2037 $self->read_branches
($1);
2038 } elsif
( ! ( $_ eq FILE_SEPARATOR or
$_ eq REV_SEPARATOR
) ) {
2039 # If have file name, time, and author, then we're just grabbing
2040 # log message texts:
2041 $self->{rev_msg
} .
= $_ .
"\n"; # Normally, just accumulate the message...
2044 if ( ! $self->{rev_msg
}
2045 or
$self->{rev_msg
} =~
/^\s
*(\.\s
*)?$
/
2046 or index
($self->{rev_msg
}, EMPTY_LOG_MESSAGE
) > -1 ) {
2047 # ... until a msg separator is encountered:
2048 # Ensure the message contains something:
2049 $self->clear_msg
, $noadd = 1
2050 if $Prune_Empty_Msgs;
2051 $self->{rev_msg
} = "[no log message]\n";
2054 $self->add_file_entry
2057 if ( $_ eq FILE_SEPARATOR
) {
2066 or die
"Couldn't close pipe reader: $!\n";
2067 if ( defined
$pid ) {
2071 or $
!=1, die sprintf
("Problem reading log input (pid/exit/signal/core: %d/%d/%d/%d)\n",
2072 $pid, $?
>> 8, $?
& 127, $?
& 128);
2077 # -------------------------------------
2079 sub add_file_entry
{
2080 $_[0]->grand_poobah-
>add_fileentry
(@
{$_[0]}{qw
(filename rev_time rev_revision
2081 rev_state lines branch_names
2085 rev_author rev_msg
)});
2088 # -------------------------------------
2090 sub maybe_read_user_map_file
{
2096 if (defined
$User_Passwd_File)
2098 if ( ! defined
$Domain ) {
2099 if ( -e MAILNAME
) {
2100 chomp
($Domain = slurp_file
(MAILNAME
));
2103 for ([qw
(hostname
-d)], 'dnsdomainname', 'domainname') {
2104 my
($text, $exit, $sig, $core) = run_ext
($_);
2105 if ( $exit == 0 && $sig == 0 && $core == 0 ) {
2107 if ( length
$text ) {
2109 last MAILDOMAIN_CMD
;
2116 die
"No mail domain found\n"
2117 unless defined
$Domain;
2119 open
(MAPFILE
, "<$User_Passwd_File")
2120 or die
("Unable to open $User_Passwd_File ($!)");
2123 # all lines are valid
2124 my
($username, $pw, $uid, $gid, $gecos, $homedir, $shell) = split ':';
2126 ($expansion) = split (',', $gecos)
2127 if defined
$gecos && length
$gecos;
2129 my
$mailname = $Domain eq
'' ?
$username : "$username\@$Domain";
2130 $expansions{$username} = "$expansion <$mailname>";
2137 if ( $User_Map_File =~ m
{^
([-\w\@
+=.
,\
/]+):([-\w\@
+=.
,\
/:]+)} and
2138 !-f $User_Map_File )
2140 my
$rsh = (exists
$ENV{'CVS_RSH'} ?
$ENV{'CVS_RSH'} : 'ssh');
2141 $User_Map_Input = "$rsh $1 'cat $2' |";
2142 &main
::debug
("(run \"${User_Map_Input}\")\n");
2146 $User_Map_Input = "<$User_Map_File";
2149 open
(MAPFILE
, $User_Map_Input)
2150 or die
("Unable to open $User_Map_File ($!)");
2154 next
if /^\s
*#/; # Skip comment lines.
2155 next
if not
/:/; # Skip lines without colons.
2157 # It is now safe to split on ':'.
2158 my
($username, $expansion) = split ':';
2160 $expansion =~ s
/^
'(.*)'$
/$1/;
2161 $expansion =~ s
/^
"(.*)"$
/$1/;
2163 # If it looks like the expansion has a real name already, then
2164 # we toss the username we got from CVS log. Otherwise, keep
2165 # it to use in combination with the email address.
2167 if ($expansion =~
/^\s
*<{0,1}\S
+@.
*/) {
2168 # Also, add angle brackets if none present
2169 if (! ($expansion =~
/<\S
+@\S
+>/)) {
2170 $expansions{$username} = "$username <$expansion>";
2173 $expansions{$username} = "$username $expansion";
2177 $expansions{$username} = $expansion;
2179 } # fi ($User_Map_File)
2184 $self->{usermap
} = \
%expansions
;
2187 # -------------------------------------
2189 sub read_file_path
{
2190 my
($self, $line) = @_
;
2194 if ( $line =~
/^Working
file: (.
*)/ ) {
2196 } elsif
( defined
$RCS_Root
2198 $line =~ m|^RCS
file: $RCS_Root[/\\](.
*),v$|
) {
2200 $path =~ s
!Attic
/!!;
2205 if ( @Ignore_Files
) {
2207 ($base, undef
, undef
) = fileparse
($path);
2209 my
$xpath = $Case_Insensitive ? lc
($path) : $path;
2211 if grep $path =~
/$_/, @Ignore_Files
;
2214 $self->{filename
} = $path;
2218 # -------------------------------------
2220 sub read_symbolic_name
{
2221 my
($self, $line) = @_
;
2223 # All tag names are listed with whitespace in front in cvs log
2224 # output; so if see non-whitespace, then we're done collecting.
2226 $self->{collecting_symbolic_names
} = 0;
2229 # we're looking at a tag name, so parse & store it
2231 # According to the Cederqvist manual, in node "Tags", tag names must start
2232 # with an uppercase or lowercase letter and can contain uppercase and
2233 # lowercase letters, digits, `-', and `_'. However, it's not our place to
2234 # enforce that, so we'll allow anything CVS hands us to be a tag:
2235 my
($tag_name, $tag_rev) = ($line =~
/^\s
+([^
:]+): ([\d.
]+)$
/);
2237 # A branch number either has an odd number of digit sections
2238 # (and hence an even number of dots), or has ".0." as the
2239 # second-to-last digit section. Test for these conditions.
2240 my
$real_branch_rev = '';
2241 if ( $tag_rev =~
/^
(\d
+\.\d
+\.
)+\d
+$
/ # Even number of dots...
2243 $tag_rev !~
/^
(1\.
)+1$
/ ) { # ...but not "1.[1.]1"
2244 $real_branch_rev = $tag_rev;
2245 } elsif
($tag_rev =~
/(\d
+\.
(\d
+\.
)+)0.
(\d
+)/) { # Has ".0."
2246 $real_branch_rev = $1 .
$3;
2249 # If we got a branch, record its number.
2250 if ( $real_branch_rev ) {
2251 $self->{branch_names
}->{$real_branch_rev} = $tag_name;
2252 $self->{branch_numbers
}->{$tag_name} = $real_branch_rev;
2254 # Else it's just a regular (non-branch) tag.
2255 push @
{$self->{symbolic_names
}->{$tag_rev}}, $tag_name;
2259 $self->{collecting_symbolic_names
} = 1;
2263 # -------------------------------------
2266 my
($self, $line) = @_
;
2268 my
($revision) = ( $line =~
/^revision
(\d
+\.
[\d.
]+)/ );
2273 $self->{rev_revision
} = $revision;
2277 # -------------------------------------
2279 { # Closure over %gecos_warned
2281 sub read_date_author_and_state
{
2282 my
($self, $line) = @_
;
2284 my
($time, $author, $state) = $self->parse_date_author_and_state
($line);
2286 if ( defined
($self->{usermap
}->{$author}) and
$self->{usermap
}->{$author} ) {
2287 $author = $self->{usermap
}->{$author};
2288 } elsif
( defined
$Domain or
$Gecos == 1 ) {
2289 my
$email = $author;
2290 $email = $author.
"@".
$Domain
2291 if defined
$Domain && $Domain ne
'';
2293 my
$pw = getpwnam
($author);
2294 my
($fullname, $office, $workphone, $homephone, $gcos);
2295 if ( defined
$pw ) {
2296 $gcos = (getpwnam
($author))[6];
2297 ($fullname, $office, $workphone, $homephone) =
2298 split /\s
*,\s
*/, $gcos;
2300 warn
"Couldn't find gecos info for author '$author'\n"
2301 unless
$gecos_warned{$author}++;
2304 for (grep defined
, $fullname, $office, $workphone, $homephone) {
2305 s
/&/ucfirst
(lc
($pw))/ge
;
2307 $author = $fullname .
" <" .
$email .
">"
2308 if defined
$fullname && $fullname ne
'';
2311 $self->{rev_state
} = $state;
2312 $self->{rev_time
} = $time;
2313 $self->{rev_author
} = $author;
2318 # -------------------------------------
2321 # A "branches: ..." line here indicates that one or more branches
2322 # are rooted at this revision. If we're showing branches, then we
2323 # want to show that fact as well, so we collect all the branches
2324 # that this is the latest ancestor of and store them in
2325 # $self->[rev_branch_roots}. Just for reference, the format of the
2326 # line we're seeing at this point is:
2328 # branches: 1.5.2; 1.5.4; ...;
2331 my
($self, $line) = @_
;
2333 # Ugh. This really bothers me. Suppose we see a log entry
2336 # ----------------------------
2338 # date: 1999/10/17 03:07:38; author: jrandom; state: Exp;
2340 # Intended first line of log message begins here.
2341 # ----------------------------
2343 # The question is, how we can tell the difference between that
2344 # log message and a *two*-line log message whose first line is
2346 # "branches: 1.1.2;"
2348 # See the problem? The output of "cvs log" is inherently
2351 # For now, we punt: we liberally assume that people don't
2352 # write log messages like that, and just toss a "branches:"
2353 # line if we see it but are not showing branches. I hope no
2354 # one ever loses real log data because of this.
2355 if ( $Show_Branches ) {
2356 $line =~ s
/(1\.
)+1;|
(1\.
)+1$
//; # ignore the trivial branch 1.1.1
2357 $self->{rev_branch_roots
} = [split /;\s
+/, $line]
2362 # -------------------------------------
2364 sub parse_date_author_and_state
{
2365 my
($self, $line) = @_
;
2366 # Parses the date/time and author out of a line like:
2368 # date: 1999/02/19 23:29:05; author: apharris; state: Exp;
2370 # or, in CVS 1.12.9:
2372 # date: 2004-06-05 16:10:32 +0000; author: somebody; state: Exp;
2374 my
($year, $mon, $mday, $hours, $min, $secs, $utcOffset, $author, $state, $rest) =
2376 m
!(\d
+)[-/](\d
+)[-/](\d
+)\s
+(\d
+):(\d
+):(\d
+)(\s
+[+-]\d
{4})?
;\s
+
2377 author
:\s
+([^
;]+);\s
+state
:\s
+([^
;]+);(.
*)!x
2378 or die
"Couldn't parse date ``$line''";
2379 die
"Bad date or Y2K issues"
2380 unless
$year > 1969 and
$year < 2258;
2381 # Kinda arbitrary, but useful as a sanity check
2382 my
$time = timegm
($secs, $min, $hours, $mday, $mon-1, $year-1900);
2383 if ( defined
$utcOffset ) {
2384 my
($plusminus, $hour, $minute) = ($utcOffset =~ m
/([+-])(\d\d
)(\d\d
)/);
2385 my
$offset = (($hour * 60) + $minute) * 60 * ($plusminus eq
'+' ?
-1 : 1);
2388 if ( $rest =~ m
!\s
+lines
:\s
+(.
*)! ) {
2389 $self->{lines
} = $1;
2392 return $time, $author, $state;
2395 # Subrs ----------------------------------------------------------------------
2400 my
($time, $tags) = @_
;
2402 # If we're in 'delta' mode, update the latest observed times for the
2403 # beginning and ending tags, and when we get around to printing output, we
2404 # will simply restrict ourselves to that timeframe...
2408 $Delta_StartTime = $time
2409 if $time > $Delta_StartTime and
$Delta_From and
grep { $_ eq
$Delta_From } @
$tags;
2411 $Delta_EndTime = $time
2412 if $time > $Delta_EndTime and
$Delta_To and
grep { $_ eq
$Delta_To } @
$tags;
2420 my $out = qx"@
$cmd 2>&1";
2422 my ($sig, $core, $exit) = ($? & 127, $? & 128, $? >> 8);
2423 return $out, $exit, $sig, $core;
2426 # -------------------------------------
2428 # If accumulating, grab the boundary date from pre-existing ChangeLog.
2429 sub maybe_grab_accumulation_date {
2430 if (! $Cumulative || $Update) {
2436 open (LOG, "$Log_File_Name")
2437 or die ("trouble opening
$Log_File_Name for reading
($
!)");
2442 if (/^(\d\d\d\d-\d\d-\d\d\s+(\w+\s+)?\d\d:\d\d)/)
2444 $boundary_date = "$1";
2451 # convert time from utc to local timezone if the ChangeLog has
2452 # dates/times in utc
2453 if ($UTC_Times && $boundary_date)
2455 # convert the utc time to a time value
2456 my ($year,$mon,$mday,$hour,$min) = $boundary_date =~
2457 m#(\d+)-(\d+)-(\d+)\s+(\d+):(\d+)#;
2458 my $time = timegm(0,$min,$hour,$mday,$mon-1,$year-1900);
2459 # print the timevalue in the local timezone
2461 ($ignore,$min,$hour,$mday,$mon,$year,$wday) = localtime($time);
2462 $boundary_date=sprintf ("%4u-%02u-%02u %02u:%02u",
2463 $year+1900,$mon+1,$mday,$hour,$min);
2466 return $boundary_date;
2469 # -------------------------------------
2471 # Fills up a ChangeLog structure in the current directory.
2472 sub derive_changelog {
2475 # See "The Plan
" above for a full explanation.
2477 # Might be adding to an existing ChangeLog
2478 my $accumulation_date = maybe_grab_accumulation_date;
2479 if ($accumulation_date) {
2480 # Insert -d immediately after 'cvs log'
2481 my $Log_Date_Command = "-d>${accumulation_date}";
2483 my ($log_index) = grep $command->[$_] eq 'log', 0..$#$command;
2484 splice @$command, $log_index+1, 0, $Log_Date_Command;
2485 &debug ("(adding log msg starting from
$accumulation_date)\n");
2488 # output_changelog(read_changelog($command));
2489 my $builder = CVS::Utils::ChangeLog::EntrySetBuilder->new;
2490 $builder->read_changelog($command);
2491 $builder->grand_poobah->output_changelog;
2494 # -------------------------------------
2496 sub min { $_[0] < $_[1] ? $_[0] : $_[1] }
2498 # -------------------------------------
2500 sub common_path_prefix {
2501 my ($path1, $path2) = @_;
2503 # For compatibility (with older versions of cvs2cl.pl), we think in UN*X
2504 # terms, and mould windoze filenames to match. Is this really appropriate?
2505 # If a file is checked in under UN*X, and cvs log run on windoze, which way
2506 # do the path separators slope? Can we use fileparse as per the local
2507 # conventions? If so, we should probably have a user option to specify an
2508 # OS to emulate to handle stdin-fed logs. If we did this, we could avoid
2509 # the nasty \-/ transmogrification below.
2511 my ($dir1, $dir2) = map +(fileparse($_))[1], $path1, $path2;
2513 # Transmogrify Windows filenames to look like Unix.
2514 # (It is far more likely that someone is running cvs2cl.pl under
2515 # Windows than that they would genuinely have backslashes in their
2520 my ($accum1, $accum2, $last_common_prefix) = ('') x 3;
2522 my @path1 = grep length($_), split qr!/!, $dir1;
2523 my @path2 = grep length($_), split qr!/!, $dir2;
2526 for (0..min($#path1,$#path2)) {
2527 if ( $path1[$_] eq $path2[$_]) {
2528 push @common_path, $path1[$_];
2534 return join '', map "$_/", @common_path;
2537 # -------------------------------------
2540 # Check this internally before setting the global variable.
2543 # If this gets set, we encountered unknown options and will exit at
2544 # the end of this subroutine.
2545 my $exit_with_admonishment = 0;
2547 # command to generate the log
2548 my @log_source_command = qw( cvs log );
2550 my (@Global_Opts, @Local_Opts);
2552 Getopt::Long::Configure(qw( bundling permute no_getopt_compat
2553 pass_through no_ignore_case ));
2554 GetOptions('help|usage|h' => \$Print_Usage,
2555 'debug' => \$Debug, # unadvertised option, heh
2556 'version' => \$Print_Version,
2558 'file|f=s' => \$output_file,
2559 'accum' => \$Cumulative,
2560 'update' => \$Update,
2561 'fsf' => \$FSF_Style,
2562 'rcs=s' => \$RCS_Root,
2563 'usermap|U=s' => \$User_Map_File,
2565 'domain=s' => \$Domain,
2566 'passwd=s' => \$User_Passwd_File,
2567 'window|W=i' => \$Max_Checkin_Duration,
2568 'chrono' => \$Chronological_Order,
2569 'ignore|I=s' => \@Ignore_Files,
2570 'case-insensitive|C' => \$Case_Insensitive,
2571 'regexp|R=s' => \$Regexp_Gate,
2572 'stdin' => \$Input_From_Stdin,
2573 'stdout' => \$Output_To_Stdout,
2574 'distributed|d' => sub { CVS::Utils::ChangeLog::FileEntry->distributed(1) },
2575 'prune|P' => \$Prune_Empty_Msgs,
2576 'no-wrap' => \$No_Wrap,
2577 'gmt|utc' => \$UTC_Times,
2578 'day-of-week|w' => \$Show_Day_Of_Week,
2579 'revisions|r' => \$Show_Revisions,
2580 'show-dead' => \$Show_Dead,
2581 'tags|t' => \$Show_Tags,
2582 'tag-regexp=s' => \$Regexp_Tag,
2583 'tagdates|T' => \$Show_Tag_Dates,
2584 'branches|b' => \$Show_Branches,
2585 'follow|F=s' => \@Follow_Branches,
2586 'follow-only=s' => \@Follow_Only,
2587 'xml-encoding=s' => \$XML_Encoding,
2588 'xml' => \$XML_Output,
2589 'noxmlns' => \$No_XML_Namespace,
2590 'no-xml-iso-date' => \$No_XML_ISO_Date,
2591 'no-ancestors' => \$No_Ancestors,
2592 'lines-modified' => \$Show_Lines_Modified,
2594 'no-indent' => sub {
2600 $After_Header = "\n\n"; # Summary implies --separate-header
2607 'no-hide-branch-additions' => sub {
2608 $Hide_Branch_Additions = 0;
2611 'no-common-dir' => sub {
2615 'ignore-tag=s' => sub {
2616 $ignore_tags{$_[1]} = 1;
2619 'show-tag=s' => sub {
2620 $show_tags{$_[1]} = 1;
2623 # Deliberately undocumented. This is not a public interface, and
2624 # may change/disappear at any time.
2625 'test-code=s' => \$TestCode,
2630 /^([A-Za-z][A-Za-z0-9_\-\]\[\.]*)?:([A-Za-z][A-Za-z0-9_\-\]\[\.]*)?$/ )
2636 die "--delta FROM_TAG
:TO_TAG is what you meant to say.
\n";
2644 $No_Extra_Indent = 1;
2650 $ChangeLog_Header = &slurp_file ($narg);
2651 if (! defined ($ChangeLog_Header)) {
2652 $ChangeLog_Header = '';
2656 'global-opts|g=s' => sub {
2658 push @Global_Opts, $narg;
2659 splice @log_source_command, 1, 0, $narg;
2662 'log-opts|l=s' => sub {
2664 push @Local_Opts, $narg;
2665 push @log_source_command, $narg;
2668 'mailname=s' => sub {
2670 warn "--mailname is deprecated
; please use
--domain instead
\n";
2674 'separate-header|S' => sub {
2675 $After_Header = "\n\n";
2676 $No_Extra_Indent = 1;
2679 'group-by-date' => sub {
2684 'group-by-author' => sub {
2690 'hide-filenames' => sub {
2691 $Hide_Filenames = 1;
2695 or die "options parsing failed
\n";
2697 push @log_source_command, map "$_", @ARGV;
2699 ## Check for contradictions...
2701 if ($Output_To_Stdout && CVS::Utils::ChangeLog::FileEntry->distributed) {
2702 print STDERR "cannot pass both
--stdout and
--distributed\n";
2703 $exit_with_admonishment = 1;
2706 if ($Output_To_Stdout && $output_file) {
2707 print STDERR "cannot pass both
--stdout and
--file\n";
2708 $exit_with_admonishment = 1;
2711 if ($Input_From_Stdin && @Global_Opts) {
2712 print STDERR "cannot pass both
--stdin and
-g\n";
2713 $exit_with_admonishment = 1;
2716 if ($Input_From_Stdin && @Local_Opts) {
2717 print STDERR "cannot pass both
--stdin and
-l\n";
2718 $exit_with_admonishment = 1;
2721 if ($XML_Output && $Cumulative) {
2722 print STDERR "cannot pass both
--xml and
--accum\n";
2723 $exit_with_admonishment = 1;
2726 if ($FSF_Output && $Cumulative) {
2727 print STDERR "cannot pass both
--FSF and
--accum\n";
2728 $exit_with_admonishment = 1;
2731 # Other consistency checks and option-driven logic
2733 # Bleargh. Compensate for a deficiency of custom wrapping.
2734 if ( ($After_Header ne " ") and $FSF_Style ) {
2735 $After_Header .= "\t";
2738 @Ignore_Files = map lc, @Ignore_Files
2739 if $Case_Insensitive;
2741 # Or if any other error message has already been printed out, we
2743 if ($exit_with_admonishment) {
2747 elsif ($Print_Usage) {
2751 elsif ($Print_Version) {
2756 ## Else no problems, so proceed.
2759 $Log_File_Name = $output_file;
2762 return \@log_source_command;
2765 # -------------------------------------
2768 my $filename = shift || die ("no filename passed to slurp_file
()");
2771 open (SLURPEE, "<${filename}") or die ("unable to open
$filename ($
!)");
2773 $retstr = <SLURPEE>;
2778 # -------------------------------------
2787 # -------------------------------------
2790 print "cvs2cl.pl version
${VERSION}; distributed under the GNU GPL.
\n";
2793 # -------------------------------------
2798 eval "use Pod
::Usage qw
( pod2usage
)";
2803 * Pod::Usage was not found. The formatting may be suboptimal. Consider
2804 upgrading your Perl --- Pod::Usage is standard from 5.6 onwards, and
2805 versions of perl prior to 5.6 are getting rather rusty, now. Alternatively,
2806 install Pod::Usage direct from CPAN.
2810 my $message = <DATA>;
2811 $message =~ s/^=(head1|item) //gm;
2812 $message =~ s/^=(over|back).*\n//gm;
2813 $message =~ s/\n{3,}/\n\n/g;
2817 pod2usage( -exitval => 'NOEXIT',
2819 -output => \*STDOUT,
2826 # Main -----------------------------------------------------------------------
2828 my $log_source_command = parse_options;
2829 if ( defined $TestCode ) {
2831 die "Eval failed
: '$@'\n"
2834 derive_changelog($log_source_command);
2841 cvs2cl.pl - convert cvs log messages to changelogs
2845 B<cvs2cl> [I<options>] [I<FILE1> [I<FILE2> ...]]
2849 cvs2cl produces a GNU-style ChangeLog for CVS-controlled sources by
2850 running "cvs log
" and parsing the output. Duplicate log messages get
2851 unified in the Right Way.
2853 The default output of cvs2cl is designed to be compact, formally unambiguous,
2854 but still easy for humans to read. It should be largely self-explanatory; the
2855 one abbreviation that might not be obvious is "utags
". That stands for
2856 "universal tags
" -- a universal tag is one held by all the files in a given
2859 If you need output that's easy for a program to parse, use the B<--xml> option.
2860 Note that with XML output, just about all available information is included
2861 with each change entry, whether you asked for it or not, on the theory that
2862 your parser can ignore anything it's not looking for.
2864 If filenames are given as arguments cvs2cl only shows log information for the
2871 =item B<-h>, B<-help>, B<--help>, B<-?>
2873 Show a short help and exit.
2877 Show version and exit.
2879 =item B<-r>, B<--revisions>
2881 Show revision numbers in output.
2883 =item B<-b>, B<--branches>
2885 Show branch names in revisions when possible.
2887 =item B<-t>, B<--tags>
2889 Show tags (symbolic names) in output.
2891 =item B<-T>, B<--tagdates>
2893 Show tags in output on their first occurance.
2895 =item B<--show-dead>
2901 Read from stdin, don't run cvs log.
2905 Output to stdout not to ChangeLog.
2907 =item B<-d>, B<--distributed>
2909 Put ChangeLogs in subdirs.
2911 =item B<-f> I<FILE>, B<--file> I<FILE>
2913 Write to I<FILE> instead of ChangeLog.
2917 Use this if log data is in FSF ChangeLog style.
2921 Attempt strict FSF-standard compatible output (incompatible with B<--accum>).
2923 =item B<-W> I<SECS>, B<--window> I<SECS>
2925 Window of time within which log entries unify.
2927 =item -B<U> I<UFILE>, B<--usermap> I<UFILE>
2929 Expand usernames to email addresses from I<UFILE>.
2931 =item B<--passwd> I<PASSWORDFILE>
2933 Use system passwd file for user name expansion. If no mail domain is provided
2934 (via B<--domain>), it tries to read one from B</etc/mailname>, output of B<hostname
2935 -d>, B<dnsdomainname>, or B<domain-name>. cvs2cl exits with an error if none of
2936 those options is successful. Use a domain of '' to prevent the addition of a
2939 =item B<--domain> I<DOMAIN>
2941 Domain to build email addresses from.
2945 Get user information from GECOS data.
2947 =item B<-R> I<REGEXP>, B<--regexp> I<REGEXP>
2949 Include only entries that match I<REGEXP>. This option may be used multiple
2952 =item B<-I> I<REGEXP>, B<--ignore> I<REGEXP>
2954 Ignore files whose names match I<REGEXP>. This option may be used multiple
2955 times. The regexp is a perl regular expression. It is matched as is; you may
2956 want to prefix with a ^ or suffix with a $ to anchor the match.
2958 =item B<-C>, B<--case-insensitive>
2960 Any regexp matching is done case-insensitively.
2962 =item B<-F> I<BRANCH>, B<--follow> I<BRANCH>
2964 Show only revisions on or ancestral to I<BRANCH>.
2966 =item B<--follow-only> I<BRANCH>
2968 Like --follow, but sub-branches are not followed.
2970 =item B<--no-ancestors>
2972 When using B<-F>, only track changes since the I<BRANCH> started.
2974 =item B<--no-hide-branch-additions>
2976 By default, entries generated by cvs for a file added on a branch (a dead 1.1
2977 entry) are not shown. This flag reverses that action.
2979 =item B<-S>, B<--separate-header>
2981 Blank line between each header and log message.
2983 =item B<--group-by-date>
2985 Group ChangeLog entries on the same date together, instead of having a
2986 separate entry for each commit on that date.
2988 =item B<--group-by-author>
2990 Group consecutive ChangeLog entries from same author during same date,
2991 instead of having separate entry for each commit.
2995 Add CVS change summary information.
2999 Don't auto-wrap log message (recommend B<-S> also).
3001 =item B<--no-indent>
3003 Don't indent log message
3005 =item B<--gmt>, B<--utc>
3007 Show times in GMT/UTC instead of local time.
3011 Add to an existing ChangeLog (incompatible with B<--xml> and B<--FSF>).
3013 =item B<-w>, B<--day-of-week>
3019 Don't show times in output.
3023 Output log in chronological order (default is reverse chronological order).
3025 =item B<--header> I<FILE>
3027 Get ChangeLog header from I<FILE> ("B
<->" means stdin).
3031 Output XML instead of ChangeLog format (incompatible with B<--accum>).
3033 =item B<--xml-encoding> I<ENCODING.>
3035 Insert encoding clause in XML header.
3039 Don't include xmlns= attribute in root element.
3041 =item B<--hide-filenames>
3043 Don't show filenames (ignored for XML output).
3045 =item B<--no-common-dir>
3047 Don't shorten directory names from filenames.
3049 =item B<--rcs> I<CVSROOT>
3051 Handle filenames from raw RCS, for instance those produced by "cvs rlog
"
3052 output, stripping the prefix I<CVSROOT>.
3054 =item B<-P>, B<--prune>
3056 Don't show empty log messages.
3058 =item B<--lines-modified>
3060 Output the number of lines added and the number of lines removed for
3061 each checkin (if applicable). At the moment, this only affects the
3064 =item B<--ignore-tag> I<TAG>
3066 Ignore individual changes that are associated with a given tag.
3067 May be repeated, if so, changes that are associated with any of
3068 the given tags are ignored.
3070 =item B<--show-tag> I<TAG>
3072 Log only individual changes that are associated with a given
3073 tag. May be repeated, if so, changes that are associated with
3074 any of the given tags are logged.
3076 =item B<--delta> I<FROM_TAG>B<:>I<TO_TAG>
3078 Attempt a delta between two tags (since I<FROM_TAG> up to and
3079 including I<TO_TAG>). The algorithm is a simple date-based one
3080 (this is a hard problem) so results are imperfect.
3082 =item B<-g> I<OPTS>, B<--global-opts> I<OPTS>
3084 Pass I<OPTS> to cvs like in "cvs I
<OPTS
> log ...
".
3086 =item B<-l> I<OPTS>, B<--log-opts> I<OPTS>
3088 Pass I<OPTS> to cvs log like in "cvs ... log I
<OPTS
>".
3092 Notes about the options and arguments:
3098 The B<-I> and B<-F> options may appear multiple times.
3102 To follow trunk revisions, use "B
<-F trunk
>" ("B
<-F TRUNK
>" also works). This is
3103 okay because no would ever, ever be crazy enough to name a branch "trunk
",
3108 For the B<-U> option, the I<UFILE> should be formatted like CVSROOT/users. That is,
3109 each line of I<UFILE> looks like this:
3111 jrandom:jrandom@red-bean.com
3113 or maybe even like this
3115 jrandom:'Jesse Q. Random <jrandom@red-bean.com>'
3117 Don't forget to quote the portion after the colon if necessary.
3121 Many people want to filter by date. To do so, invoke cvs2cl.pl like this:
3123 cvs2cl.pl -l "-d'DATESPEC'"
3125 where DATESPEC is any date specification valid for "cvs log
-d". (Note that
3126 CVS 1.10.7 and below requires there be no space between -d and its argument).
3130 Dates/times are interpreted in the local time zone.
3134 Remember to quote the argument to `B<-l>' so that your shell doesn't interpret
3135 spaces as argument separators.
3139 See the 'Common Options' section of the cvs manual ('info cvs' on UNIX-like
3140 systems) for more information.
3144 Note that the rules for quoting under windows shells are different.
3148 To run in an automated environment such as CGI or PHP, suidperl may be needed
3149 in order to execute as the correct user to enable /cvsroot read lock files to
3150 be written for the 'cvs log' command. This is likely just a case of changing
3151 the /usr/bin/perl command to /usr/bin/suidperl, and explicitly declaring the
3158 Some examples (working on UNIX shells):
3160 # logs after 6th March, 2003 (inclusive)
3161 cvs2cl.pl -l "-d'>2003-03-06'"
3162 # logs after 4:34PM 6th March, 2003 (inclusive)
3163 cvs2cl.pl -l "-d'>2003-03-06 16:34'"
3164 # logs between 4:46PM 6th March, 2003 (exclusive) and
3165 # 4:34PM 6th March, 2003 (inclusive)
3166 cvs2cl.pl -l "-d'2003-03-06 16:46>2003-03-06 16:34'"
3168 Some examples (on non-UNIX shells):
3170 # Reported to work on windows xp/2000
3171 cvs2cl.pl -l "-d"">2003-10-18;today
<"""
3179 =item Melissa O'Neill
3181 =item Martyn J. Pearce
3193 =item Richard Broberg
3197 =item Oswald Buddenhagen
3201 =item Arthur de Jong
3203 =item Mark W. Eichin
3209 =item Simon Josefsson
3211 =item Robin Hugh Johnson
3223 =item Richard Laager
3227 =item Karl-Heinz Marbaise
3229 =item Mitsuaki Masuhara
3231 =item Henrik Nordstrom
3235 =item Peter Palfrader
3237 =item Thomas Parmelan
3239 =item Jordan Russell
3241 =item Jacek Sliwerski
3243 =item Johannes Stezenbach
3253 Please report bugs to C<cvs2cl-reports {_AT_} red-bean.com>.
3255 =head1 PREREQUISITES
3257 This script requires C<Text::Wrap>, C<Time::Local>, and C<File::Basename>. It
3258 also seems to require C<Perl 5.004_04> or higher.
3260 =head1 OPERATING SYSTEM COMPATIBILITY
3262 Should work on any OS.
3264 =head1 SCRIPT CATEGORIES
3270 (C) 2001,2002,2003,2004 Martyn J. Pearce, under the GNU GPL.
3272 (C) 1999 Karl Fogel, under the GNU GPL.
3274 cvs2cl.pl is free software; you can redistribute it and/or modify
3275 it under the terms of the GNU General Public License as published by
3276 the Free Software Foundation; either version 2, or (at your option)
3279 cvs2cl.pl is distributed in the hope that it will be useful,
3280 but WITHOUT ANY WARRANTY; without even the implied warranty of
3281 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3282 GNU General Public License for more details.
3284 You may have received a copy of the GNU General Public License
3285 along with cvs2cl.pl; see the file COPYING. If not, write to the
3286 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
3287 Boston, MA 02111-1307, USA.