mktar: Use `wc` instead of `du` in summary message
[sunny256-utils.git] / svndiff
blobac79a283670fae89d3b880e820b732c22c3b6363
1 #!/usr/bin/env perl
3 #=======================================================================
4 # svndiff
5 # File ID: 175cfc6a-f744-11dd-bd1f-000475e441b9
6 # Uses a specified diff program for viewing differences in a Subversion
7 # versioned directory tree.
9 # Character set: UTF-8
10 # ©opyleft 2004– Øyvind A. Holm <sunny@sunbase.org>
11 # License: GNU General Public License version 2 or later, see end of
12 # file for legal stuff.
13 # This file is part of the svnutils project — http://svnutils.tigris.org
14 #=======================================================================
16 use strict;
17 use warnings;
18 use Getopt::Long;
20 $| = 1;
22 our $Debug = 0;
24 our %Opt = (
26 'conflict' => 0,
27 'create-rc' => 0,
28 'debug' => 0,
29 'diff-cmd' => "",
30 'diffargs' => "", # DEPRECATED
31 'diffcmd' => "", # DEPRECATED
32 'extensions' => "",
33 'help' => 0,
34 'revision' => "",
35 'svn-cmd' => "",
36 'svncmd' => "", # DEPRECATED
37 'verbose' => 0,
38 'version' => 0,
42 our $progname = $0;
43 $progname =~ s/^.*\/(.*?)$/$1/;
44 our $VERSION = "0.00";
46 Getopt::Long::Configure("bundling");
47 GetOptions(
49 "conflict|C" => \$Opt{'conflict'},
50 "create-rc" => \$Opt{'create-rc'},
51 "debug" => \$Opt{'debug'},
52 "diff-cmd=s" => \$Opt{'diff-cmd'},
53 "diffargs|p=s" => \$Opt{'diffargs'}, # DEPRECATED
54 "diffcmd|c=s" => \$Opt{'diffcmd'}, # DEPRECATED
55 "extensions|x=s" => \$Opt{'extensions'},
56 "help|h" => \$Opt{'help'},
57 "revision|r=s" => \$Opt{'revision'},
58 "svn-cmd|e=s" => \$Opt{'svn-cmd'},
59 "svncmd=s" => \$Opt{'svncmd'}, # DEPRECATED
60 "verbose|v+" => \$Opt{'verbose'},
61 "version" => \$Opt{'version'},
63 ) || die("$progname: Option error. Use -h for help.\n");
65 deprecated("diffargs", "extensions", "--diffargs (-p)", "--extensions (-x)");
66 deprecated("diffcmd", "diff-cmd", "--diffcmd (-c)", "--diff-cmd");
67 deprecated("svncmd", "svn-cmd", "--svncmd", "--svn-cmd (-e)");
69 sub deprecated {
70 # Temporary subroutine until the old versions of the options are
71 # removed.
72 # {{{
73 my ($old_name, $new_name, $Old, $New) = @_;
75 if (length($Opt{$old_name})) {
76 warn("$progname: WARNING: The use of the $Old option " .
77 "is deprecated, use $New instead\n");
78 $Opt{$new_name} = $Opt{$old_name};
79 $Opt{$old_name} = undef;
81 # }}}
82 } # deprecated()
84 $Opt{'debug'} && ($Debug = 1);
85 if ($Opt{'version'}) {
86 print_version();
87 exit(0);
90 # Default value, can be overridden in ~/.svndiffrc
91 my $Cmd = "vimdiff";
93 # Change this if the svn executable is non-standard and you don’t want
94 # to use the -e option all the time:
95 my $CMD_SVN = "svn";
97 my $ST_CONFLICT = 'C';
98 my $ST_MODIFIED = 'M';
99 my $valid_rev = '\d+|HEAD|{\d+[^}]*?[Z\d]}'; # Used in regexps
101 my %rev_diff = ();
103 my $rc_file = defined($ENV{SVNDIFFRC}) ? $ENV{SVNDIFFRC} : "";
105 unless (length($rc_file)) {
106 if (defined($ENV{HOME})) {
107 $rc_file = "$ENV{HOME}/.svndiffrc";
108 } else {
109 warn("Both SVNDIFFRC and HOME environment variables not defined, " .
110 "unable to read rc file.\n" .
111 "Using default values. To override, " .
112 "define the SVNDIFFRC variable.\n"
117 length($rc_file) && (-e $rc_file) && read_rcfile($rc_file);
119 $Opt{'help'} && usage(0);
121 if ($Opt{'create-rc'}) {
122 print(<<END);
123 <?xml version="1.0" encoding="UTF-8"?>
124 <!DOCTYPE svndiffrc [
125 <!ELEMENT svndiffrc (diffprog, svnclient, reversediffs?)>
126 <!ELEMENT diffprog (#PCDATA)>
127 <!ELEMENT svnclient (#PCDATA)>
128 <!ELEMENT reversediffs (program)*>
129 <!ELEMENT program (name, reverse)>
130 <!ELEMENT name (#PCDATA)>
131 <!ELEMENT reverse (#PCDATA)>
133 <svndiffrc>
134 <diffprog>vimdiff</diffprog>
135 <svnclient>svn</svnclient>
136 <reversediffs>
137 <program>
138 <name>vimdiff</name>
139 <reverse>1</reverse>
140 </program>
141 <program>
142 <name>meld</name>
143 <reverse>1</reverse>
144 </program>
145 <program>
146 <name>kompare</name>
147 <reverse>1</reverse>
148 </program>
149 <program>
150 <name>xxdiff</name>
151 <reverse>1</reverse>
152 </program>
153 </reversediffs>
154 </svndiffrc>
156 exit(0);
159 length($Opt{'diff-cmd'}) && ($Cmd = $Opt{'diff-cmd'});
160 length($Opt{'extensions'}) && ($Cmd .= " $Opt{'extensions'}");
161 length($Opt{'svn-cmd'}) && ($CMD_SVN = $Opt{'svn-cmd'});
163 my $stat_chars = "$ST_CONFLICT$ST_MODIFIED";
164 $Opt{'conflict'} && ($stat_chars = "$ST_CONFLICT");
166 my @mod_array = ();
168 if (scalar(@ARGV)) {
169 # Filename(s) specified on command line. {{{
170 @mod_array = @ARGV;
171 for my $Curr (@mod_array) {
172 D("ARG = \"$Curr\"\n");
173 if ((-f $Curr && !-l $Curr) || is_url($Curr)) {
174 D("$Curr is a file or URL.");
175 my $has_conflict;
176 if (!is_url($Curr)) {
177 D("Before PipeFP 1: CMD_SVN = \"$CMD_SVN\"");
178 if (open(PipeFP, "$CMD_SVN stat $Curr -q |")) {
179 $has_conflict = (<PipeFP> =~ /^$ST_CONFLICT/) ? 1 : 0;
180 } else {
181 warn("$progname: Error opening " .
182 "\"$CMD_SVN $Curr stat -q\" pipe: $!");
184 } else {
185 if (!length($Opt{'revision'})) {
186 die("$progname: Need to specify the --revision option " .
187 "when diffing an URL\n");
189 $has_conflict = 0;
191 diff_file($Curr, $has_conflict, $Opt{'revision'});
192 } else {
193 D("$Curr is NOT a file.");
194 warn("$progname: \"$Curr\" is not a file or doesn't exist\n");
197 # }}}
198 } else {
199 # {{{
200 length($Opt{'revision'}) && die("Need to specify one or more " .
201 "files when using the -r option\n");
202 D("Before PipeFP 2: CMD_SVN = \"$CMD_SVN\"");
203 if (open(PipeFP, "$CMD_SVN stat -q |")) {
204 my %has_conflict = ();
205 while (<PipeFP>) {
206 chomp();
207 D("<PipeFP> = \"$_\"\n");
208 # FIXME: Various svn versions add space columns now and
209 # then. Find a way to check current svn version and use the
210 # appropriate number of columns. Or maybe better, use --xml,
211 # but have to check how it works with older svn versions.
212 if (/^([$stat_chars]) +(.*)/) {
213 my ($Stat, $File) =
214 ( $1, $2);
215 D("\$Stat = \"$Stat\", \$File = \"$File\"\n");
216 push(@mod_array, $File);
217 $has_conflict{$File} = ($Stat =~ /^$ST_CONFLICT/) ? 1 : 0;
218 D("\$has_conflict{$File} = \"$has_conflict{$File}\"\n");
221 close(PipeFP);
222 for (sort @mod_array) {
223 my $File = $_;
224 (-f $File && !-l $File) && diff_file($File, $has_conflict{$File});
226 } else {
227 warn("$progname: Error opening \"$CMD_SVN stat -q\" pipe: $!");
229 # }}}
232 sub diff_file {
233 # {{{
234 my ($File1, $has_conflict, $Revs) = @_;
235 my $Path = "";
236 my $File = $File1;
237 defined($Revs) || ($Revs = "");
239 D("diff_file(\"$File1\", \"$has_conflict\", \"$Revs\");\n");
241 if ($File =~ m#^(.*/)(.+?)$#) {
242 $Path = $1;
243 $File = $2;
246 my $File2 = "";
247 my @rm_files = ();
249 D("Opt{'revision'} = \"$Opt{'revision'}\"");
250 if (length($Opt{'revision'})) {
251 my ($Rev1, $Rev2);
252 my ($tmp1, $tmp2);
253 if ($Opt{'revision'} =~ /^($valid_rev)$/) {
254 $Rev1 = $1;
255 $Rev2 = "";
256 $tmp1 = "$File1.r$Rev1.tmp";
257 } elsif ($Opt{'revision'} =~ /^($valid_rev):($valid_rev)$/) {
258 $Rev1 = $1;
259 $Rev2 = $2;
260 $tmp1 = "$File1.r$Rev1.tmp";
261 $tmp2 = "$File1.r$Rev2.tmp";
262 } else {
263 die("$progname: Revision format error in --revision argument, " .
264 "use -h for help\n");
266 if (is_url($File1)) {
267 $tmp1 =~ s#^(\S+/)(\S+?)$#$2#;
268 length($Rev2) || ($Rev2 = "HEAD");
269 $tmp2 = "$File1.r$Rev2.tmp";
270 $tmp2 =~ s#^(\S+/)(\S+?)$#$2#;
271 } else {
272 $tmp2 = "$File1.r$Rev2.tmp";
274 D("Rev1 = \"$Rev1\", Rev2 = \"$Rev2\"\n");
275 (-e $tmp1)
276 && (die("$progname: $tmp1: Temporary file already exists\n"));
277 (length($Rev2) && -e $tmp2)
278 && (die("$progname: $tmp2: Temporary file already exists\n"));
279 D("tmp1 = \"$tmp1\"");
280 D("tmp2 = \"$tmp2\"");
281 if ($tmp1 eq $tmp2) {
282 warn("$progname: $File1: Start and end revisions are the same\n");
283 return;
285 mysyst("$CMD_SVN cat -r$Rev1 $File1 >$tmp1");
286 mysyst("$CMD_SVN cat -r$Rev2 $File1 >$tmp2") if (length($Rev2));
287 if (length($Rev2)) {
288 $File2 = "$tmp2";
289 $File1 = "$tmp1";
290 push(@rm_files, $tmp1, $tmp2);
291 } else {
292 $File2 = "$tmp1";
293 push(@rm_files, $tmp1);
295 } else {
296 $File2 = "$Path.svn/text-base/$File.svn-base";
299 D("File1 = \"$File1\"\n");
300 D("File2 = \"$File2\"\n");
302 if (!is_url($File1)) {
303 (-e $File1) || (warn("$File1: File not found\n"), return);
304 (-e $File2) || (warn("$File2: File not found" .
305 length($Opt{'revision'})
306 ? ""
307 : ", is not under version control\n"
309 return
313 my $use_reverse = 0;
315 if (defined($rev_diff{$Cmd})) {
316 ($rev_diff{$Cmd} eq "1") && ($use_reverse = 1);
319 if ($use_reverse) {
320 mysyst("$Cmd $File1 $File2");
321 } else {
322 mysyst("$Cmd $File2 $File1");
325 for my $curr_rm (@rm_files) {
326 D("Removing tempfile \"$curr_rm\"...");
327 unlink($curr_rm) || warn("$progname: $curr_rm: " .
328 "Can't delete temporary file: $!\n");
331 if (!length($Opt{'revision'}) && $has_conflict) {
332 print("$progname: Write y and press ENTER if the conflict " .
333 "in $File1 is resolved: ");
334 if (<STDIN> =~ /^y$/i) {
335 print("$progname: OK, marking $File1 as resolved.\n");
336 mysyst("$CMD_SVN resolved $File1");
340 # Sleep one second after $Cmd is done to make it easier to interrupt
341 # the thing with CTRL-C if there are many files
342 sleep(1) if (scalar(@mod_array) > 1);
343 # }}}
344 } # diff_file()
346 sub deb_wait {
347 # {{{
348 $Debug || return;
349 print("debug: Press ENTER...");
350 <STDIN>;
351 # }}}
352 } # deb_wait()
354 sub is_url {
355 # {{{
356 my $Url = shift;
358 my $Retval = ($Url =~ m#^\S+://\S+/#) ? 1 : 0;
359 D("is_url(\"$Url\") returns \"$Retval\".");
360 return($Retval);
361 # }}}
362 } # is_url()
364 sub mysyst {
365 # {{{
366 my @Args = @_;
367 my $system_txt = sprintf("system(\"%s\");", join("\", \"", @Args));
368 D("$system_txt");
369 deb_wait();
370 system(@_);
371 # }}}
372 } # mysyst()
374 sub read_rcfile {
375 # {{{
376 my $File = shift;
378 D("read_rcfile(\"$File\")");
379 if (open(RcFP, "<$File")) {
380 my $all_rc = join("", <RcFP>);
381 close(RcFP);
382 # D("\$all_rc \x7B\x7B\x7B\n$all_rc\n\x7D\x7D\x7D");
384 my $el_top = $all_rc;
385 $el_top =~ s/<!--.*?-->//gsx;
387 $el_top =~
389 <svndiffrc\b(.*?)>(.*?)</svndiffrc>
392 my $el_svndiffrc = $2;
393 # D("Inside <svndiffrc></svndiffrc>");
394 # D("\$el_svndiffrc \x7B\x7B\x7B\n$el_svndiffrc\n\x7D\x7D\x7D");
395 $el_svndiffrc =~
397 <diffprog\b(.*?)>(.*?)</diffprog>
400 $Cmd = xml_to_txt($2);
401 # D("read_rcfile(): \$Cmd = \"$Cmd\"");
403 }sex;
405 $el_svndiffrc =~
407 <svnclient\b(.*?)>(.*?)</svnclient>
410 $CMD_SVN = xml_to_txt($2);
411 # D("read_rcfile(): \$CMD_SVN = \"$CMD_SVN\"");
413 }sex;
415 $el_svndiffrc =~
417 <reversediffs\b(.*?)>(.*?)</reversediffs>
420 my $el_reversediffs = $2;
421 # D("Inside <reversediffs></reversediffs>");
423 $el_reversediffs =~
425 <program\b(.*?)>(.*?)</program>
428 my $el_program = $2;
429 # D("Inside <program></program>");
431 my ($Name, $Reverse) =
432 ( "", "");
434 $el_program =~
436 <name\b(.*?)>(.*?)</name>
439 $Name = xml_to_txt($2);
440 # D("Name = \"$Name\"");
442 }sex;
444 $el_program =~
446 <reverse\b(.*?)>(.*?)</reverse>
449 $Reverse = xml_to_txt($2);
450 # D("Reverse = \"$Reverse\"");
452 }sex;
454 if (length($Name)) {
455 $rev_diff{$Name} = ($Reverse eq "1" ? 1 : 0);
456 # D("\$rev_diff{$Name} = \"$rev_diff{$Name}\"");
457 } else {
458 warn("$progname: $File: Found empty " .
459 "<name></name> element.\n");
462 }gsex;
464 }sex;
465 print_leftover($el_svndiffrc, "svndiffrc");
466 }sex;
467 print_leftover($el_top, "top");
468 } else {
469 warn("$progname: $File: Can't open rc file for read: $!\n");
471 # }}}
472 } # read_rcfile()
474 sub print_leftover {
475 # Print all non-whitespace in a string, used to spot erroneous XML. {{{
476 $Debug || return("");
477 my ($Txt, $Element) = @_;
478 $Txt =~ s/^\s+//gs;
479 $Txt =~ s/\s+$//gs;
480 $Txt =~ s/\s+/ /g;
481 defined($Element) || ($Element = "[unknown]");
482 if ($Txt =~ /\S/) {
483 warn("$progname: Leftover: $Element: \"$Txt\"\n");
485 return("");
486 # }}}
487 } # print_leftover()
489 sub txt_to_xml {
490 # {{{
491 my $Txt = shift;
492 $Txt =~ s/&/&amp;/gs;
493 $Txt =~ s/</&lt;/gs;
494 $Txt =~ s/>/&gt;/gs;
495 return($Txt);
496 # }}}
497 } # txt_to_xml()
499 sub xml_to_txt {
500 # {{{
501 my $Txt = shift;
502 $Txt =~ s/&lt;/</gs;
503 $Txt =~ s/&gt;/>/gs;
504 $Txt =~ s/&amp;/&/gs;
505 return($Txt);
506 # }}}
507 } # xml_to_txt()
509 sub print_version {
510 # Print program version {{{
511 print("$progname v$VERSION\n");
512 # }}}
513 } # print_version()
515 sub usage {
516 # Send the help message to stdout {{{
517 my $Retval = shift;
519 if ($Opt{'verbose'}) {
520 print("\n");
521 print_version();
523 print(<<END);
525 Usage: $progname [options] [file [...]]
527 "file" can also be an URL, but then the --revision option has to be
528 specified.
530 Options:
532 -C, --conflict
533 Only run diff on conflicted files.
534 --create-rc
535 Send a configuration file example to stdout. To create a new
536 ~/.svndiffrc file, write
537 $progname --create-rc >~/.svndiffrc
538 --diff-cmd x
539 Use x as the diff command. Default: "$Cmd".
540 -e, --svn-cmd x
541 Use x as the svn executable. Default: "$CMD_SVN".
542 -h, --help
543 Show this help.
544 -x, --extensions x
545 Use x as parameters to the diff program.
546 -r, --revision x
547 Run a $Cmd command against previous revisions:
548 111:222
549 Compare r111 and r222.
551 Compare your working file against r123. If the file is an URL,
552 the second revision is set to HEAD.
553 {2001-05-17T18:12:16Z}:900
554 Compare between a specific point in time with r900.
555 -v, --verbose
556 Increase level of verbosity. Can be repeated.
557 --version
558 Print version information.
559 --debug
560 Print debugging messages.
563 exit($Retval);
564 # }}}
565 } # usage()
567 sub msg {
568 # Print a status message to stderr based on verbosity level {{{
569 my ($verbose_level, $Txt) = @_;
571 if ($Opt{'verbose'} >= $verbose_level) {
572 print(STDERR "$progname: $Txt\n");
574 # }}}
575 } # msg()
577 sub D {
578 # Print a debugging message {{{
579 $Debug || return;
580 my @call_info = caller;
581 chomp(my $Txt = shift);
582 my $File = $call_info[1];
583 $File =~ s#\\#/#g;
584 $File =~ s#^.*/(.*?)$#$1#;
585 print(STDERR "$File:$call_info[2] $$ $Txt\n");
586 return("");
587 # }}}
588 } # D()
590 __END__
592 # Plain Old Documentation (POD) {{{
594 =pod
596 =head1 NAME
598 svndiff
600 =head1 SYNOPSIS
602 B<svndiff> [I<options>] [I<file> [I<...>]]
604 =head1 DESCRIPTION
606 Run the specified diff program on every modified file in current
607 directory and all subdirectories or on the files specified on the
608 command line.
609 An URL to a file can also be specified, but then the --revision option
610 has to be specified.
612 The program needs the svn(1) commandline client to run.
614 =over 4
616 =item B<-C>, B<--conflict>
618 Only run diff on conflicted files.
620 =item B<--create-rc>
622 Send a configuration file example to stdout. To create a new
623 F<~/.svndiffrc> file, write
625 $progname --create-rc >~/.svndiffrc
627 =item B<--diff-cmd> x
629 Use x as the diff command.
630 Default: "vimdiff".
632 =item B<-e>, B<--svn-cmd> x
634 Use x as the svn executable.
635 Example:
637 svndiff -e /usr/local/bin/svn-1.0
639 =item B<-x>, B<--extensions> x
641 Use x as parameters to the diff program.
643 =item B<-h>, B<--help>
645 Print a brief help summary.
647 =item B<-r>, B<--revision> x
649 Run the external diff command against previous revisions:
651 111:222
652 Compare r111 and r222.
654 Compare your working file against r123. If the file is an URL, the
655 second revision is set to HEAD.
657 =item B<-v>, B<--verbose>
659 Increase level of verbosity. Can be repeated.
661 =item B<--version>
663 Print version information.
665 =item B<--debug>
667 Print debugging messages.
669 =back
671 =head1 FILES
673 =over 4
675 =item F<~/.svndiffrc>
677 A configuration file where you can store your own settings.
678 It is a standard XML file with this structure:
680 <svndiffrc>
681 <diffprog>vimdiff</diffprog>
682 <svnclient>svn</svnclient>
683 <reversediffs>
684 <program>
685 <name>vimdiff</name>
686 <reverse>1</reverse>
687 </program>
688 <!-- Several "program" element groups can be specified -->
689 </reversediffs>
690 </svndiffrc>
692 (Whitespace and linebreaks are optional.)
694 The string inside the C<diffprog> elements can be set to whatever your
695 diff program is called as, the default string is "vimdiff".
697 You can also define an alternative svn(1) client to use inside the
698 C<svnclient> elements.
699 The default value here is of course "svn".
701 When using visual diff viewers (for example B<vimdiff>), the program
702 sometimes expects the file names to be switched on the command line so
703 your modified file appears in the window of your taste.
704 By creating a C<E<lt>programE<gt>E<lt>/programE<gt>> section, programs
705 can be instructed to take arguments the opposite way.
706 If you for example use the B<meld> program and you want your modified
707 file to be in the left window, add this to the file (I<inside> the
708 C<E<lt>reversediffsE<gt>E<lt>/reversediffsE<gt>> elements):
710 <program>
711 <name>meld</name>
712 <reverse>1</reverse>
713 </program>
715 The value in the C<reverse> element have to be B<1>, all other values
716 will count as B<0>.
718 =back
720 =head1 ENVIRONMENT VARIABLES
722 =over 4
724 =item I<SVNDIFFRC>
726 Path to a configuration file in another location than F<~/.svndiffrc> .
728 =back
730 =head1 USAGE TIPS
732 =head2 vimdiff mappings
734 The standard diff program used in this script is vimdiff(1) which in
735 fact is the Vim editor called with another name.
736 The main reasons for this are because Vim is Free and widely available,
737 portable, console based and an effective diff tool.
738 The following macros makes moving differences between windows easier
739 (can also be put into F<~/.vimrc>):
741 " F1: Move differences from the other window to the current window.
742 map <f1> :diffget<cr>]cz.
744 " F2: Move differences from the current window to the other window.
745 map <f2> :diffput<cr>]c
747 " F12: Update the syntax highlighting and the diffs. Use this if your
748 " diff isn’t properly updated.
749 noremap <f12> :syntax sync fromstart<cr>:diffu<cr>
750 inoremap <f12> <esc>:syntax sync fromstart<cr>:diffu<cr>a
752 =head1 AUTHOR
754 Made by Øyvind A. Holm S<E<lt>sunny@sunbase.orgE<gt>>.
756 =head1 COPYRIGHT
758 Copyleft © Øyvind A. Holm E<lt>sunny@sunbase.orgE<gt>
759 This is free software; see the file F<COPYING> for legalese stuff.
761 This file is part of the svnutils project —
762 L<http://svnutils.tigris.org/>
764 =head1 LICENCE
766 This program is free software: you can redistribute it and/or modify it
767 under the terms of the GNU General Public License as published by the
768 Free Software Foundation, either version 2 of the License, or (at your
769 option) any later version.
771 This program is distributed in the hope that it will be useful, but
772 WITHOUT ANY WARRANTY; without even the implied warranty of
773 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
774 See the GNU General Public License for more details.
776 You should have received a copy of the GNU General Public License along
777 with this program.
778 If not, see L<http://www.gnu.org/licenses/>.
780 =head1 SEE ALSO
782 svn(1)
784 =cut
786 # }}}
788 # vim: set fenc=UTF-8 ft=perl fdm=marker ts=4 sw=4 sts=4 et fo+=w :