3 #=======================================================================
5 # File ID: 175cfc6a-f744-11dd-bd1f-000475e441b9
6 # Uses a specified diff program for viewing differences in a Subversion
7 # versioned directory tree.
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 #=======================================================================
30 'diffargs' => "", # DEPRECATED
31 'diffcmd' => "", # DEPRECATED
36 'svncmd' => "", # DEPRECATED
43 $progname =~ s/^.*\/(.*?)$/$1/;
44 our $VERSION = "0.00";
46 Getopt
::Long
::Configure
("bundling");
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)");
70 # Temporary subroutine until the old versions of the options are
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;
84 $Opt{'debug'} && ($Debug = 1);
85 if ($Opt{'version'}) {
90 # Default value, can be overridden in ~/.svndiffrc
93 # Change this if the svn executable is non-standard and you don’t want
94 # to use the -e option all the time:
97 my $ST_CONFLICT = 'C';
98 my $ST_MODIFIED = 'M';
99 my $valid_rev = '\d+|HEAD|{\d+[^}]*?[Z\d]}'; # Used in regexps
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";
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'}) {
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)>
134 <diffprog>vimdiff</diffprog>
135 <svnclient>svn</svnclient>
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");
169 # Filename(s) specified on command line. {{{
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.");
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;
181 warn("$progname: Error opening " .
182 "\"$CMD_SVN $Curr stat -q\" pipe: $!");
185 if (!length($Opt{'revision'})) {
186 die("$progname: Need to specify the --revision option " .
187 "when diffing an URL\n");
191 diff_file
($Curr, $has_conflict, $Opt{'revision'});
193 D
("$Curr is NOT a file.");
194 warn("$progname: \"$Curr\" is not a file or doesn't exist\n");
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 = ();
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]) +(.*)/) {
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");
222 for (sort @mod_array) {
224 (-f
$File && !-l
$File) && diff_file
($File, $has_conflict{$File});
227 warn("$progname: Error opening \"$CMD_SVN stat -q\" pipe: $!");
234 my ($File1, $has_conflict, $Revs) = @_;
237 defined($Revs) || ($Revs = "");
239 D
("diff_file(\"$File1\", \"$has_conflict\", \"$Revs\");\n");
241 if ($File =~ m
#^(.*/)(.+?)$#) {
249 D
("Opt{'revision'} = \"$Opt{'revision'}\"");
250 if (length($Opt{'revision'})) {
253 if ($Opt{'revision'} =~ /^($valid_rev)$/) {
256 $tmp1 = "$File1.r$Rev1.tmp";
257 } elsif ($Opt{'revision'} =~ /^($valid_rev):($valid_rev)$/) {
260 $tmp1 = "$File1.r$Rev1.tmp";
261 $tmp2 = "$File1.r$Rev2.tmp";
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#;
272 $tmp2 = "$File1.r$Rev2.tmp";
274 D
("Rev1 = \"$Rev1\", Rev2 = \"$Rev2\"\n");
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");
285 mysyst
("$CMD_SVN cat -r$Rev1 $File1 >$tmp1");
286 mysyst
("$CMD_SVN cat -r$Rev2 $File1 >$tmp2") if (length($Rev2));
290 push(@rm_files, $tmp1, $tmp2);
293 push(@rm_files, $tmp1);
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'})
307 : ", is not under version control\n"
315 if (defined($rev_diff{$Cmd})) {
316 ($rev_diff{$Cmd} eq "1") && ($use_reverse = 1);
320 mysyst
("$Cmd $File1 $File2");
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);
349 print("debug: Press ENTER...");
358 my $Retval = ($Url =~ m
#^\S+://\S+/#) ? 1 : 0;
359 D
("is_url(\"$Url\") returns \"$Retval\".");
367 my $system_txt = sprintf("system(\"%s\");", join("\", \"", @Args));
378 D
("read_rcfile(\"$File\")");
379 if (open(RcFP
, "<$File")) {
380 my $all_rc = join("", <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;
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");
397 <diffprog
\b(.*?
)>(.*?
)</diffprog
>
400 $Cmd = xml_to_txt
($2);
401 # D("read_rcfile(): \$Cmd = \"$Cmd\"");
407 <svnclient
\b(.*?
)>(.*?
)</svnclient
>
410 $CMD_SVN = xml_to_txt
($2);
411 # D("read_rcfile(): \$CMD_SVN = \"$CMD_SVN\"");
417 <reversediffs
\b(.*?
)>(.*?
)</reversediffs
>
420 my $el_reversediffs = $2;
421 # D("Inside <reversediffs></reversediffs>");
425 <program
\b(.*?
)>(.*?
)</program
>
429 # D("Inside <program></program>");
431 my ($Name, $Reverse) =
436 <name
\b(.*?
)>(.*?
)</name
>
439 $Name = xml_to_txt
($2);
440 # D("Name = \"$Name\"");
446 <reverse\b(.*?
)>(.*?
)</reverse>
449 $Reverse = xml_to_txt
($2);
450 # D("Reverse = \"$Reverse\"");
455 $rev_diff{$Name} = ($Reverse eq "1" ?
1 : 0);
456 # D("\$rev_diff{$Name} = \"$rev_diff{$Name}\"");
458 warn("$progname: $File: Found empty " .
459 "<name></name> element.\n");
465 print_leftover
($el_svndiffrc, "svndiffrc");
467 print_leftover
($el_top, "top");
469 warn("$progname: $File: Can't open rc file for read: $!\n");
475 # Print all non-whitespace in a string, used to spot erroneous XML. {{{
476 $Debug || return("");
477 my ($Txt, $Element) = @_;
481 defined($Element) || ($Element = "[unknown]");
483 warn("$progname: Leftover: $Element: \"$Txt\"\n");
492 $Txt =~ s/&/&/gs;
504 $Txt =~ s/&/&/gs;
510 # Print program version {{{
511 print("$progname v$VERSION\n");
516 # Send the help message to stdout {{{
519 if ($Opt{'verbose'}) {
525 Usage: $progname [options] [file [...]]
527 "file" can also be an URL, but then the --revision option has to be
533 Only run diff on conflicted files.
535 Send a configuration file example to stdout. To create a new
536 ~/.svndiffrc file, write
537 $progname --create-rc >~/.svndiffrc
539 Use x as the diff command. Default: "$Cmd".
541 Use x as the svn executable. Default: "$CMD_SVN".
545 Use x as parameters to the diff program.
547 Run a $Cmd command against previous revisions:
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.
556 Increase level of verbosity. Can be repeated.
558 Print version information.
560 Print debugging messages.
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");
578 # Print a debugging message {{{
580 my @call_info = caller;
581 chomp(my $Txt = shift);
582 my $File = $call_info[1];
584 $File =~ s
#^.*/(.*?)$#$1#;
585 print(STDERR
"$File:$call_info[2] $$ $Txt\n");
592 # Plain Old Documentation (POD) {{{
602 B<svndiff> [I<options>] [I<file> [I<...>]]
606 Run the specified diff program on every modified file in current
607 directory and all subdirectories or on the files specified on the
609 An URL to a file can also be specified, but then the --revision option
612 The program needs the svn(1) commandline client to run.
616 =item B<-C>, B<--conflict>
618 Only run diff on conflicted files.
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.
632 =item B<-e>, B<--svn-cmd> x
634 Use x as the svn executable.
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:
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.
663 Print version information.
667 Print debugging messages.
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:
681 <diffprog>vimdiff</diffprog>
682 <svnclient>svn</svnclient>
688 <!-- Several "program" element groups can be specified -->
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):
715 The value in the C<reverse> element have to be B<1>, all other values
720 =head1 ENVIRONMENT VARIABLES
726 Path to a configuration file in another location than F<~/.svndiffrc> .
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
754 Made by Øyvind A. Holm S<E<lt>sunny@sunbase.orgE<gt>>.
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/>
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
778 If not, see L<http://www.gnu.org/licenses/>.
788 # vim: set fenc=UTF-8 ft=perl fdm=marker ts=4 sw=4 sts=4 et fo+=w :