datefmt: Define 1 year as 365.2425 days instead of 365.25
[sunny256-utils.git] / cvse
blobe8483e0af1840a785e74ead0c7b63fd4a2269acb
1 #!/usr/bin/env perl
3 #=======================================================================
4 # $Id$
5 # Edit cvs log messages based on output from “cvs log”
7 # Character set used in this file: UTF-8
8 # Made by Øyvind A. Holm <sunny@sunbase.org>
9 # License: GNU General Public License. See end of file for legal stuff.
10 #=======================================================================
12 use strict;
13 use warnings;
15 use Getopt::Std;
16 our ($opt_d, $opt_h, $opt_i, $opt_s, $opt_v) =
17 ( "", 0, 0, 0, 0);
18 getopts('d:hisv') || die("Option error. Use -h for help.\n");
20 $| = 1;
22 # When changing the version number, also update the POD.
23 our $VERSION = "0.5";
25 our $rcs_id = '$Id$';
26 our $id_date = $rcs_id;
27 $id_date =~ s/^.*?\d+ (\d\d\d\d-.*?\d\d:\d\d:\d\d\S+).*/$1/;
29 if ($opt_h) {
30 print_help(0);
33 my $Simulate = $opt_s;
34 my $has_diff = 0;
35 ($has_diff = 1) if (`diff --version` =~ /diff/);
36 my ($curr_rev, $curr_rcs_file, $curr_work_file, $total_ignored) =
37 ( "", "", "", "");
38 my @Curr = ();
39 my %Entry = ();
40 my ($Rev, $Line) =
41 ( "", "");
42 my ($header_done, $subheader_done, $tmp_count) =
43 ( 0, 0, 0);
44 my %Text = ();
45 my %missing_file = ();
46 my ($start_utc, $total_skipped, $total_changed, $total_files) =
47 ( time, 0, 0, 0);
48 my @all_revs = ();
49 my $eroot_str = "";
51 defined($ENV{CVSE_ROOT}) && ($eroot_str = " -d $ENV{CVSE_ROOT}");
52 length($opt_d) && ($eroot_str = " -d $opt_d");
54 while (<>) {
55 $Line = $_;
56 if ($Line =~ /^-{28}$/) {
57 # New revision {{{
58 $header_done = 1;
59 if (length($Rev) && scalar(@Curr)) {
60 $Entry{$Rev} = join("", @Curr);
62 @Curr = ();
63 $Line = <>;
64 if ($Line =~ /^revision ([\d\.]+)/) {
65 $curr_rev = $1;
66 } else {
67 die("Line $.: Expected \"revision \", " .
68 "got \"$Line\".\". Aborting.");
70 $Line = <>;
71 unless ($Line =~ /^date: \S+\s+\S+ .*/) {
72 warn("Expected \"date: \", got \"$Line\".\".");
74 $Line = <>;
75 unless ($Line =~ /^branches: .*;$/) {
76 push(@Curr, $Line);
78 $Rev = "$curr_work_file,v.$curr_rev";
79 # }}}
80 } elsif ($Line =~ /^={77}$/) {
81 # List finished for this file, change the modified messages {{{
82 $header_done = 0;
83 $total_files++;
84 if (length($Rev) && scalar(@Curr)) {
85 $Entry{$Rev} = join("", @Curr);
87 @all_revs = ();
88 while (my ($l_name, $l_val) = each %Entry) {
89 push(@all_revs, $l_name);
91 for (@all_revs) {
92 # Scan through all revisions {{{
93 my $Curr = $_;
94 my ($a_file, $a_rev) =
95 ( "", "");
96 if ($Curr =~ /^(.+),v\.([\d\.]+?)$/) {
97 ($a_file, $a_rev) =
98 ( $1, $2);
99 if (length($a_file) && length($a_rev)) {
100 change_message($a_file, $a_rev, $Entry{$Curr});
102 } else {
103 warn("Wrong revision format \"$Curr\", " .
104 "skipping revision\n");
106 # }}}
108 ($curr_rev, $Rev) =
109 ( "", "");
110 %Entry = ();
111 @Curr = ();
112 # }}}
113 } elsif (!$header_done && $Line =~ /^RCS file: (.*)/) {
114 $curr_rcs_file = $1;
115 } elsif (!$header_done && $Line =~ /^Working file: (.*)/) {
116 $curr_work_file = $1;
117 } else {
118 # Regular log message {{{
119 if (length($Rev)) {
120 push(@Curr, $_);
122 # }}}
126 my $Seconds = time-$start_utc;
127 printf("\n%u file%s processed%s. %u revision%s changed, " .
128 "%u revision%s skipped. %u second%s used.\n",
129 $total_files, $total_files == 1 ? "" : "s",
130 $opt_i ? (", $total_ignored ignored") : "",
131 $total_changed, $total_changed == 1 ? "" : "s",
132 $total_skipped, $total_skipped == 1 ? "" : "s",
133 $Seconds, $Seconds == 1 ? "" : "s");
135 exit 0;
137 sub change_message {
138 # Changes a log message for a specific revision of a file if it has
139 # changed.
140 # {{{
142 my ($File, $Rev, $Txt) = @_;
143 my $esc_file = escape_filename($File);
145 if ($opt_i && !-e $File) {
146 unless (defined($missing_file{$File})) {
147 print("Ignoring non-existing file $esc_file\n");
148 $missing_file{$File} = 1;
149 $total_files--;
150 $total_ignored++;
152 return;
154 my $tmp_file = "cvse.$$.$tmp_count.tmp"; $tmp_count++;
155 my $compare_text = get_log_message($esc_file, $Rev);
156 if ($Txt ne $compare_text) {
157 print("\nChanging message for $esc_file rev. $Rev ...\n");
159 if (!defined($missing_file{$File}) && !(-e $File)) {
160 # File does not exist in this revision, change revision to
161 # make it appear and make it possible for CVS to update the
162 # message
163 # {{{
165 print("$esc_file not found, running cvs update with random " .
166 "revisions to try to make it appear...\n");
167 for my $Curr (@all_revs) {
168 if ($Curr =~ /^(.+),v\.([\d\.]+?)$/) {
169 my $t_rev = $2;
170 my $ex_str = "cvs$eroot_str upd -r $t_rev $esc_file";
171 print("Executing \"$ex_str\"\n");
172 system($ex_str);
173 if (-e $File) {
174 print("File exists with (old) revision $t_rev, " .
175 "CVS is now able to change the log message.\n");
176 last;
180 if (!-e $File && !defined($missing_file{$File})) {
181 warn("$esc_file: File does still not exist, messages for " .
182 "this file will not be changed\n");
183 $missing_file{$File} = 1;
185 # }}}
187 my @Arr = split(/\n/, $Txt);
188 if (open(TxtFP, ">$tmp_file")) {
189 for (@Arr) {
190 my $Line = $_;
191 if (/^date: .*/ || /^branches: .*/) {
192 # NOP
193 } else {
194 print(TxtFP "$Line\n");
197 close(TxtFP) || die("$tmp_file: Error closing file: $!");
198 my $exec_str = "cvs$eroot_str admin " .
199 "-m$Rev:\"`cat $tmp_file`\" $esc_file";
200 my $Deb = "";
201 $Deb = get_log_message($esc_file, $Rev);
202 if ($has_diff) {
203 if (open(DiffFP, ">BEFORE.cvse")) {
204 print(DiffFP $Deb);
205 close(DiffFP);
207 } else {
208 print("==== BEFORE: $esc_file $Rev \x7B\x7B\x7B ====\n" .
209 "$Deb==== \x7D\x7D\x7D ====\n");
211 printf("%s \"%s\"\n", $Simulate ? "Simulating"
212 : "Executing", $exec_str);
213 system($exec_str) unless $Simulate;
214 $Deb = get_log_message($esc_file, $Rev);
215 unlink($tmp_file) || warn("$tmp_file: Cannot remove file: $!");
216 if ($has_diff) {
217 if (open(DiffFP, ">AFTER.cvse")) {
218 print(DiffFP $Deb);
219 close(DiffFP);
221 print(
222 join("",
223 "==== Log diff for $File,v $Rev \x7B\x7B\x7B ====\n",
224 `diff -u BEFORE.cvse AFTER.cvse`,
225 "==== \x7D\x7D\x7D ====\n"
228 for ("BEFORE.cvse", "AFTER.cvse") {
229 unlink($_) || warn("$_: Cannot remove file: $!");
231 } else {
232 print("==== AFTER : $esc_file $Rev \x7B\x7B\x7B ====\n" .
233 "$Deb==== \x7D\x7D\x7D ====\n") if $opt_v;
235 print("\n");
236 } else {
237 warn("Cannot open temporary file \"$tmp_file\", " .
238 "log messages not changed: $!");
240 $total_changed++;
241 } else {
242 print("Message for $esc_file rev. $Rev is unchanged\n") if $opt_v;
243 $total_skipped++;
245 # }}}
248 sub get_log_message {
249 # Returns the cvs log message for the specified revision of a file.
250 # Used by change_message().
251 # {{{
253 my ($File, $Rev) = @_;
254 my $header_done = 0;
255 my @Arr = ();
256 my $getl_call = "get_log_message(\"$File\", \"$Rev\")";
258 if (open(PipeFP, "cvs$eroot_str log -r$Rev $File |")) {
259 while (my $Line = <PipeFP>) {
260 if ($Line =~ /^={77}$/) {
261 if (!$header_done) {
262 # No /^----------------------------$/ found
263 die("Header terminator line not found in $getl_call, " .
264 "incompatible version of CVS?");
265 } else {
266 last;
269 push(@Arr, $Line) if ($header_done);
270 if (!$header_done && $Line =~ /^-{28}$/) {
271 if ($header_done) {
272 # FIXME: Should we die instead?
273 warn("Found extra header separator in $getl_call, " .
274 "continuing...\n");
276 $header_done = 1;
277 $Line = <PipeFP>;
278 if ($Line =~ /^revision (\S+)/) {
279 my $check_rev = $1;
280 unless ($check_rev eq $Rev) {
281 die("cvs log returned wrong revision \"$check_rev\", " .
282 "expected \"$Rev\"");
284 } else {
285 die("$getl_call expected \"^revision \", " .
286 "got \"$Line\".\".");
288 $Line = <PipeFP>;
289 unless ($Line =~ /^date: .*;\s+author: .*;/) {
290 die("Expected \"date: \", got \"$Line\".\n");
292 $Line = <PipeFP>;
293 unless ($Line =~ /^branches: .+;$/) {
294 push(@Arr, $Line);
298 close(PipeFP);
299 if ($header_done) {
300 # print("======= $getl_call returns: ===========\n");
301 # print(join("", @Arr));
302 # print("============\n");
303 return(join("", @Arr));
304 } else {
305 warn("Header separator not found, " .
306 "$getl_call returns nothing\n");
307 return("");
309 } else {
310 die("Can't open cvs pipe: $!");
312 # }}}
315 sub escape_filename {
316 # Kludge for handling file names with spaces and characters that
317 # trigger shell functions
318 # {{{
320 my $Name = shift;
321 # $Name =~ s/\\/\\\\/g;
322 # $Name =~ s/([ \t;\|!&"'`#\$\(\)<>\*\?])/\\$1/g;
323 $Name =~ s/'/\\'/g;
324 $Name = "'$Name'";
325 return($Name);
326 # }}}
329 sub print_help {
330 # Send the help message to stdout
331 # {{{
332 my $Retval = shift;
333 print(<<END);
334 cvse v$VERSION -- $id_date
336 Syntax: cvse [options] [logfile [...]]
338 Options:
340 -d x Use x as CVSROOT instead of the cvsroot specified in CVS/Root or
341 the CVSE_ROOT environment variable.
342 -h Print this help message.
343 -i Ignore files which doesn't exist in this revision. Avoids update
344 to random revisions.
345 -s Simulate only. Normal execution except the messages are not
346 changed.
347 -v Verbose execution, print some extra progress messages.
350 exit($Retval);
351 # }}}
354 __END__
356 # Plain Old Documentation (POD) {{{
358 =pod
360 =head1 NAME
362 cvse -- CVSEdit -- edit CVS log messages
364 =head1 REVISION
366 Version: 0.5
368 $Id$
370 =head1 SYNOPSIS
372 cvse [options] [logfile [...]]
374 =head1 DESCRIPTION
376 B<cvse> is a Perl script which changes CVS log messages for one or many
377 files based on the output from a regular S<C<cvs log>> command.
378 This makes it easy to edit lots of messages and then run the script once
379 which changes all the modified messages.
381 An easy way to do this can be:
383 =over 4
385 =item 1. Go to the directory where your source files are, or check out a
386 new revision into an empty directory.
388 =item 2. Run C<cvs log E<gt>logfile.txt>
390 =item 3. Edit F<logfile.txt> (or whatever you call it) with your
391 favourite text editor.
393 =item 4. Run C<cvse logfile.txt>
395 =back
397 All the messages you modified will now be changed by CVS using the
398 S<C<cvs admin>> command.
399 Unchanged messages will not be updated.
401 Another, faster way is to just read the output into your editor, edit it
402 and filter the file through cvse.
403 An example on how to do this in the vi(1) editor:
405 :r !cvs log myfile.c
406 [make changes]
407 :%!cvse
409 Done!
411 =head1 OPTIONS
413 =over 4
415 =item B<-d x>
417 Use x as CVSROOT instead of the cvsroot specified in F<CVS/Root> or the
418 C<CVSE_ROOT> environment variable.
420 =item B<-i>
422 Ignore files which doesn't exist in this revision.
423 Avoids update to random revisions.
425 =item B<-h>
427 Print a brief help summary.
429 =item B<-s>
431 Simulate only.
432 Normal execution except the messages are not changed.
434 =item B<-v>
436 Verbose execution.
437 Print some extra progress messages.
439 =back
441 =head1 ENVIRONMENT
443 =over 4
445 =item CVSE_ROOT
447 Specifies which CVSROOT to use during the message update.
448 Can be used to force direct access to the repository directories to
449 speed up things a lot if the current access method is client/server
450 based.
451 As an example, if you have local access to the repository and your
452 current CVSROOT is
454 CVSROOT=user@cvs.example.com:/my/repository
456 you can set
458 CVSE_ROOT=/my/repository
460 to force CVS to work directly against the directory.
461 This will improve the working speed dramatically because the client
462 doesn't have to connect to the CVS server for every operation.
464 The C<-d> option will override this variable.
466 =back
468 =head1 BUGS
470 Not really bugs, but:
472 Due to the format of S<C<cvs log>> output, messages can't contain any
473 lines matching these patterns:
475 /^-{28}$/
476 /^={77}$/
477 /^date: \d\d\d\d\/\d\d\/\d\d \d\d:\d\d:\d\d;\s+author: .*/
478 /^branches: .+;$/
480 If any of these patterns are found, the script will either ignore the
481 line or interpret it as a message separator.
483 CVS refuses to change the log message of a file that doesn't exist in
484 the current revision.
485 When the script notices that a certain file doesn't exist on this branch
486 or in the current revision, it tries to restore an earlier revision with
487 S<C<cvs update>> to get the file in place.
488 When the file exists, CVS is able to change the log message.
489 This results in random revisions of missing files showing up, but
490 everything can be restored to normal with the usual S<C<cvs update -A>>
491 command.
492 To avoid messing up source trees with lots of tags and revisions, it's
493 recommended to check out the files in a separate directory where the
494 script can work, or use the C<-i> option which ignores non-existing
495 files.
496 If no revision can be restored, a warning is generated and no messages
497 for this file will be changed.
499 This only applies to files that I<does not exist>, revisions of existing
500 files is untouched.
502 Please send any bug reports or suggestions to the mail address below.
504 =head1 AUTHOR
506 Made by Øyvind A. Holm S<E<lt>sunny _AT_ sunbase.orgE<gt>>.
508 =head1 DOWNLOAD
510 The newest version of the script can be found at
511 L<http://www.sunbase.org/src/cvse/>
513 =head1 COPYRIGHT
515 Copyright © 2003–2004 Free Software Foundation, Inc.
516 This is free software; see the file F<COPYING> for legalese stuff.
518 =head1 LICENCE
520 This program is free software; you can redistribute it and/or modify it
521 under the terms of the GNU General Public License as published by the
522 Free Software Foundation; either version 2 of the License, or (at your
523 option) any later version.
525 This program is distributed in the hope that it will be useful, but
526 WITHOUT ANY WARRANTY; without even the implied warranty of
527 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
528 See the GNU General Public License for more details.
530 You should have received a copy of the GNU General Public License along
531 with this program; if not, write to the Free Software Foundation, Inc.,
532 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
534 =head1 SEE ALSO
536 cvs(1)
538 =cut
540 # }}}
542 # vim: set fdm=marker ts=4 sw=4 sts=4 et :
543 # vim: set fo+=2w fo-=n fenc=utf8 :
544 # End of file $Id$