File indexing completed on 2025-08-03 08:22:00
0001
0002 exec perl -w -x $0 ${1+"$@"}
0003
0004
0005
0006
0007
0008
0009
0010
0011
0012
0013
0014
0015
0016
0017
0018
0019
0020
0021
0022
0023
0024
0025
0026
0027
0028
0029
0030
0031
0032
0033
0034
0035
0036 use strict;
0037 use Text::Wrap;
0038 use Time::Local;
0039 use File::Basename;
0040
0041
0042
0043
0044
0045
0046
0047
0048
0049
0050
0051
0052
0053
0054
0055
0056
0057
0058
0059
0060
0061
0062
0063
0064
0065
0066
0067
0068
0069
0070
0071
0072
0073
0074
0075
0076
0077
0078 my $Log_Source_Command = "cvs log";
0079
0080
0081 my $VERSION = '$Revision: 1.1.1.1 $';
0082 $VERSION =~ s/\S+\s+(\S+)\s+\S+/$1/;
0083
0084
0085
0086
0087 my $Debug = 0;
0088
0089
0090 my $Print_Version = 0;
0091
0092
0093 my $Print_Usage = 0;
0094
0095
0096 my $Distributed = 0;
0097
0098
0099 my $Log_File_Name = "ChangeLog";
0100
0101
0102 my $User_Map_File = "";
0103
0104
0105 my $Output_To_Stdout = 0;
0106
0107
0108 my $Prune_Empty_Msgs = 0;
0109
0110
0111 my $No_Wrap = 0;
0112
0113
0114 my $After_Header = " ";
0115
0116
0117 my $XML_Output = 0;
0118
0119
0120 my $UTC_Times = 0;
0121
0122
0123 my $Show_Day_Of_Week = 0;
0124
0125
0126 my $Show_Revisions = 0;
0127
0128
0129 my $Show_Tags = 0;
0130
0131
0132 my $Show_Branches = 0;
0133
0134
0135 my @Follow_Branches;
0136
0137
0138 my @Ignore_Files;
0139
0140
0141
0142 my $Case_Insensitive = 0;
0143
0144
0145 my $Regexp_Gate = "";
0146
0147
0148 my $Global_Opts = "";
0149
0150
0151 my $Command_Opts = "";
0152
0153
0154 my $Input_From_Stdin = 0;
0155
0156
0157
0158
0159
0160
0161 my $Max_Checkin_Duration = 180;
0162
0163
0164 my $ChangeLog_Header = "";
0165
0166
0167
0168
0169
0170 my $file_separator = "======================================="
0171 . "======================================";
0172
0173
0174
0175 my $logmsg_separator = "----------------------------";
0176
0177
0178
0179
0180
0181
0182
0183 &parse_options ();
0184 &derive_change_log ();
0185
0186
0187
0188
0189
0190
0191 sub derive_change_log ()
0192 {
0193
0194
0195 my %grand_poobah;
0196
0197 my $file_full_path;
0198 my $time;
0199 my $revision;
0200 my $author;
0201 my $msg_txt;
0202 my $detected_file_separator;
0203
0204
0205 my %usermap;
0206
0207
0208
0209
0210
0211 my $collecting_symbolic_names = 0;
0212 my %symbolic_names;
0213 my %branch_names;
0214 my %branch_numbers;
0215 my @branch_roots;
0216
0217
0218 if (! $Input_From_Stdin) {
0219 open (LOG_SOURCE, "$Log_Source_Command |")
0220 or die "unable to run \"${Log_Source_Command}\"";
0221 }
0222 else {
0223 open (LOG_SOURCE, "-") or die "unable to open stdin for reading";
0224 }
0225
0226 %usermap = &maybe_read_user_map_file ();
0227
0228 while (<LOG_SOURCE>)
0229 {
0230
0231
0232 if ((! (defined $file_full_path)) and /^Working file: (.*)/) {
0233 $file_full_path = $1;
0234 if (@Ignore_Files) {
0235 my $base;
0236 ($base, undef, undef) = fileparse ($file_full_path);
0237
0238
0239 if ($Case_Insensitive) {
0240 if (grep ($file_full_path =~ , @Ignore_Files)) {
0241 undef $file_full_path;
0242 }
0243 }
0244 elsif (grep ($file_full_path =~ , @Ignore_Files)) {
0245 undef $file_full_path;
0246 }
0247 }
0248 next;
0249 }
0250
0251
0252 next if (! $file_full_path);
0253
0254
0255 if (/^symbolic names:$/) {
0256 $collecting_symbolic_names = 1;
0257 next;
0258 }
0259 if ($collecting_symbolic_names)
0260 {
0261
0262
0263 if (/^\S/) {
0264 $collecting_symbolic_names = 0;
0265 }
0266 else
0267 {
0268
0269
0270
0271
0272
0273 /^\s([^:]+): ([\d.]+)$/;
0274 my $tag_name = $1;
0275 my $tag_rev = $2;
0276
0277
0278
0279 if ($tag_rev =~ /(\d+\.(\d+\.)+)0.(\d+)/) {
0280 my $real_branch_rev = $1 . $3;
0281 $branch_names{$real_branch_rev} = $tag_name;
0282 if (@Follow_Branches) {
0283 if (grep ($_ eq $tag_name, @Follow_Branches)) {
0284 $branch_numbers{$tag_name} = $real_branch_rev;
0285 }
0286 }
0287 }
0288 else {
0289
0290 push (@{$symbolic_names{$tag_rev}}, $tag_name);
0291 }
0292 }
0293 }
0294
0295
0296
0297
0298
0299 if ((! (defined $revision)) and (/^revision (\d+\.[\d.]+)/))
0300 {
0301 $revision = $1;
0302
0303 if (@Follow_Branches)
0304 {
0305 foreach my $branch (@Follow_Branches)
0306 {
0307
0308 if (($branch =~ /^trunk$/i) and ($revision =~ /^[0-9]+\.[0-9]+$/))
0309 {
0310 goto dengo;
0311 }
0312
0313 my $branch_number = $branch_numbers{$branch};
0314 if ($branch_number)
0315 {
0316
0317
0318
0319
0320
0321
0322
0323
0324
0325
0326
0327
0328
0329
0330 if ((substr ($revision, 0, ((length ($branch_number)) + 1)))
0331 eq ($branch_number . "."))
0332 {
0333 goto dengo;
0334 }
0335
0336 elsif ((length ($branch_number)) > (length ($revision)))
0337 {
0338 $revision =~ /^([\d\.]+)(\d+)$/;
0339 my $r_left = $1;
0340 my $r_end = $2;
0341
0342 $branch_number =~ /^([\d\.]+)(\d+)\.\d+$/;
0343 my $b_left = $1;
0344 my $b_mid = $2;
0345
0346 if (($r_left eq $b_left)
0347 && ($r_end <= $b_mid))
0348 {
0349 goto dengo;
0350 }
0351 }
0352 }
0353 }
0354 }
0355 else
0356 {
0357 next;
0358 }
0359
0360
0361
0362 undef $revision;
0363 dengo:
0364 next;
0365 }
0366
0367
0368
0369 if (! (defined ($revision))) {
0370 $detected_file_separator = /^$file_separator$/o;
0371 if ($detected_file_separator) {
0372
0373 goto CLEAR;
0374 }
0375 else {
0376 next;
0377 }
0378 }
0379
0380
0381
0382 unless (defined $time) {
0383 if (/^date: .*/)
0384 {
0385 ($time, $author) = &parse_date_and_author ($_);
0386 if (defined ($usermap{$author}) and $usermap{$author}) {
0387 $author = $usermap{$author};
0388 }
0389 }
0390 else {
0391 $detected_file_separator = /^$file_separator$/o;
0392 if ($detected_file_separator) {
0393
0394 goto CLEAR;
0395 }
0396 }
0397
0398
0399 next;
0400 }
0401
0402
0403
0404
0405
0406
0407
0408
0409
0410
0411
0412
0413 if (/^branches:\s+(.*);$/)
0414 {
0415 if ($Show_Branches)
0416 {
0417 my $lst = $1;
0418 $lst =~ s/(1\.)+1;|(1\.)+1$//;
0419 if ($lst) {
0420 @branch_roots = split (/;\s+/, $lst);
0421 }
0422 else {
0423 undef @branch_roots;
0424 }
0425 next;
0426 }
0427 else
0428 {
0429
0430
0431
0432
0433
0434
0435
0436
0437
0438
0439
0440
0441
0442
0443
0444
0445
0446
0447
0448
0449
0450
0451 next;
0452 }
0453 }
0454
0455
0456
0457 $detected_file_separator = /^$file_separator$/o;
0458 if ($detected_file_separator && ! (defined $revision)) {
0459
0460 goto CLEAR;
0461 }
0462 unless ($detected_file_separator || /^$logmsg_separator$/o)
0463 {
0464 $msg_txt .= $_;
0465 next;
0466 }
0467
0468
0469 if ((! $msg_txt)
0470 || ($msg_txt =~ /^\s*\.\s*$|^\s*$/)
0471 || ($msg_txt =~ /\*\*\* empty log message \*\*\*/)) {
0472 if ($Prune_Empty_Msgs) {
0473 goto CLEAR;
0474 }
0475
0476 $msg_txt = "[no log message]\n";
0477 }
0478
0479
0480 {
0481 my $dir_key;
0482 my %qunk;
0483
0484
0485
0486
0487
0488
0489
0490
0491
0492
0493
0494
0495
0496
0497
0498
0499
0500
0501
0502
0503
0504
0505
0506
0507
0508
0509 if ($Distributed) {
0510
0511 ($qunk{'filename'}, $dir_key, undef) = fileparse ($file_full_path);
0512 }
0513 else {
0514 $dir_key = "./";
0515 $qunk{'filename'} = $file_full_path;
0516 }
0517
0518
0519
0520
0521
0522 $qunk{'time'} = $time;
0523
0524
0525
0526
0527
0528 $qunk{'revision'} = $revision;
0529
0530
0531 $qunk{'revision'} =~ /([\d.]+)\d+/;
0532 my $branch_prefix = $1;
0533 $branch_prefix =~ s/\.$//;
0534 if ($branch_names{$branch_prefix}) {
0535 $qunk{'branch'} = $branch_names{$branch_prefix};
0536 }
0537
0538
0539
0540
0541
0542 if (@branch_roots) {
0543 my @roots = map { $branch_names{$_} } @branch_roots;
0544 $qunk{'branchroots'} = \@roots;
0545 }
0546
0547
0548 if (defined ($symbolic_names{$revision})) {
0549 $qunk{'tags'} = $symbolic_names{$revision};
0550 delete $symbolic_names{$revision};
0551 }
0552
0553
0554
0555
0556
0557 &debug ("(pushing log msg for ${dir_key}$qunk{'filename'})\n");
0558
0559
0560
0561
0562 push (@{$grand_poobah{$dir_key}{$author}{$time}{$msg_txt}}, \%qunk);
0563 }
0564
0565 CLEAR:
0566
0567 undef $msg_txt;
0568 undef $time;
0569 undef $revision;
0570 undef $author;
0571 undef @branch_roots;
0572
0573
0574 if ($detected_file_separator) {
0575 undef $file_full_path;
0576 undef %branch_names;
0577 }
0578 }
0579
0580 close (LOG_SOURCE);
0581
0582
0583
0584 while (my ($dir,$authorhash) = each %grand_poobah)
0585 {
0586 &debug ("DOING DIR: $dir\n");
0587
0588
0589
0590
0591
0592
0593
0594
0595
0596
0597
0598
0599
0600
0601
0602 my %changelog;
0603 while (my ($author,$timehash) = each %$authorhash)
0604 {
0605 my $lasttime;
0606 my %stamptime;
0607 foreach my $time (sort {$main::a <=> $main::b} (keys %$timehash))
0608 {
0609 my $msghash = $timehash->{$time};
0610 while (my ($msg,$qunklist) = each %$msghash)
0611 {
0612 my $stamptime = $stamptime{$msg};
0613 if ((defined $stamptime)
0614 and (($time - $stamptime) < $Max_Checkin_Duration)
0615 and (defined $changelog{$stamptime}{$author}{$msg}))
0616 {
0617 push(@{$changelog{$stamptime}{$author}{$msg}}, @$qunklist);
0618 }
0619 else {
0620 $changelog{$time}{$author}{$msg} = $qunklist;
0621 $stamptime{$msg} = $time;
0622 }
0623 }
0624 }
0625 }
0626 undef (%$authorhash);
0627
0628
0629
0630 my ($logfile_here, $logfile_bak, $tmpfile);
0631
0632 if (! $Output_To_Stdout) {
0633 $logfile_here = $dir . $Log_File_Name;
0634 $logfile_here =~ s/^\.\/\//\//;
0635 $tmpfile = "${logfile_here}.cvs2cl$$.tmp";
0636 $logfile_bak = "${logfile_here}.bak";
0637
0638 open (LOG_OUT, ">$tmpfile") or die "Unable to open \"$tmpfile\"";
0639 }
0640 else {
0641 open (LOG_OUT, ">-") or die "Unable to open stdout for writing";
0642 }
0643
0644 print LOG_OUT $ChangeLog_Header;
0645
0646 if ($XML_Output) {
0647 print LOG_OUT "<?xml version=\"1.0\"?>\n\n<changelog>\n\n";
0648 }
0649
0650 foreach my $time (sort {$main::b <=> $main::a} (keys %changelog))
0651 {
0652 my $authorhash = $changelog{$time};
0653 while (my ($author,$mesghash) = each %$authorhash)
0654 {
0655
0656 if ($XML_Output) {
0657 $author = &xml_escape ($author);
0658 }
0659
0660 while (my ($msg,$qunklist) = each %$mesghash)
0661 {
0662 my $files = &pretty_file_list ($qunklist);
0663 my $logtext = &pretty_msg_text ($msg);
0664 my $header_line;
0665 my $body;
0666 my $wholething;
0667
0668
0669
0670
0671 my ($ignore,$min,$hour,$mday,$mon,$year,$wday)
0672 = $UTC_Times ? gmtime($time) : localtime($time);
0673
0674
0675
0676 if ($Show_Day_Of_Week or $XML_Output) {
0677 $wday = ("Sunday", "Monday", "Tuesday", "Wednesday",
0678 "Thursday", "Friday", "Saturday")[$wday];
0679 $wday = ($XML_Output) ? "<weekday>${wday}</weekday>\n" : " $wday";
0680 }
0681 else {
0682 $wday = "";
0683 }
0684
0685 if ($XML_Output) {
0686 $header_line =
0687 sprintf ("<date>%4u-%02u-%02u</date>\n"
0688 . "${wday}"
0689 . "<time>%02u:%02u</time>\n"
0690 . "<author>%s</author>\n",
0691 $year+1900, $mon+1, $mday, $hour, $min, $author);
0692 }
0693 else {
0694 $header_line =
0695 sprintf ("%4u-%02u-%02u${wday} %02u:%02u %s\n\n",
0696 $year+1900, $mon+1, $mday, $hour, $min, $author);
0697 }
0698
0699
0700 if ($XML_Output) {
0701 $body = $files . $logtext;
0702 }
0703 elsif ($No_Wrap) {
0704 $files = wrap ("\t", " ", "$files");
0705 $logtext =~ s/\n(.*)/\n\t$1/g;
0706 unless ($After_Header eq " ") {
0707 $logtext =~ s/^(.*)/\t$1/g;
0708 }
0709 $body = $files . $After_Header . $logtext;
0710 }
0711 else {
0712 $body = $files . $After_Header . $logtext;
0713 $body = wrap ("\t", " ", "$body");
0714 }
0715
0716 $wholething = $header_line . $body;
0717
0718 if ($XML_Output) {
0719 $wholething = "<entry>\n${wholething}</entry>\n";
0720 }
0721
0722
0723
0724
0725
0726
0727
0728
0729
0730 if ($Case_Insensitive) {
0731 unless ($Regexp_Gate && ($wholething !~ /$Regexp_Gate/oi)) {
0732 print LOG_OUT "${wholething}\n";
0733 }
0734 }
0735 else {
0736 unless ($Regexp_Gate && ($wholething !~ /$Regexp_Gate/o)) {
0737 print LOG_OUT "${wholething}\n";
0738 }
0739 }
0740 }
0741 }
0742 }
0743
0744 if ($XML_Output) {
0745 print LOG_OUT "</changelog>\n";
0746 }
0747
0748 close (LOG_OUT);
0749
0750 if (! $Output_To_Stdout)
0751 {
0752 if (-f $logfile_here) {
0753 rename ($logfile_here, $logfile_bak);
0754 }
0755 rename ($tmpfile, $logfile_here);
0756 }
0757 }
0758 }
0759
0760
0761 sub parse_date_and_author ()
0762 {
0763
0764
0765
0766
0767 my $line = shift;
0768
0769 my ($year, $mon, $mday, $hours, $min, $secs, $author) = $line =~
0770
0771 or die "Couldn't parse date ``$line''";
0772 die "Bad date or Y2K issues" unless ($year > 1969 and $year < 2258);
0773
0774 my $time = timegm($secs,$min,$hours,$mday,$mon-1,$year-1900);
0775
0776 return ($time, $author);
0777 }
0778
0779
0780
0781
0782 sub pretty_file_list ()
0783 {
0784 my $qunksref = shift;
0785 my @qunkrefs = @$qunksref;
0786 my @filenames;
0787 my $beauty = "";
0788 my %non_unanimous_tags;
0789 my %unanimous_tags;
0790 my %all_branches;
0791 my $common_dir;
0792 my $fbegun = 0;
0793
0794
0795
0796
0797 foreach my $qunkref (@qunkrefs)
0798 {
0799
0800
0801
0802 if ((scalar (@qunkrefs)) > 1)
0803 {
0804 if (! (defined ($common_dir))) {
0805 my ($base, $dir);
0806 ($base, $dir, undef) = fileparse ($$qunkref{'filename'});
0807
0808 if (($dir eq "./") || ($dir eq ".\\")) {
0809 $common_dir = "";
0810 }
0811 else {
0812 $common_dir = $dir;
0813 }
0814
0815 ($dir eq "./") ? ($common_dir = "") : ($common_dir = $dir);
0816 }
0817 elsif ($common_dir) {
0818 $common_dir = &common_path_prefix ($$qunkref{'filename'}, $common_dir);
0819 }
0820 }
0821 else
0822 {
0823 $common_dir = "";
0824 }
0825
0826 if (defined ($$qunkref{'branch'})) {
0827 $all_branches{$$qunkref{'branch'}} = 1;
0828 }
0829 if (defined ($$qunkref{'tags'})) {
0830 foreach my $tag (@{$$qunkref{'tags'}}) {
0831 $non_unanimous_tags{$tag} = 1;
0832 }
0833 }
0834 }
0835
0836
0837
0838 if ((scalar (@qunkrefs)) > 1) {
0839 foreach my $tag (keys (%non_unanimous_tags)) {
0840 my $everyone_has_this_tag = 1;
0841 foreach my $qunkref (@qunkrefs) {
0842 if ((! (defined ($$qunkref{'tags'})))
0843 or (! (grep ($_ eq $tag, @{$$qunkref{'tags'}})))) {
0844 $everyone_has_this_tag = 0;
0845 }
0846 }
0847 if ($everyone_has_this_tag) {
0848 $unanimous_tags{$tag} = 1;
0849 delete $non_unanimous_tags{$tag};
0850 }
0851 }
0852 }
0853
0854 if ($XML_Output)
0855 {
0856
0857
0858
0859
0860
0861 foreach my $qunkref (@qunkrefs)
0862 {
0863 my $filename = $$qunkref{'filename'};
0864 my $revision = $$qunkref{'revision'};
0865 my $tags = $$qunkref{'tags'};
0866 my $branch = $$qunkref{'branch'};
0867 my $branchroots = $$qunkref{'branchroots'};
0868
0869 $filename = &xml_escape ($filename);
0870 $revision = &xml_escape ($revision);
0871
0872 $beauty .= "<file>\n";
0873 $beauty .= "<name>${filename}</name>\n";
0874 $beauty .= "<revision>${revision}</revision>\n";
0875 if ($branch) {
0876 $branch = &xml_escape ($branch);
0877 $beauty .= "<branch>${branch}</branch>\n";
0878 }
0879 foreach my $tag (@$tags) {
0880 $tag = &xml_escape ($tag);
0881 $beauty .= "<tag>${tag}</tag>\n";
0882 }
0883 foreach my $root (@$branchroots) {
0884 $root = &xml_escape ($root);
0885 $beauty .= "<branchroot>${root}</branchroot>\n";
0886 }
0887 $beauty .= "</file>\n";
0888 }
0889
0890
0891
0892
0893
0894
0895 if ((scalar (keys (%unanimous_tags))) > 1) {
0896 foreach my $utag ((keys (%unanimous_tags))) {
0897 $utag = &xml_escape ($utag);
0898 $beauty .= "<utag>${utag}</utag>\n";
0899 }
0900 }
0901 if ($common_dir) {
0902 $common_dir = &xml_escape ($common_dir);
0903 $beauty .= "<commondir>${common_dir}</commondir>\n";
0904 }
0905
0906
0907 return $beauty;
0908 }
0909
0910
0911
0912
0913
0914 if ($common_dir) {
0915
0916 $beauty .= "$common_dir: ";
0917 }
0918
0919 if ($Show_Branches)
0920 {
0921
0922 my @brevisions;
0923
0924 foreach my $branch (keys (%all_branches))
0925 {
0926 foreach my $qunkref (@qunkrefs)
0927 {
0928 if ((defined ($$qunkref{'branch'}))
0929 and ($$qunkref{'branch'} eq $branch))
0930 {
0931 if ($fbegun) {
0932
0933 $beauty .= ", ";
0934 }
0935 else {
0936 $fbegun = 1;
0937 }
0938 my $fname = substr ($$qunkref{'filename'}, length ($common_dir));
0939 $beauty .= $fname;
0940 $$qunkref{'printed'} = 1;
0941
0942 if ($Show_Tags && (defined @{$$qunkref{'tags'}})) {
0943 my @tags = grep ($non_unanimous_tags{$_}, @{$$qunkref{'tags'}});
0944 if (@tags) {
0945 $beauty .= " (tags: ";
0946 $beauty .= join (', ', @tags);
0947 $beauty .= ")";
0948 }
0949 }
0950
0951 if ($Show_Revisions) {
0952
0953
0954
0955 $$qunkref{'revision'} =~ /.+\.([\d])+$/;
0956 push (@brevisions, $1);
0957
0958
0959
0960
0961
0962 }
0963 }
0964 }
0965 $beauty .= " ($branch";
0966 if (@brevisions) {
0967 if ((scalar (@brevisions)) > 1) {
0968 $beauty .= ".[";
0969 $beauty .= (join (',', @brevisions));
0970 $beauty .= "]";
0971 }
0972 else {
0973 $beauty .= ".$brevisions[0]";
0974 }
0975 }
0976 $beauty .= ")";
0977 }
0978 }
0979
0980
0981
0982
0983 foreach my $qunkref (@qunkrefs)
0984 {
0985 next if (defined ($$qunkref{'printed'}));
0986
0987 if ($fbegun) {
0988 $beauty .= ", ";
0989 }
0990 else {
0991 $fbegun = 1;
0992 }
0993 $beauty .= substr ($$qunkref{'filename'}, length ($common_dir));
0994 $$qunkref{'printed'} = 1;
0995
0996 if ($Show_Revisions || $Show_Tags)
0997 {
0998 my $started_addendum = 0;
0999
1000 if ($Show_Revisions) {
1001 $started_addendum = 1;
1002 $beauty .= " (";
1003 $beauty .= "$$qunkref{'revision'}";
1004 }
1005 if ($Show_Tags && (defined $$qunkref{'tags'})) {
1006 my @tags = grep ($non_unanimous_tags{$_}, @{$$qunkref{'tags'}});
1007 if ((scalar (@tags)) > 0) {
1008 if ($started_addendum) {
1009 $beauty .= ", ";
1010 }
1011 else {
1012 $beauty .= " (tags: ";
1013 }
1014 $beauty .= join (', ', @tags);
1015 $started_addendum = 1;
1016 }
1017 }
1018 if ($started_addendum) {
1019 $beauty .= ")";
1020 }
1021 }
1022 }
1023
1024
1025 if ($Show_Tags && %unanimous_tags)
1026 {
1027 $beauty .= " (utags: ";
1028 $beauty .= join (', ', keys (%unanimous_tags));
1029 $beauty .= ")";
1030 }
1031
1032
1033
1034 $beauty = "* $beauty:";
1035
1036 return $beauty;
1037 }
1038
1039
1040 sub common_path_prefix ()
1041 {
1042 my $path1 = shift;
1043 my $path2 = shift;
1044
1045 my ($dir1, $dir2);
1046 (undef, $dir1, undef) = fileparse ($path1);
1047 (undef, $dir2, undef) = fileparse ($path2);
1048
1049
1050
1051
1052
1053 $dir1 =~ tr#\\#/#;
1054 $dir2 =~ tr#\\#/#;
1055
1056 my $accum1 = "";
1057 my $accum2 = "";
1058 my $last_common_prefix = "";
1059
1060 while ($accum1 eq $accum2)
1061 {
1062 $last_common_prefix = $accum1;
1063 last if ($accum1 eq $dir1);
1064 my ($tmp1) = split (/\//, (substr ($dir1, length ($accum1))));
1065 my ($tmp2) = split (/\//, (substr ($dir2, length ($accum2))));
1066 $accum1 .= "$tmp1/" if ((defined ($tmp1)) and $tmp1);
1067 $accum2 .= "$tmp2/" if ((defined ($tmp2)) and $tmp2);
1068 }
1069
1070 return $last_common_prefix;
1071 }
1072
1073
1074 sub pretty_msg_text ()
1075 {
1076 my $text = shift;
1077
1078
1079 $text =~ s/\r\n/\n/g;
1080
1081
1082 $text =~ s/\n\s*\n/\n\n/g;
1083
1084 if ($XML_Output)
1085 {
1086 $text = &xml_escape ($text);
1087 $text = "<msg>${text}</msg>\n";
1088 }
1089 elsif (! $No_Wrap)
1090 {
1091
1092
1093
1094
1095 1 while ($text =~ s/(^|\n)([^>\s].*[^.\n])\n([^>\n])/$1$2 $3/g);
1096
1097
1098
1099 1 while ($text =~ s/(^|\n)([^>\s].*)\n([^>\n])/$1$2 $3/g);
1100 }
1101
1102 return $text;
1103 }
1104
1105
1106 sub xml_escape ()
1107 {
1108 my $txt = shift;
1109 $txt =~ s/&/&/g;
1110 $txt =~ s/</</g;
1111 $txt =~ s/>/>/g;
1112 return $txt;
1113 }
1114
1115
1116 sub maybe_read_user_map_file ()
1117 {
1118 my %expansions;
1119
1120 if ($User_Map_File)
1121 {
1122 open (MAPFILE, "<$User_Map_File")
1123 or die ("Unable to open $User_Map_File ($!)");
1124
1125 while (<MAPFILE>)
1126 {
1127 my ($username, $expansion) = split ':';
1128 chomp $expansion;
1129 $expansion =~ s/^'(.*)'$/$1/;
1130 $expansion =~ s/^"(.*)"$/$1/;
1131
1132
1133
1134
1135
1136 if ($expansion =~ /^\s*<{0,1}\S+@.*/) {
1137
1138 if (! ($expansion =~ /<\S+@\S+>/)) {
1139 $expansions{$username} = "$username <$expansion>";
1140 }
1141 else {
1142 $expansions{$username} = "$username $expansion";
1143 }
1144 }
1145 else {
1146 $expansions{$username} = $expansion;
1147 }
1148 }
1149
1150 close (MAPFILE);
1151 }
1152
1153 return %expansions;
1154 }
1155
1156
1157 sub parse_options ()
1158 {
1159
1160 my $output_file;
1161
1162
1163
1164 my $exit_with_admonishment = 0;
1165
1166 while (my $arg = shift (@ARGV))
1167 {
1168 if ($arg =~ /^-h$|^-help$|^--help$|^--usage$|^-?$/) {
1169 $Print_Usage = 1;
1170 }
1171 elsif ($arg =~ /^--debug$/) {
1172 $Debug = 1;
1173 }
1174 elsif ($arg =~ /^--version$/) {
1175 $Print_Version = 1;
1176 }
1177 elsif ($arg =~ /^-g$|^--global-opts$/) {
1178 my $narg = shift (@ARGV) || die "$arg needs argument.\n";
1179
1180 $Log_Source_Command =~ s/(^\S*)/$1 $narg/;
1181 }
1182 elsif ($arg =~ /^-l$|^--log-opts$/) {
1183 my $narg = shift (@ARGV) || die "$arg needs argument.\n";
1184 $Log_Source_Command .= " $narg";
1185 }
1186 elsif ($arg =~ /^-f$|^--file$/) {
1187 my $narg = shift (@ARGV) || die "$arg needs argument.\n";
1188 $output_file = $narg;
1189 }
1190 elsif ($arg =~ /^-U$|^--usermap$/) {
1191 my $narg = shift (@ARGV) || die "$arg needs argument.\n";
1192 $User_Map_File = $narg;
1193 }
1194 elsif ($arg =~ /^-W$|^--window$/) {
1195 my $narg = shift (@ARGV) || die "$arg needs argument.\n";
1196 $Max_Checkin_Duration = $narg;
1197 }
1198 elsif ($arg =~ /^-I$|^--ignore$/) {
1199 my $narg = shift (@ARGV) || die "$arg needs argument.\n";
1200 push (@Ignore_Files, $narg);
1201 }
1202 elsif ($arg =~ /^-C$|^--case-insensitive$/) {
1203 $Case_Insensitive = 1;
1204 }
1205 elsif ($arg =~ /^-R$|^--regexp$/) {
1206 my $narg = shift (@ARGV) || die "$arg needs argument.\n";
1207 $Regexp_Gate = $narg;
1208 }
1209 elsif ($arg =~ /^--stdout$/) {
1210 $Output_To_Stdout = 1;
1211 }
1212 elsif ($arg =~ /^--version$/) {
1213 $Print_Version = 1;
1214 }
1215 elsif ($arg =~ /^-d$|^--distributed$/) {
1216 $Distributed = 1;
1217 }
1218 elsif ($arg =~ /^-P$|^--prune$/) {
1219 $Prune_Empty_Msgs = 1;
1220 }
1221 elsif ($arg =~ /^-S$|^--separate-header$/) {
1222
1223 $After_Header = "\n";
1224 }
1225 elsif ($arg =~ /^--no-wrap$/) {
1226 $No_Wrap = 1;
1227 }
1228 elsif ($arg =~ /^--gmt$|^--utc$/) {
1229 $UTC_Times = 1;
1230 }
1231 elsif ($arg =~ /^-w$|^--day-of-week$/) {
1232 $Show_Day_Of_Week = 1;
1233 }
1234 elsif ($arg =~ /^-r$|^--revisions$/) {
1235 $Show_Revisions = 1;
1236 }
1237 elsif ($arg =~ /^-t$|^--tags$/) {
1238 $Show_Tags = 1;
1239 }
1240 elsif ($arg =~ /^-b$|^--branches$/) {
1241 $Show_Branches = 1;
1242 }
1243 elsif ($arg =~ /^-F$|^--follow$/) {
1244 my $narg = shift (@ARGV) || die "$arg needs argument.\n";
1245 push (@Follow_Branches, $narg);
1246 }
1247 elsif ($arg =~ /^--stdin$/) {
1248 $Input_From_Stdin = 1;
1249 }
1250 elsif ($arg =~ /^--header$/) {
1251 my $narg = shift (@ARGV) || die "$arg needs argument.\n";
1252 $ChangeLog_Header = &slurp_file ($narg);
1253 if (! defined ($ChangeLog_Header)) {
1254 $ChangeLog_Header = "";
1255 }
1256 }
1257 elsif ($arg =~ /^--xml$/) {
1258 $XML_Output = 1;
1259 }
1260 else {
1261
1262 $Log_Source_Command .= " $arg";
1263 }
1264 }
1265
1266
1267
1268 if ($Output_To_Stdout && $Distributed) {
1269 print STDERR "cannot pass both --stdout and --distributed\n";
1270 $exit_with_admonishment = 1;
1271 }
1272
1273 if ($Output_To_Stdout && $output_file) {
1274 print STDERR "cannot pass both --stdout and --file\n";
1275 $exit_with_admonishment = 1;
1276 }
1277
1278
1279
1280 if ($exit_with_admonishment) {
1281 &usage ();
1282 exit (1);
1283 }
1284 elsif ($Print_Usage) {
1285 &usage ();
1286 exit (0);
1287 }
1288 elsif ($Print_Version) {
1289 &version ();
1290 exit (0);
1291 }
1292
1293
1294
1295 if ($Output_To_Stdout) {
1296 undef $Log_File_Name;
1297 }
1298 elsif ($output_file) {
1299 $Log_File_Name = $output_file;
1300 }
1301 }
1302
1303
1304 sub slurp_file ()
1305 {
1306 my $filename = shift || die ("no filename passed to slurp_file()");
1307 my $retstr;
1308
1309 open (SLURPEE, "<${filename}") or die ("unable to open $filename ($!)");
1310 my $saved_sep = $/;
1311 undef $/;
1312 $retstr = <SLURPEE>;
1313 $/ = $saved_sep;
1314 close (SLURPEE);
1315 return $retstr;
1316 }
1317
1318
1319 sub debug ()
1320 {
1321 if ($Debug) {
1322 my $msg = shift;
1323 print STDERR $msg;
1324 }
1325 }
1326
1327
1328 sub version ()
1329 {
1330 print "cvs2cl.pl version ${VERSION}; distributed under the GNU GPL.\n";
1331 }
1332
1333
1334 sub usage ()
1335 {
1336 &version ();
1337 print <<'END_OF_INFO';
1338 Generate GNU-style ChangeLogs in CVS working copies.
1339
1340 Notes about the output format(s):
1341
1342 The default output of cvs2cl.pl is designed to be compact, formally
1343 unambiguous, but still easy for humans to read. It is largely
1344 self-explanatory, I hope; the one abbreviation that might not be
1345 obvious is "utags". That stands for "universal tags" -- a
1346 universal tag is one held by all the files in a given change entry.
1347
1348 If you need output that's easy for a program to parse, use the
1349 --xml option. Note that with XML output, just about all available
1350 information is included with each change entry, whether you asked
1351 for it or not, on the theory that your parser can ignore anything
1352 it's not looking for.
1353
1354 Notes about the options and arguments (the actual options are listed
1355 last in this usage message):
1356
1357 * The -I and -F options may appear multiple times.
1358
1359 * To follow trunk revisions, use "1360 This is okay because no would ever, ever be crazy enough to name a
1361 branch "trunk", right? Right.
1362
1363 * For the -U option, the UFILE should be formatted like
1364 CVSROOT">-F trunk" ("-F TRUNK" also works).
1365 This is okay because no would ever, ever be crazy enough to name a
1366 branch "trunk", right? Right.
1367
1368 * For the -U option, the UFILE should be formatted like
1369 CVSROOT/-F trunk" ("-F TRUNK" also works).
1370 This is okay because no would ever, ever be crazy enough to name a
1371 branch "trunk", right? Right.
1372
1373 * For the -U option, the UFILE should be formatted like
1374 CVSROOT/users. That is, each line of UFILE looks like this
1375 jrandom:jrandom@red-bean.com
1376 or maybe even like this
1377 jrandom:'Jesse Q. Random <jrandom@red-bean.com>'
1378 Don't forget to quote the portion after the colon if necessary.
1379
1380 * Many people want to filter by date. To do so, invoke cvs2cl.pl
1381 like this:
1382 cvs2cl.pl -l "-d'DATESPEC'"
1383 where DATESPEC is any date specification valid for "cvs log -d".
1384 (Note that CVS 1.10.7 and below requires there be no space between
1385 -d and its argument).
1386
1387 Options/-F trunk" ("-F TRUNK" also works).
1388 This is okay because no would ever, ever be crazy enough to name a
1389 branch "trunk", right? Right.
1390
1391 * For the -U option, the UFILE should be formatted like
1392 CVSROOT/users. That is, each line of UFILE looks like this
1393 jrandom:jrandom@red-bean.com
1394 or maybe even like this
1395 jrandom:'Jesse Q. Random <jrandom@red-bean.com>Arguments:
1396
1397 -h, -help, --help, or -? Show this usage and exit
1398 --version Show version and exit
1399 -r, --revisions Show revision numbers in output
1400 -b, --branches Show branch names in revisions when possible
1401 -t, --tags Show tags (symbolic names) in output
1402 --stdin Read from stdin, don't run cvs log
1403 --stdout Output to stdout not to ChangeLog
1404 -d, --distributed Put ChangeLogs in subdirs
1405 -f FILE, --file FILE Write to FILE instead of "ChangeLog"
1406 -W SECS, --window SECS Window of time within which log entries unify
1407 -U UFILE, --usermap UFILE Expand usernames to email addresses from UFILE
1408 -R REGEXP, --regexp REGEXP Include only entries that match REGEXP
1409 -I REGEXP, --ignore REGEXP Ignore files whose names match REGEXP
1410 -C, --case-insensitive Any regexp matching is done case-insensitively
1411 -F BRANCH, --follow BRANCH Show only revisions on or ancestral to BRANCH
1412 -S, --separate-header Blank line between each header and log message
1413 --no-wrap Don't auto-wrap log message (recommend -S also)
1414 --gmt, --utc Show times in GMT/-F trunk" ("-F TRUNK" also works).
1415 This is okay because no would ever, ever be crazy enough to name a
1416 branch "trunk", right? Right.
1417
1418 * For the -U option, the UFILE should be formatted like
1419 CVSROOT/users. That is, each line of UFILE looks like this
1420 jrandom:jrandom@red-bean.com
1421 or maybe even like this
1422 jrandom:'Jesse Q. Random <jrandom@red-bean.com>UTC instead of local time
1423 -w, --day-of-week Show day of week
1424 --header FILE Get ChangeLog header from FILE ("-" means stdin)
1425 --xml Output XML instead of ChangeLog format
1426 -P, --prune Don't show empty log messages
1427 -g OPTS, --global-opts OPTS Invoke like this "cvs OPTS log ..."
1428 -l OPTS, --log-opts OPTS Invoke like this "cvs ... log OPTS"
1429 FILE1 [FILE2 ...] Show only log information for the named FILE(s)
1430
1431 See http://-F trunk" ("-F TRUNK" also works).
1432 This is okay because no would ever, ever be crazy enough to name a
1433 branch "trunk", right? Right.
1434
1435 * For the -U option, the UFILE should be formatted like
1436 CVSROOT/users. That is, each line of UFILE looks like this
1437 jrandom:jrandom@red-bean.com
1438 or maybe even like this
1439 jrandom:'Jesse Q. Random <jrandom@red-bean.com>UTC instead of local time
1440 -w, --day-of-week Show day of week
1441 --header FILE Get ChangeLog header from FILE ("-" means stdin)
1442 --xml Output XML instead of ChangeLog format
1443 -P, --prune Don't show empty log messages
1444 -g OPTS, --global-opts OPTS Invoke like this "cvs OPTS log ..."
1445 -l OPTS, --log-opts OPTS Invoke like this "cvs ... log OPTS"
1446 FILE1 [FILE2 ...] Show only log information for the named FILE(s)
1447
1448 See http://www.red-bean.com/-F trunk" ("-F TRUNK" also works).
1449 This is okay because no would ever, ever be crazy enough to name a
1450 branch "trunk", right? Right.
1451
1452 * For the -U option, the UFILE should be formatted like
1453 CVSROOT/users. That is, each line of UFILE looks like this
1454 jrandom:jrandom@red-bean.com
1455 or maybe even like this
1456 jrandom:'Jesse Q. Random <jrandom@red-bean.com>www.red-bean.com/~kfogel/1457 END_OF_INFO
1458 }
1459
1460 __END__
1461
1462 =head1 NAME
1463
1464 cvs2cl.pl - produces GNU-style ChangeLogs in CVS working copies, by
1465 running "cvs log" and parsing the output. Shared log entries are
1466 unified in an intuitive way.
1467
1468 =head1 DESCRIPTION
1469
1470 This script generates GNU-style ChangeLog files from CVS log
1471 information. Basic usage: just run it inside a working copy and a
1472 ChangeLog will appear. It requires repository access (i.e., 'cvs log'
1473 must work). Run "cvs2cl.pl --help" to see more advanced options.
1474
1475 See http:">cvs2cl.shtml for maintenance and bug info.
1476 END_OF_INFO
1477 }
1478
1479 __END__
1480
1481 =head1 NAME
1482
1483 cvs2cl.pl - produces GNU-style ChangeLogs in CVS working copies, by
1484 running "cvs log" and parsing the output. Shared log entries are
1485 unified in an intuitive way.
1486
1487 =head1 DESCRIPTION
1488
1489 This script generates GNU-style ChangeLog files from CVS log
1490 information. Basic usage: just run it inside a working copy and a
1491 ChangeLog will appear. It requires repository access (i.e., 'cvs log'
1492 must work). Run "cvs2cl.pl --help" to see more advanced options.
1493
1494 See http://www.red-bean.com/~kfogel/cvs2cl.shtml for updates, and
1495 for instructions on getting anonymous CVS access to this script.
1496
1497 Maintainer: Karl Fogel <kfogel@red-bean.com>
1498 Please report bugs to <cvs2cl-bugs@red-bean.com>.
1499
1500 =head1 README
1501
1502 This script generates GNU-style ChangeLog files from CVS log
1503 information. Basic usage: just run it inside a working copy and a
1504 ChangeLog will appear. It requires repository access (i.e., 'cvs log'
1505 must work). Run "cvs2cl.pl --help" to see more advanced options.
1506
1507 See http://www.red-bean.com/~kfogel/cvs2cl.shtml for updates, and
1508 for instructions on getting anonymous CVS access to this script.
1509
1510 Maintainer: Karl Fogel <kfogel@red-bean.com