#!/usr/bin/perl -w #======================================================================= # $Id$ # Edit cvs log messages based on output from “cvs log” # # Character set used in this file: UTF-8 # Made by Øyvind A. Holm # License: GNU General Public License. See end of file for legal stuff. #======================================================================= use strict; use Getopt::Std; our ($opt_d, $opt_h, $opt_i, $opt_s, $opt_v) = ( "", 0, 0, 0, 0); getopts('d:hisv') || die("Option error. Use -h for help.\n"); $| = 1; # When changing the version number, also update the POD. our $VERSION = "0.5"; our $rcs_id = '$Id$'; our $id_date = $rcs_id; $id_date =~ s/^.*?\d+ (\d\d\d\d-.*?\d\d:\d\d:\d\d\S+).*/$1/; if ($opt_h) { print_help(0); } my $Simulate = $opt_s; my $has_diff = 0; ($has_diff = 1) if (`diff --version` =~ /diff/); my ($curr_rev, $curr_rcs_file, $curr_work_file, $total_ignored) = ( "", "", "", ""); my @Curr = (); my %Entry = (); my ($Rev, $Line) = ( "", ""); my ($header_done, $subheader_done, $tmp_count) = ( 0, 0, 0); my %Text = (); my %missing_file = (); my ($start_utc, $total_skipped, $total_changed, $total_files) = ( time, 0, 0, 0); my @all_revs = (); my $eroot_str = ""; defined($ENV{CVSE_ROOT}) && ($eroot_str = " -d $ENV{CVSE_ROOT}"); length($opt_d) && ($eroot_str = " -d $opt_d"); while (<>) { $Line = $_; if ($Line =~ /^-{28}$/) { # New revision {{{ $header_done = 1; if (length($Rev) && scalar(@Curr)) { $Entry{$Rev} = join("", @Curr); } @Curr = (); $Line = <>; if ($Line =~ /^revision ([\d\.]+)/) { $curr_rev = $1; } else { die("Line $.: Expected \"revision \", " . "got \"$Line\".\". Aborting."); } $Line = <>; unless ($Line =~ /^date: \S+\s+\S+ .*/) { warn("Expected \"date: \", got \"$Line\".\"."); } $Line = <>; unless ($Line =~ /^branches: .*;$/) { push(@Curr, $Line); } $Rev = "$curr_work_file,v.$curr_rev"; # }}} } elsif ($Line =~ /^={77}$/) { # List finished for this file, change the modified messages {{{ $header_done = 0; $total_files++; if (length($Rev) && scalar(@Curr)) { $Entry{$Rev} = join("", @Curr); } @all_revs = (); while (my ($l_name, $l_val) = each %Entry) { push(@all_revs, $l_name); } for (@all_revs) { # Scan through all revisions {{{ my $Curr = $_; my ($a_file, $a_rev) = ( "", ""); if ($Curr =~ /^(.+),v\.([\d\.]+?)$/) { ($a_file, $a_rev) = ( $1, $2); if (length($a_file) && length($a_rev)) { change_message($a_file, $a_rev, $Entry{$Curr}); } } else { warn("Wrong revision format \"$Curr\", " . "skipping revision\n"); } # }}} } ($curr_rev, $Rev) = ( "", ""); %Entry = (); @Curr = (); # }}} } elsif (!$header_done && $Line =~ /^RCS file: (.*)/) { $curr_rcs_file = $1; } elsif (!$header_done && $Line =~ /^Working file: (.*)/) { $curr_work_file = $1; } else { # Regular log message {{{ if (length($Rev)) { push(@Curr, $_); } # }}} } } my $Seconds = time-$start_utc; printf("\n%u file%s processed%s. %u revision%s changed, " . "%u revision%s skipped. %u second%s used.\n", $total_files, $total_files == 1 ? "" : "s", $opt_i ? (", $total_ignored ignored") : "", $total_changed, $total_changed == 1 ? "" : "s", $total_skipped, $total_skipped == 1 ? "" : "s", $Seconds, $Seconds == 1 ? "" : "s"); exit 0; sub change_message { # Changes a log message for a specific revision of a file if it has # changed. # {{{ my ($File, $Rev, $Txt) = @_; my $esc_file = escape_filename($File); if ($opt_i && !-e $File) { unless (defined($missing_file{$File})) { print("Ignoring non-existing file $esc_file\n"); $missing_file{$File} = 1; $total_files--; $total_ignored++; } return; } my $tmp_file = "cvse.$$.$tmp_count.tmp"; $tmp_count++; my $compare_text = get_log_message($esc_file, $Rev); if ($Txt ne $compare_text) { print("\nChanging message for $esc_file rev. $Rev ...\n"); if (!defined($missing_file{$File}) && !(-e $File)) { # File does not exist in this revision, change revision to # make it appear and make it possible for CVS to update the # message # {{{ print("$esc_file not found, running cvs update with random " . "revisions to try to make it appear...\n"); for my $Curr (@all_revs) { if ($Curr =~ /^(.+),v\.([\d\.]+?)$/) { my $t_rev = $2; my $ex_str = "cvs$eroot_str upd -r $t_rev $esc_file"; print("Executing \"$ex_str\"\n"); system($ex_str); if (-e $File) { print("File exists with (old) revision $t_rev, " . "CVS is now able to change the log message.\n"); last; } } } if (!-e $File && !defined($missing_file{$File})) { warn("$esc_file: File does still not exist, messages for " . "this file will not be changed\n"); $missing_file{$File} = 1; } # }}} } my @Arr = split(/\n/, $Txt); if (open(TxtFP, ">$tmp_file")) { for (@Arr) { my $Line = $_; if (/^date: .*/ || /^branches: .*/) { # NOP } else { print(TxtFP "$Line\n"); } } close(TxtFP) || die("$tmp_file: Error closing file: $!"); my $exec_str = "cvs$eroot_str admin " . "-m$Rev:\"`cat $tmp_file`\" $esc_file"; my $Deb = ""; $Deb = get_log_message($esc_file, $Rev); if ($has_diff) { if (open(DiffFP, ">BEFORE.cvse")) { print(DiffFP $Deb); close(DiffFP); } } else { print("==== BEFORE: $esc_file $Rev \x7B\x7B\x7B ====\n" . "$Deb==== \x7D\x7D\x7D ====\n"); } printf("%s \"%s\"\n", $Simulate ? "Simulating" : "Executing", $exec_str); system($exec_str) unless $Simulate; $Deb = get_log_message($esc_file, $Rev); unlink($tmp_file) || warn("$tmp_file: Cannot remove file: $!"); if ($has_diff) { if (open(DiffFP, ">AFTER.cvse")) { print(DiffFP $Deb); close(DiffFP); } print( join("", "==== Log diff for $File,v $Rev \x7B\x7B\x7B ====\n", `diff -u BEFORE.cvse AFTER.cvse`, "==== \x7D\x7D\x7D ====\n" ) ); for ("BEFORE.cvse", "AFTER.cvse") { unlink($_) || warn("$_: Cannot remove file: $!"); } } else { print("==== AFTER : $esc_file $Rev \x7B\x7B\x7B ====\n" . "$Deb==== \x7D\x7D\x7D ====\n") if $opt_v; } print("\n"); } else { warn("Cannot open temporary file \"$tmp_file\", " . "log messages not changed: $!"); } $total_changed++; } else { print("Message for $esc_file rev. $Rev is unchanged\n") if $opt_v; $total_skipped++; } # }}} } sub get_log_message { # Returns the cvs log message for the specified revision of a file. # Used by change_message(). # {{{ my ($File, $Rev) = @_; my $header_done = 0; my @Arr = (); my $getl_call = "get_log_message(\"$File\", \"$Rev\")"; if (open(PipeFP, "cvs$eroot_str log -r$Rev $File |")) { while (my $Line = ) { if ($Line =~ /^={77}$/) { if (!$header_done) { # No /^----------------------------$/ found die("Header terminator line not found in $getl_call, " . "incompatible version of CVS?"); } else { last; } } push(@Arr, $Line) if ($header_done); if (!$header_done && $Line =~ /^-{28}$/) { if ($header_done) { # FIXME: Should we die instead? warn("Found extra header separator in $getl_call, " . "continuing...\n"); } $header_done = 1; $Line = ; if ($Line =~ /^revision (\S+)/) { my $check_rev = $1; unless ($check_rev eq $Rev) { die("cvs log returned wrong revision \"$check_rev\", " . "expected \"$Rev\""); } } else { die("$getl_call expected \"^revision \", " . "got \"$Line\".\"."); } $Line = ; unless ($Line =~ /^date: .*;\s+author: .*;/) { die("Expected \"date: \", got \"$Line\".\n"); } $Line = ; unless ($Line =~ /^branches: .+;$/) { push(@Arr, $Line); } } } close(PipeFP); if ($header_done) { # print("======= $getl_call returns: ===========\n"); # print(join("", @Arr)); # print("============\n"); return(join("", @Arr)); } else { warn("Header separator not found, " . "$getl_call returns nothing\n"); return(""); } } else { die("Can't open cvs pipe: $!"); } # }}} } sub escape_filename { # Kludge for handling file names with spaces and characters that # trigger shell functions # {{{ my $Name = shift; # $Name =~ s/\\/\\\\/g; # $Name =~ s/([ \t;\|!&"'`#\$\(\)<>\*\?])/\\$1/g; $Name =~ s/'/\\'/g; $Name = "'$Name'"; return($Name); # }}} } sub print_help { # Send the help message to stdout # {{{ my $Retval = shift; print(< is a Perl script which changes CVS log messages for one or many files based on the output from a regular S> command. This makes it easy to edit lots of messages and then run the script once which changes all the modified messages. An easy way to do this can be: =over 4 =item 1. Go to the directory where your source files are, or check out a new revision into an empty directory. =item 2. Run Clogfile.txt> =item 3. Edit F (or whatever you call it) with your favourite text editor. =item 4. Run C =back All the messages you modified will now be changed by CVS using the S> command. Unchanged messages will not be updated. Another, faster way is to just read the output into your editor, edit it and filter the file through cvse. An example on how to do this in the vi(1) editor: :r !cvs log myfile.c [make changes] :%!cvse Done! =head1 OPTIONS =over 4 =item B<-d x> Use x as CVSROOT instead of the cvsroot specified in F or the C environment variable. =item B<-i> Ignore files which doesn't exist in this revision. Avoids update to random revisions. =item B<-h> Print a brief help summary. =item B<-s> Simulate only. Normal execution except the messages are not changed. =item B<-v> Verbose execution. Print some extra progress messages. =back =head1 ENVIRONMENT =over 4 =item CVSE_ROOT Specifies which CVSROOT to use during the message update. Can be used to force direct access to the repository directories to speed up things a lot if the current access method is client/server based. As an example, if you have local access to the repository and your current CVSROOT is CVSROOT=user@cvs.example.com:/my/repository you can set CVSE_ROOT=/my/repository to force CVS to work directly against the directory. This will improve the working speed dramatically because the client doesn't have to connect to the CVS server for every operation. The C<-d> option will override this variable. =back =head1 BUGS Not really bugs, but: Due to the format of S> output, messages can't contain any lines matching these patterns: /^-{28}$/ /^={77}$/ /^date: \d\d\d\d\/\d\d\/\d\d \d\d:\d\d:\d\d;\s+author: .*/ /^branches: .+;$/ If any of these patterns are found, the script will either ignore the line or interpret it as a message separator. CVS refuses to change the log message of a file that doesn't exist in the current revision. When the script notices that a certain file doesn't exist on this branch or in the current revision, it tries to restore an earlier revision with S> to get the file in place. When the file exists, CVS is able to change the log message. This results in random revisions of missing files showing up, but everything can be restored to normal with the usual S> command. To avoid messing up source trees with lots of tags and revisions, it's recommended to check out the files in a separate directory where the script can work, or use the C<-i> option which ignores non-existing files. If no revision can be restored, a warning is generated and no messages for this file will be changed. This only applies to files that I, revisions of existing files is untouched. Please send any bug reports or suggestions to the mail address below. =head1 AUTHOR Made by Øyvind A. Holm Ssunny _AT_ sunbase.orgE>. =head1 DOWNLOAD The newest version of the script can be found at L =head1 COPYRIGHT Copyright © 2003–2004 Free Software Foundation, Inc. This is free software; see the file F for legalese stuff. =head1 LICENCE This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA =head1 SEE ALSO cvs(1) =cut # }}} # vim: set fdm=marker ts=4 sw=4 sts=4 et : # vim: set fo+=2w fo-=n fenc=utf8 : # End of file $Id$