From 3f94ff755e266520ba2f6007d557395c33d30fdc Mon Sep 17 00:00:00 2001 From: Tim Henigan Date: Thu, 22 Mar 2012 15:52:16 -0400 Subject: [PATCH 1/8] difftool: parse options using Getopt::Long Replace custom option/argument parser with standard Getopt::Long module. This shortens the code and makes it easier to understand. Signed-off-by: Tim Henigan Signed-off-by: Junio C Hamano --- git-difftool.perl | 111 +++++++++++++++++++--------------------------- 1 file changed, 46 insertions(+), 65 deletions(-) diff --git a/git-difftool.perl b/git-difftool.perl index 09b65f177..bade735cc 100755 --- a/git-difftool.perl +++ b/git-difftool.perl @@ -15,17 +15,15 @@ use warnings; use Cwd qw(abs_path); use File::Basename qw(dirname); - -require Git; - -my $DIR = abs_path(dirname($0)); - +use Getopt::Long qw(:config pass_through); +use Git; sub usage { print << 'USAGE'; -usage: git difftool [-t|--tool=] [-x|--extcmd=] - [-y|--no-prompt] [-g|--gui] +usage: git difftool [-t|--tool=] + [-x|--extcmd=] [-g|--gui] + [--prompt] [-y|--no-prompt] ['git diff' options] USAGE exit 1; @@ -33,6 +31,7 @@ sub usage sub setup_environment { + my $DIR = abs_path(dirname($0)); $ENV{PATH} = "$DIR:$ENV{PATH}"; $ENV{GIT_PAGER} = ''; $ENV{GIT_EXTERNAL_DIFF} = 'git-difftool--helper'; @@ -47,75 +46,57 @@ sub exe return $exe; } -sub generate_command -{ - my @command = (exe('git'), 'diff'); - my $skip_next = 0; - my $idx = -1; - my $prompt = ''; - for my $arg (@ARGV) { - $idx++; - if ($skip_next) { - $skip_next = 0; - next; - } - if ($arg eq '-t' || $arg eq '--tool') { - usage() if $#ARGV <= $idx; - $ENV{GIT_DIFF_TOOL} = $ARGV[$idx + 1]; - $skip_next = 1; - next; - } - if ($arg =~ /^--tool=/) { - $ENV{GIT_DIFF_TOOL} = substr($arg, 7); - next; - } - if ($arg eq '-x' || $arg eq '--extcmd') { - usage() if $#ARGV <= $idx; - $ENV{GIT_DIFFTOOL_EXTCMD} = $ARGV[$idx + 1]; - $skip_next = 1; - next; - } - if ($arg =~ /^--extcmd=/) { - $ENV{GIT_DIFFTOOL_EXTCMD} = substr($arg, 9); - next; - } - if ($arg eq '-g' || $arg eq '--gui') { - eval { - my $tool = Git::command_oneline('config', - 'diff.guitool'); - if (length($tool)) { - $ENV{GIT_DIFF_TOOL} = $tool; - } - }; - next; - } - if ($arg eq '-y' || $arg eq '--no-prompt') { - $prompt = 'no'; - next; - } - if ($arg eq '--prompt') { - $prompt = 'yes'; - next; - } - if ($arg eq '-h') { - usage(); - } - push @command, $arg; +# parse command-line options. all unrecognized options and arguments +# are passed through to the 'git diff' command. +my ($difftool_cmd, $extcmd, $gui, $help, $prompt); +GetOptions('g|gui' => \$gui, + 'h' => \$help, + 'prompt!' => \$prompt, + 'y' => sub { $prompt = 0; }, + 't|tool:s' => \$difftool_cmd, + 'x|extcmd:s' => \$extcmd); + +if (defined($help)) { + usage(); +} +if (defined($difftool_cmd)) { + if (length($difftool_cmd) > 0) { + $ENV{GIT_DIFF_TOOL} = $difftool_cmd; + } else { + print "No given for --tool=\n"; + usage(); } - if ($prompt eq 'yes') { +} +if (defined($extcmd)) { + if (length($extcmd) > 0) { + $ENV{GIT_DIFFTOOL_EXTCMD} = $extcmd; + } else { + print "No given for --extcmd=\n"; + usage(); + } +} +if (defined($gui)) { + my $guitool = ""; + $guitool = Git::config('diff.guitool'); + if (length($guitool) > 0) { + $ENV{GIT_DIFF_TOOL} = $guitool; + } +} +if (defined($prompt)) { + if ($prompt) { $ENV{GIT_DIFFTOOL_PROMPT} = 'true'; - } elsif ($prompt eq 'no') { + } else { $ENV{GIT_DIFFTOOL_NO_PROMPT} = 'true'; } - return @command } setup_environment(); +my @command = (exe('git'), 'diff', @ARGV); # ActiveState Perl for Win32 does not implement POSIX semantics of # exec* system call. It just spawns the given executable and finishes # the starting program, exiting with code 0. # system will at least catch the errors returned by git diff, # allowing the caller of git difftool better handling of failures. -my $rc = system(generate_command()); +my $rc = system(@command); exit($rc | ($rc >> 8)); From 850896042a42c1c16912a3981ca844e2cbcd14d4 Mon Sep 17 00:00:00 2001 From: Tim Henigan Date: Thu, 22 Mar 2012 15:52:17 -0400 Subject: [PATCH 2/8] difftool: add '--no-gui' option This commit teaches difftool to handle the '--no-gui' option. This option negates the existing '--gui' option. The last setting given on the command line wins. This allows a user to configure "[alias] mdt = difftool --gui", but still have the ability to override the setting without error: $ git mdt --no-gui Signed-off-by: Tim Henigan Signed-off-by: Junio C Hamano --- git-difftool.perl | 7 ++++--- t/t7800-difftool.sh | 13 +++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/git-difftool.perl b/git-difftool.perl index bade735cc..93e84e8d3 100755 --- a/git-difftool.perl +++ b/git-difftool.perl @@ -22,7 +22,8 @@ sub usage { print << 'USAGE'; usage: git difftool [-t|--tool=] - [-x|--extcmd=] [-g|--gui] + [-x|--extcmd=] + [-g|--gui] [--no-gui] [--prompt] [-y|--no-prompt] ['git diff' options] USAGE @@ -49,7 +50,7 @@ sub exe # parse command-line options. all unrecognized options and arguments # are passed through to the 'git diff' command. my ($difftool_cmd, $extcmd, $gui, $help, $prompt); -GetOptions('g|gui' => \$gui, +GetOptions('g|gui!' => \$gui, 'h' => \$help, 'prompt!' => \$prompt, 'y' => sub { $prompt = 0; }, @@ -75,7 +76,7 @@ sub exe usage(); } } -if (defined($gui)) { +if ($gui) { my $guitool = ""; $guitool = Git::config('diff.guitool'); if (length($guitool) > 0) { diff --git a/t/t7800-difftool.sh b/t/t7800-difftool.sh index 4fb4c9384..e716d066c 100755 --- a/t/t7800-difftool.sh +++ b/t/t7800-difftool.sh @@ -94,6 +94,19 @@ test_expect_success PERL 'difftool honors --gui' ' restore_test_defaults ' +test_expect_success PERL 'difftool --gui last setting wins' ' + git config diff.guitool bogus-tool && + git difftool --no-prompt --gui --no-gui && + + git config merge.tool bogus-tool && + git config diff.tool bogus-tool && + git config diff.guitool test-tool && + diff=$(git difftool --no-prompt --no-gui --gui branch) && + test "$diff" = "branch" && + + restore_test_defaults +' + test_expect_success PERL 'difftool --gui works without configured diff.guitool' ' git config diff.tool test-tool && From 283607694c1248a8e1ff4cda48aa5d1eb1287733 Mon Sep 17 00:00:00 2001 From: Tim Henigan Date: Thu, 22 Mar 2012 15:52:18 -0400 Subject: [PATCH 3/8] difftool: exit(0) when usage is printed Prior to this commit, the script exited with an error whenever the usage string was printed, regardless of the reason it was done. In cases where usage was printed due to a user request (e.g. '-h' option), the script should exit without error (exit 0). This commit adds an argument to the usage function that allows the exit code to be specified when the function is called. Signed-off-by: Tim Henigan Signed-off-by: Junio C Hamano --- git-difftool.perl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/git-difftool.perl b/git-difftool.perl index 93e84e8d3..4555cd8b1 100755 --- a/git-difftool.perl +++ b/git-difftool.perl @@ -20,6 +20,7 @@ sub usage { + my $exitcode = shift; print << 'USAGE'; usage: git difftool [-t|--tool=] [-x|--extcmd=] @@ -27,7 +28,7 @@ sub usage [--prompt] [-y|--no-prompt] ['git diff' options] USAGE - exit 1; + exit($exitcode); } sub setup_environment @@ -58,14 +59,14 @@ sub exe 'x|extcmd:s' => \$extcmd); if (defined($help)) { - usage(); + usage(0); } if (defined($difftool_cmd)) { if (length($difftool_cmd) > 0) { $ENV{GIT_DIFF_TOOL} = $difftool_cmd; } else { print "No given for --tool=\n"; - usage(); + usage(1); } } if (defined($extcmd)) { @@ -73,7 +74,7 @@ sub exe $ENV{GIT_DIFFTOOL_EXTCMD} = $extcmd; } else { print "No given for --extcmd=\n"; - usage(); + usage(1); } } if ($gui) { From db607087d0f1256b745b9a878623060d775f1817 Mon Sep 17 00:00:00 2001 From: Tim Henigan Date: Thu, 22 Mar 2012 15:52:19 -0400 Subject: [PATCH 4/8] difftool: remove explicit change of PATH Adding the script directory to PATH is not needed. The script is located at '$(git --exec-path)', which is already on the PATH. Signed-off-by: Tim Henigan Signed-off-by: Junio C Hamano --- git-difftool.perl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/git-difftool.perl b/git-difftool.perl index 4555cd8b1..a399f3d2a 100755 --- a/git-difftool.perl +++ b/git-difftool.perl @@ -13,8 +13,6 @@ use 5.008; use strict; use warnings; -use Cwd qw(abs_path); -use File::Basename qw(dirname); use Getopt::Long qw(:config pass_through); use Git; @@ -33,8 +31,6 @@ sub usage sub setup_environment { - my $DIR = abs_path(dirname($0)); - $ENV{PATH} = "$DIR:$ENV{PATH}"; $ENV{GIT_PAGER} = ''; $ENV{GIT_EXTERNAL_DIFF} = 'git-difftool--helper'; } From 11bf87076a686dbfe696dd21333486cab55e6fbb Mon Sep 17 00:00:00 2001 From: Tim Henigan Date: Thu, 22 Mar 2012 15:52:20 -0400 Subject: [PATCH 5/8] difftool: stop appending '.exe' to git The system call to Git works the same whether or not ".exe" is appended to "git". The extra code is not necessary. Signed-off-by: Tim Henigan Signed-off-by: Junio C Hamano --- git-difftool.perl | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/git-difftool.perl b/git-difftool.perl index a399f3d2a..a3ad38956 100755 --- a/git-difftool.perl +++ b/git-difftool.perl @@ -35,15 +35,6 @@ sub setup_environment $ENV{GIT_EXTERNAL_DIFF} = 'git-difftool--helper'; } -sub exe -{ - my $exe = shift; - if ($^O eq 'MSWin32' || $^O eq 'msys') { - return "$exe.exe"; - } - return $exe; -} - # parse command-line options. all unrecognized options and arguments # are passed through to the 'git diff' command. my ($difftool_cmd, $extcmd, $gui, $help, $prompt); @@ -89,7 +80,7 @@ sub exe } setup_environment(); -my @command = (exe('git'), 'diff', @ARGV); +my @command = ('git', 'diff', @ARGV); # ActiveState Perl for Win32 does not implement POSIX semantics of # exec* system call. It just spawns the given executable and finishes From e9653615fafcbac6109da99fac4fa66b0b432048 Mon Sep 17 00:00:00 2001 From: Tim Henigan Date: Thu, 22 Mar 2012 15:52:21 -0400 Subject: [PATCH 6/8] difftool: eliminate setup_environment function Removing this function shortens the code and makes it easier to read. Now all environment variables are set as part of procedural operation. Signed-off-by: Tim Henigan Signed-off-by: Junio C Hamano --- git-difftool.perl | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/git-difftool.perl b/git-difftool.perl index a3ad38956..aba3d2f8c 100755 --- a/git-difftool.perl +++ b/git-difftool.perl @@ -29,12 +29,6 @@ sub usage exit($exitcode); } -sub setup_environment -{ - $ENV{GIT_PAGER} = ''; - $ENV{GIT_EXTERNAL_DIFF} = 'git-difftool--helper'; -} - # parse command-line options. all unrecognized options and arguments # are passed through to the 'git diff' command. my ($difftool_cmd, $extcmd, $gui, $help, $prompt); @@ -79,7 +73,8 @@ sub setup_environment } } -setup_environment(); +$ENV{GIT_PAGER} = ''; +$ENV{GIT_EXTERNAL_DIFF} = 'git-difftool--helper'; my @command = ('git', 'diff', @ARGV); # ActiveState Perl for Win32 does not implement POSIX semantics of From 7e0abcec103b3649943b236881cf88e8fd6cf3a4 Mon Sep 17 00:00:00 2001 From: Tim Henigan Date: Mon, 23 Apr 2012 14:23:41 -0400 Subject: [PATCH 7/8] difftool: teach difftool to handle directory diffs When 'difftool' is called to compare a range of commits that modify more than one file, it opens a separate instance of the diff tool for each file that changed. The new '--dir-diff' option copies all the modified files to a temporary location and runs a directory diff on them in a single instance of the diff tool. Signed-off-by: Tim Henigan Signed-off-by: Junio C Hamano --- Documentation/git-difftool.txt | 6 + git-difftool--helper.sh | 19 ++- git-difftool.perl | 266 ++++++++++++++++++++++++++++++--- t/t7800-difftool.sh | 39 +++++ 4 files changed, 303 insertions(+), 27 deletions(-) diff --git a/Documentation/git-difftool.txt b/Documentation/git-difftool.txt index fe38f667f..aba5e76c5 100644 --- a/Documentation/git-difftool.txt +++ b/Documentation/git-difftool.txt @@ -19,6 +19,12 @@ linkgit:git-diff[1]. OPTIONS ------- +-d:: +--dir-diff:: + Copy the modified files to a temporary location and perform + a directory diff on them. This mode never prompts before + launching the diff tool. + -y:: --no-prompt:: Do not prompt before launching a diff tool. diff --git a/git-difftool--helper.sh b/git-difftool--helper.sh index e6558d101..3d0fe0cd9 100755 --- a/git-difftool--helper.sh +++ b/git-difftool--helper.sh @@ -73,9 +73,16 @@ then fi fi -# Launch the merge tool on each path provided by 'git diff' -while test $# -gt 6 -do - launch_merge_tool "$1" "$2" "$5" - shift 7 -done +if test -n "$GIT_DIFFTOOL_DIRDIFF" +then + LOCAL="$1" + REMOTE="$2" + run_merge_tool "$merge_tool" false +else + # Launch the merge tool on each path provided by 'git diff' + while test $# -gt 6 + do + launch_merge_tool "$1" "$2" "$5" + shift 7 + done +fi diff --git a/git-difftool.perl b/git-difftool.perl index aba3d2f8c..850ebbe57 100755 --- a/git-difftool.perl +++ b/git-difftool.perl @@ -1,21 +1,31 @@ -#!/usr/bin/env perl +#!/usr/bin/perl # Copyright (c) 2009, 2010 David Aguilar +# Copyright (c) 2012 Tim Henigan # # This is a wrapper around the GIT_EXTERNAL_DIFF-compatible # git-difftool--helper script. # # This script exports GIT_EXTERNAL_DIFF and GIT_PAGER for use by git. -# GIT_DIFFTOOL_NO_PROMPT, GIT_DIFFTOOL_PROMPT, and GIT_DIFF_TOOL -# are exported for use by git-difftool--helper. +# The GIT_DIFF* variables are exported for use by git-difftool--helper. # # Any arguments that are unknown to this script are forwarded to 'git diff'. use 5.008; use strict; use warnings; +use File::Basename qw(dirname); +use File::Copy; +use File::stat; +use File::Path qw(mkpath); +use File::Temp qw(tempdir); use Getopt::Long qw(:config pass_through); use Git; +my @working_tree; +my $rc; +my $repo = Git->repository(); +my $repo_path = $repo->repo_path(); + sub usage { my $exitcode = shift; @@ -24,15 +34,205 @@ sub usage [-x|--extcmd=] [-g|--gui] [--no-gui] [--prompt] [-y|--no-prompt] + [-d|--dir-diff] ['git diff' options] USAGE exit($exitcode); } +sub find_worktree +{ + # Git->repository->wc_path() does not honor changes to the working + # tree location made by $ENV{GIT_WORK_TREE} or the 'core.worktree' + # config variable. + my $worktree; + my $env_worktree = $ENV{GIT_WORK_TREE}; + my $core_worktree = Git::config('core.worktree'); + + if (defined($env_worktree) and (length($env_worktree) > 0)) { + $worktree = $env_worktree; + } elsif (defined($core_worktree) and (length($core_worktree) > 0)) { + $worktree = $core_worktree; + } else { + $worktree = $repo->wc_path(); + } + + return $worktree; +} + +my $workdir = find_worktree(); + +sub setup_dir_diff +{ + # Run the diff; exit immediately if no diff found + # 'Repository' and 'WorkingCopy' must be explicitly set to insure that + # if $GIT_DIR and $GIT_WORK_TREE are set in ENV, they are actually used + # by Git->repository->command*. + my $diffrepo = Git->repository(Repository => $repo_path, WorkingCopy => $workdir); + my $diffrtn = $diffrepo->command_oneline('diff', '--raw', '--no-abbrev', '-z', @ARGV); + exit(0) if (length($diffrtn) == 0); + + # Setup temp directories + my $tmpdir = tempdir('git-diffall.XXXXX', CLEANUP => 1, TMPDIR => 1); + my $ldir = "$tmpdir/left"; + my $rdir = "$tmpdir/right"; + mkpath($ldir) or die $!; + mkpath($rdir) or die $!; + + # Build index info for left and right sides of the diff + my $submodule_mode = '160000'; + my $symlink_mode = '120000'; + my $null_mode = '0' x 6; + my $null_sha1 = '0' x 40; + my $lindex = ''; + my $rindex = ''; + my %submodule; + my %symlink; + my @rawdiff = split('\0', $diffrtn); + + my $i = 0; + while ($i < $#rawdiff) { + if ($rawdiff[$i] =~ /^::/) { + print "Combined diff formats ('-c' and '--cc') are not supported in directory diff mode.\n"; + exit(1); + } + + my ($lmode, $rmode, $lsha1, $rsha1, $status) = split(' ', substr($rawdiff[$i], 1)); + my $src_path = $rawdiff[$i + 1]; + my $dst_path; + + if ($status =~ /^[CR]/) { + $dst_path = $rawdiff[$i + 2]; + $i += 3; + } else { + $dst_path = $src_path; + $i += 2; + } + + if (($lmode eq $submodule_mode) or ($rmode eq $submodule_mode)) { + $submodule{$src_path}{left} = $lsha1; + if ($lsha1 ne $rsha1) { + $submodule{$dst_path}{right} = $rsha1; + } else { + $submodule{$dst_path}{right} = "$rsha1-dirty"; + } + next; + } + + if ($lmode eq $symlink_mode) { + $symlink{$src_path}{left} = $diffrepo->command_oneline('show', "$lsha1"); + } + + if ($rmode eq $symlink_mode) { + $symlink{$dst_path}{right} = $diffrepo->command_oneline('show', "$rsha1"); + } + + if (($lmode ne $null_mode) and ($status !~ /^C/)) { + $lindex .= "$lmode $lsha1\t$src_path\0"; + } + + if ($rmode ne $null_mode) { + if ($rsha1 ne $null_sha1) { + $rindex .= "$rmode $rsha1\t$dst_path\0"; + } else { + push(@working_tree, $dst_path); + } + } + } + + # If $GIT_DIR is not set prior to calling 'git update-index' and + # 'git checkout-index', then those commands will fail if difftool + # is called from a directory other than the repo root. + my $must_unset_git_dir = 0; + if (not defined($ENV{GIT_DIR})) { + $must_unset_git_dir = 1; + $ENV{GIT_DIR} = $repo_path; + } + + # Populate the left and right directories based on each index file + my ($inpipe, $ctx); + $ENV{GIT_INDEX_FILE} = "$tmpdir/lindex"; + ($inpipe, $ctx) = $repo->command_input_pipe(qw/update-index -z --index-info/); + print($inpipe $lindex); + $repo->command_close_pipe($inpipe, $ctx); + $rc = system('git', 'checkout-index', '--all', "--prefix=$ldir/"); + exit($rc | ($rc >> 8)) if ($rc != 0); + + $ENV{GIT_INDEX_FILE} = "$tmpdir/rindex"; + ($inpipe, $ctx) = $repo->command_input_pipe(qw/update-index -z --index-info/); + print($inpipe $rindex); + $repo->command_close_pipe($inpipe, $ctx); + $rc = system('git', 'checkout-index', '--all', "--prefix=$rdir/"); + exit($rc | ($rc >> 8)) if ($rc != 0); + + # If $GIT_DIR was explicitly set just for the update/checkout + # commands, then it should be unset before continuing. + delete($ENV{GIT_DIR}) if ($must_unset_git_dir); + delete($ENV{GIT_INDEX_FILE}); + + # Changes in the working tree need special treatment since they are + # not part of the index + for my $file (@working_tree) { + my $dir = dirname($file); + unless (-d "$rdir/$dir") { + mkpath("$rdir/$dir") or die $!; + } + copy("$workdir/$file", "$rdir/$file") or die $!; + chmod(stat("$workdir/$file")->mode, "$rdir/$file") or die $!; + } + + # Changes to submodules require special treatment. This loop writes a + # temporary file to both the left and right directories to show the + # change in the recorded SHA1 for the submodule. + for my $path (keys %submodule) { + if (defined($submodule{$path}{left})) { + write_to_file("$ldir/$path", "Subproject commit $submodule{$path}{left}"); + } + if (defined($submodule{$path}{right})) { + write_to_file("$rdir/$path", "Subproject commit $submodule{$path}{right}"); + } + } + + # Symbolic links require special treatment. The standard "git diff" + # shows only the link itself, not the contents of the link target. + # This loop replicates that behavior. + for my $path (keys %symlink) { + if (defined($symlink{$path}{left})) { + write_to_file("$ldir/$path", $symlink{$path}{left}); + } + if (defined($symlink{$path}{right})) { + write_to_file("$rdir/$path", $symlink{$path}{right}); + } + } + + return ($ldir, $rdir); +} + +sub write_to_file +{ + my $path = shift; + my $value = shift; + + # Make sure the path to the file exists + my $dir = dirname($path); + unless (-d "$dir") { + mkpath("$dir") or die $!; + } + + # If the file already exists in that location, delete it. This + # is required in the case of symbolic links. + unlink("$path"); + + open(my $fh, '>', "$path") or die $!; + print($fh $value); + close($fh); +} + # parse command-line options. all unrecognized options and arguments # are passed through to the 'git diff' command. -my ($difftool_cmd, $extcmd, $gui, $help, $prompt); +my ($difftool_cmd, $dirdiff, $extcmd, $gui, $help, $prompt); GetOptions('g|gui!' => \$gui, + 'd|dir-diff' => \$dirdiff, 'h' => \$help, 'prompt!' => \$prompt, 'y' => sub { $prompt = 0; }, @@ -59,28 +259,52 @@ sub usage } } if ($gui) { - my $guitool = ""; + my $guitool = ''; $guitool = Git::config('diff.guitool'); if (length($guitool) > 0) { $ENV{GIT_DIFF_TOOL} = $guitool; } } -if (defined($prompt)) { - if ($prompt) { - $ENV{GIT_DIFFTOOL_PROMPT} = 'true'; + +# In directory diff mode, 'git-difftool--helper' is called once +# to compare the a/b directories. In file diff mode, 'git diff' +# will invoke a separate instance of 'git-difftool--helper' for +# each file that changed. +if (defined($dirdiff)) { + my ($a, $b) = setup_dir_diff(); + if (defined($extcmd)) { + $rc = system($extcmd, $a, $b); } else { - $ENV{GIT_DIFFTOOL_NO_PROMPT} = 'true'; + $ENV{GIT_DIFFTOOL_DIRDIFF} = 'true'; + $rc = system('git', 'difftool--helper', $a, $b); } -} -$ENV{GIT_PAGER} = ''; -$ENV{GIT_EXTERNAL_DIFF} = 'git-difftool--helper'; -my @command = ('git', 'diff', @ARGV); - -# ActiveState Perl for Win32 does not implement POSIX semantics of -# exec* system call. It just spawns the given executable and finishes -# the starting program, exiting with code 0. -# system will at least catch the errors returned by git diff, -# allowing the caller of git difftool better handling of failures. -my $rc = system(@command); -exit($rc | ($rc >> 8)); + exit($rc | ($rc >> 8)) if ($rc != 0); + + # If the diff including working copy files and those + # files were modified during the diff, then the changes + # should be copied back to the working tree + for my $file (@working_tree) { + copy("$b/$file", "$workdir/$file") or die $!; + chmod(stat("$b/$file")->mode, "$workdir/$file") or die $!; + } +} else { + if (defined($prompt)) { + if ($prompt) { + $ENV{GIT_DIFFTOOL_PROMPT} = 'true'; + } else { + $ENV{GIT_DIFFTOOL_NO_PROMPT} = 'true'; + } + } + + $ENV{GIT_PAGER} = ''; + $ENV{GIT_EXTERNAL_DIFF} = 'git-difftool--helper'; + + # ActiveState Perl for Win32 does not implement POSIX semantics of + # exec* system call. It just spawns the given executable and finishes + # the starting program, exiting with code 0. + # system will at least catch the errors returned by git diff, + # allowing the caller of git difftool better handling of failures. + my $rc = system('git', 'diff', @ARGV); + exit($rc | ($rc >> 8)); +} diff --git a/t/t7800-difftool.sh b/t/t7800-difftool.sh index e716d066c..478c1bef3 100755 --- a/t/t7800-difftool.sh +++ b/t/t7800-difftool.sh @@ -319,4 +319,43 @@ test_expect_success PERL 'say no to the second file' ' echo "$diff" | stdin_doesnot_contain br2 ' +test_expect_success PERL 'setup change in subdirectory' ' + git checkout master && + mkdir sub && + echo master >sub/sub && + git add sub/sub && + git commit -m "added sub/sub" && + echo test >>file && + echo test >>sub/sub && + git add . && + git commit -m "modified both" +' + +test_expect_success PERL 'difftool -d' ' + diff=$(git difftool -d --extcmd ls branch) && + echo "$diff" | stdin_contains sub && + echo "$diff" | stdin_contains file +' + +test_expect_success PERL 'difftool --dir-diff' ' + diff=$(git difftool --dir-diff --extcmd ls branch) && + echo "$diff" | stdin_contains sub && + echo "$diff" | stdin_contains file +' + +test_expect_success PERL 'difftool --dir-diff ignores --prompt' ' + diff=$(git difftool --dir-diff --prompt --extcmd ls branch) && + echo "$diff" | stdin_contains sub && + echo "$diff" | stdin_contains file +' + +test_expect_success PERL 'difftool --dir-diff from subdirectory' ' + ( + cd sub && + diff=$(git difftool --dir-diff --extcmd ls branch) && + echo "$diff" | stdin_contains sub && + echo "$diff" | stdin_contains file + ) +' + test_done From bf73fc212a159210398b6d46ed5e9101c650e7db Mon Sep 17 00:00:00 2001 From: Tim Henigan Date: Thu, 29 Mar 2012 09:39:18 -0400 Subject: [PATCH 8/8] difftool: print list of valid tools with '--tool-help' Since bc7a96a (mergetool--lib: Refactor tools into separate files, 2011-08-18), it is possible to add a new diff tool by creating a simple script in the '$(git --exec-path)/mergetools' directory. Updating the difftool help text is still a manual process, and the documentation can easily go out of sync. This commit teaches difftool the '--tool-help' option, which: - Reads the list of valid tools from 'mergetools/*' - Determines which of them are actually installed - Determines which are capable of diffing (i.e. not just a merge tool) - Prints the resulting list for the user Signed-off-by: Tim Henigan Signed-off-by: Junio C Hamano --- Documentation/git-difftool.txt | 11 +++---- git-difftool.perl | 55 ++++++++++++++++++++++++++++++++-- t/t7800-difftool.sh | 5 ++++ 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/Documentation/git-difftool.txt b/Documentation/git-difftool.txt index aba5e76c5..31fc2e3ae 100644 --- a/Documentation/git-difftool.txt +++ b/Documentation/git-difftool.txt @@ -36,11 +36,9 @@ OPTIONS -t :: --tool=:: - Use the diff tool specified by . - Valid diff tools are: - araxis, bc3, deltawalker, diffuse, emerge, ecmerge, gvimdiff, - kdiff3, kompare, meld, opendiff, p4merge, tkdiff, vimdiff and - xxdiff. + Use the diff tool specified by . Valid values include + emerge, kompare, meld, and vimdiff. Run `git difftool --tool-help` + for the list of valid settings. + If a diff tool is not specified, 'git difftool' will use the configuration variable `diff.tool`. If the @@ -68,6 +66,9 @@ of the diff post-image. `$MERGED` is the name of the file which is being compared. `$BASE` is provided for compatibility with custom merge tool commands and has the same value as `$MERGED`. +--tool-help:: + Print a list of diff tools that may be used with `--tool`. + -x :: --extcmd=:: Specify a custom command for viewing diffs. diff --git a/git-difftool.perl b/git-difftool.perl index 850ebbe57..ae1e0525d 100755 --- a/git-difftool.perl +++ b/git-difftool.perl @@ -15,12 +15,14 @@ use warnings; use File::Basename qw(dirname); use File::Copy; +use File::Find; use File::stat; use File::Path qw(mkpath); use File::Temp qw(tempdir); use Getopt::Long qw(:config pass_through); use Git; +my @tools; my @working_tree; my $rc; my $repo = Git->repository(); @@ -30,7 +32,7 @@ sub usage { my $exitcode = shift; print << 'USAGE'; -usage: git difftool [-t|--tool=] +usage: git difftool [-t|--tool=] [--tool-help] [-x|--extcmd=] [-g|--gui] [--no-gui] [--prompt] [-y|--no-prompt] @@ -62,6 +64,51 @@ sub find_worktree my $workdir = find_worktree(); +sub filter_tool_scripts +{ + if (-d $_) { + if ($_ ne ".") { + # Ignore files in subdirectories + $File::Find::prune = 1; + } + } else { + if ((-f $_) && ($_ ne "defaults")) { + push(@tools, $_); + } + } +} + +sub print_tool_help +{ + my ($cmd, @found, @notfound); + my $gitpath = Git::exec_path(); + + find(\&filter_tool_scripts, "$gitpath/mergetools"); + + foreach my $tool (@tools) { + $cmd = "TOOL_MODE=diff"; + $cmd .= ' && . "$(git --exec-path)/git-mergetool--lib"'; + $cmd .= " && get_merge_tool_path $tool >/dev/null 2>&1"; + $cmd .= " && can_diff >/dev/null 2>&1"; + if (system('sh', '-c', $cmd) == 0) { + push(@found, $tool); + } else { + push(@notfound, $tool); + } + } + + print "'git difftool --tool=' may be set to one of the following:\n"; + print "\t$_\n" for (sort(@found)); + + print "\nThe following tools are valid, but not currently available:\n"; + print "\t$_\n" for (sort(@notfound)); + + print "\nNOTE: Some of the tools listed above only work in a windowed\n"; + print "environment. If run in a terminal-only session, they will fail.\n"; + + exit(0); +} + sub setup_dir_diff { # Run the diff; exit immediately if no diff found @@ -230,18 +277,22 @@ sub write_to_file # parse command-line options. all unrecognized options and arguments # are passed through to the 'git diff' command. -my ($difftool_cmd, $dirdiff, $extcmd, $gui, $help, $prompt); +my ($difftool_cmd, $dirdiff, $extcmd, $gui, $help, $prompt, $tool_help); GetOptions('g|gui!' => \$gui, 'd|dir-diff' => \$dirdiff, 'h' => \$help, 'prompt!' => \$prompt, 'y' => sub { $prompt = 0; }, 't|tool:s' => \$difftool_cmd, + 'tool-help' => \$tool_help, 'x|extcmd:s' => \$extcmd); if (defined($help)) { usage(0); } +if (defined($tool_help)) { + print_tool_help(); +} if (defined($difftool_cmd)) { if (length($difftool_cmd) > 0) { $ENV{GIT_DIFF_TOOL} = $difftool_cmd; diff --git a/t/t7800-difftool.sh b/t/t7800-difftool.sh index 478c1bef3..bbe71e5f7 100755 --- a/t/t7800-difftool.sh +++ b/t/t7800-difftool.sh @@ -319,6 +319,11 @@ test_expect_success PERL 'say no to the second file' ' echo "$diff" | stdin_doesnot_contain br2 ' +test_expect_success PERL 'difftool --tool-help' ' + tool_help=$(git difftool --tool-help) && + echo "$tool_help" | stdin_contains tool +' + test_expect_success PERL 'setup change in subdirectory' ' git checkout master && mkdir sub &&