Merge branch 'da/difftool-updates'

"git difftool --dir-diff" learned to use symbolic links to prepare
temporary copy of the working tree when available.

* da/difftool-updates:
  difftool: silence warning
  Add Code Compare v2.80.4 as a merge / diff tool for Windows
  mergetool,difftool: Document --tool-help consistently
  difftool: Disable --symlinks on cygwin
  difftool: Handle compare() returning -1
  difftool: Wrap long lines for readability
  difftool: Check all return codes from compare()
  difftool: Handle finding mergetools/ in a path with spaces
  difftool: Use symlinks when diffing against the worktree
  difftool: Call the temp directory "git-difftool"
  difftool: Move option values into a hash
  difftool: Eliminate global variables
  difftool: Simplify print_tool_help()
This commit is contained in:
Junio C Hamano 2012-08-27 11:55:16 -07:00
commit de54ef2724
6 changed files with 244 additions and 101 deletions

View File

@ -69,6 +69,14 @@ with custom merge tool commands and has the same value as `$MERGED`.
--tool-help:: --tool-help::
Print a list of diff tools that may be used with `--tool`. Print a list of diff tools that may be used with `--tool`.
--symlinks::
--no-symlinks::
'git difftool''s default behavior is create symlinks to the
working tree when run in `--dir-diff` mode.
+
Specifying `--no-symlinks` instructs 'git difftool' to create
copies instead. `--no-symlinks` is the default on Windows.
-x <command>:: -x <command>::
--extcmd=<command>:: --extcmd=<command>::
Specify a custom command for viewing diffs. Specify a custom command for viewing diffs.

View File

@ -64,6 +64,9 @@ variable `mergetool.<tool>.trustExitCode` can be set to `true`.
Otherwise, 'git mergetool' will prompt the user to indicate the Otherwise, 'git mergetool' will prompt the user to indicate the
success of the resolution after the custom tool has exited. success of the resolution after the custom tool has exited.
--tool-help::
Print a list of merge tools that may be used with `--tool`.
-y:: -y::
--no-prompt:: --no-prompt::
Don't prompt before each invocation of the merge resolution Don't prompt before each invocation of the merge resolution

View File

@ -1071,7 +1071,7 @@ _git_diff ()
} }
__git_mergetools_common="diffuse ecmerge emerge kdiff3 meld opendiff __git_mergetools_common="diffuse ecmerge emerge kdiff3 meld opendiff
tkdiff vimdiff gvimdiff xxdiff araxis p4merge bc3 tkdiff vimdiff gvimdiff xxdiff araxis p4merge bc3 codecompare
" "
_git_difftool () _git_difftool ()

View File

@ -18,17 +18,11 @@ use File::Copy;
use File::Compare; use File::Compare;
use File::Find; use File::Find;
use File::stat; use File::stat;
use File::Path qw(mkpath); use File::Path qw(mkpath rmtree);
use File::Temp qw(tempdir); use File::Temp qw(tempdir);
use Getopt::Long qw(:config pass_through); use Getopt::Long qw(:config pass_through);
use Git; use Git;
my @tools;
my @working_tree;
my $rc;
my $repo = Git->repository();
my $repo_path = $repo->repo_path();
sub usage sub usage
{ {
my $exitcode = shift; my $exitcode = shift;
@ -45,6 +39,8 @@ USAGE
sub find_worktree sub find_worktree
{ {
my ($repo) = @_;
# Git->repository->wc_path() does not honor changes to the working # Git->repository->wc_path() does not honor changes to the working
# tree location made by $ENV{GIT_WORK_TREE} or the 'core.worktree' # tree location made by $ENV{GIT_WORK_TREE} or the 'core.worktree'
# config variable. # config variable.
@ -63,10 +59,9 @@ sub find_worktree
return $worktree; return $worktree;
} }
my $workdir = find_worktree();
sub filter_tool_scripts sub filter_tool_scripts
{ {
my ($tools) = @_;
if (-d $_) { if (-d $_) {
if ($_ ne ".") { if ($_ ne ".") {
# Ignore files in subdirectories # Ignore files in subdirectories
@ -74,17 +69,17 @@ sub filter_tool_scripts
} }
} else { } else {
if ((-f $_) && ($_ ne "defaults")) { if ((-f $_) && ($_ ne "defaults")) {
push(@tools, $_); push(@$tools, $_);
} }
} }
} }
sub print_tool_help sub print_tool_help
{ {
my ($cmd, @found, @notfound); my ($cmd, @found, @notfound, @tools);
my $gitpath = Git::exec_path(); my $gitpath = Git::exec_path();
find(\&filter_tool_scripts, "$gitpath/mergetools"); find(sub { filter_tool_scripts(\@tools) }, "$gitpath/mergetools");
foreach my $tool (@tools) { foreach my $tool (@tools) {
$cmd = "TOOL_MODE=diff"; $cmd = "TOOL_MODE=diff";
@ -98,34 +93,52 @@ sub print_tool_help
} }
} }
print "'git difftool --tool=<tool>' may be set to one of the following:\n"; print << 'EOF';
'git difftool --tool=<tool>' may be set to one of the following:
EOF
print "\t$_\n" for (sort(@found)); print "\t$_\n" for (sort(@found));
print "\nThe following tools are valid, but not currently available:\n"; print << 'EOF';
The following tools are valid, but not currently available:
EOF
print "\t$_\n" for (sort(@notfound)); print "\t$_\n" for (sort(@notfound));
print "\nNOTE: Some of the tools listed above only work in a windowed\n"; print << 'EOF';
print "environment. If run in a terminal-only session, they will fail.\n";
NOTE: Some of the tools listed above only work in a windowed
environment. If run in a terminal-only session, they will fail.
EOF
exit(0); exit(0);
} }
sub exit_cleanup
{
my ($tmpdir, $status) = @_;
my $errno = $!;
rmtree($tmpdir);
if ($status and $errno) {
my ($package, $file, $line) = caller();
warn "$file line $line: $errno\n";
}
exit($status | ($status >> 8));
}
sub setup_dir_diff sub setup_dir_diff
{ {
my ($repo, $workdir, $symlinks) = @_;
# Run the diff; exit immediately if no diff found # Run the diff; exit immediately if no diff found
# 'Repository' and 'WorkingCopy' must be explicitly set to insure that # '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 # if $GIT_DIR and $GIT_WORK_TREE are set in ENV, they are actually used
# by Git->repository->command*. # by Git->repository->command*.
my $diffrepo = Git->repository(Repository => $repo_path, WorkingCopy => $workdir); my $repo_path = $repo->repo_path();
my $diffrtn = $diffrepo->command_oneline('diff', '--raw', '--no-abbrev', '-z', @ARGV); my %repo_args = (Repository => $repo_path, WorkingCopy => $workdir);
exit(0) if (length($diffrtn) == 0); my $diffrepo = Git->repository(%repo_args);
# Setup temp directories my @gitargs = ('diff', '--raw', '--no-abbrev', '-z', @ARGV);
my $tmpdir = tempdir('git-diffall.XXXXX', CLEANUP => 1, TMPDIR => 1); my $diffrtn = $diffrepo->command_oneline(@gitargs);
my $ldir = "$tmpdir/left"; exit(0) unless defined($diffrtn);
my $rdir = "$tmpdir/right";
mkpath($ldir) or die $!;
mkpath($rdir) or die $!;
# Build index info for left and right sides of the diff # Build index info for left and right sides of the diff
my $submodule_mode = '160000'; my $submodule_mode = '160000';
@ -136,16 +149,21 @@ sub setup_dir_diff
my $rindex = ''; my $rindex = '';
my %submodule; my %submodule;
my %symlink; my %symlink;
my @working_tree = ();
my @rawdiff = split('\0', $diffrtn); my @rawdiff = split('\0', $diffrtn);
my $i = 0; my $i = 0;
while ($i < $#rawdiff) { while ($i < $#rawdiff) {
if ($rawdiff[$i] =~ /^::/) { if ($rawdiff[$i] =~ /^::/) {
print "Combined diff formats ('-c' and '--cc') are not supported in directory diff mode.\n"; warn << 'EOF';
Combined diff formats ('-c' and '--cc') are not supported in
directory diff mode ('-d' and '--dir-diff').
EOF
exit(1); exit(1);
} }
my ($lmode, $rmode, $lsha1, $rsha1, $status) = split(' ', substr($rawdiff[$i], 1)); my ($lmode, $rmode, $lsha1, $rsha1, $status) =
split(' ', substr($rawdiff[$i], 1));
my $src_path = $rawdiff[$i + 1]; my $src_path = $rawdiff[$i + 1];
my $dst_path; my $dst_path;
@ -157,7 +175,7 @@ sub setup_dir_diff
$i += 2; $i += 2;
} }
if (($lmode eq $submodule_mode) or ($rmode eq $submodule_mode)) { if ($lmode eq $submodule_mode or $rmode eq $submodule_mode) {
$submodule{$src_path}{left} = $lsha1; $submodule{$src_path}{left} = $lsha1;
if ($lsha1 ne $rsha1) { if ($lsha1 ne $rsha1) {
$submodule{$dst_path}{right} = $rsha1; $submodule{$dst_path}{right} = $rsha1;
@ -168,14 +186,16 @@ sub setup_dir_diff
} }
if ($lmode eq $symlink_mode) { if ($lmode eq $symlink_mode) {
$symlink{$src_path}{left} = $diffrepo->command_oneline('show', "$lsha1"); $symlink{$src_path}{left} =
$diffrepo->command_oneline('show', "$lsha1");
} }
if ($rmode eq $symlink_mode) { if ($rmode eq $symlink_mode) {
$symlink{$dst_path}{right} = $diffrepo->command_oneline('show', "$rsha1"); $symlink{$dst_path}{right} =
$diffrepo->command_oneline('show', "$rsha1");
} }
if (($lmode ne $null_mode) and ($status !~ /^C/)) { if ($lmode ne $null_mode and $status !~ /^C/) {
$lindex .= "$lmode $lsha1\t$src_path\0"; $lindex .= "$lmode $lsha1\t$src_path\0";
} }
@ -188,6 +208,13 @@ sub setup_dir_diff
} }
} }
# Setup temp directories
my $tmpdir = tempdir('git-difftool.XXXXX', CLEANUP => 0, TMPDIR => 1);
my $ldir = "$tmpdir/left";
my $rdir = "$tmpdir/right";
mkpath($ldir) or exit_cleanup($tmpdir, 1);
mkpath($rdir) or exit_cleanup($tmpdir, 1);
# If $GIT_DIR is not set prior to calling 'git update-index' and # If $GIT_DIR is not set prior to calling 'git update-index' and
# 'git checkout-index', then those commands will fail if difftool # 'git checkout-index', then those commands will fail if difftool
# is called from a directory other than the repo root. # is called from a directory other than the repo root.
@ -200,18 +227,22 @@ sub setup_dir_diff
# Populate the left and right directories based on each index file # Populate the left and right directories based on each index file
my ($inpipe, $ctx); my ($inpipe, $ctx);
$ENV{GIT_INDEX_FILE} = "$tmpdir/lindex"; $ENV{GIT_INDEX_FILE} = "$tmpdir/lindex";
($inpipe, $ctx) = $repo->command_input_pipe(qw/update-index -z --index-info/); ($inpipe, $ctx) =
$repo->command_input_pipe(qw(update-index -z --index-info));
print($inpipe $lindex); print($inpipe $lindex);
$repo->command_close_pipe($inpipe, $ctx); $repo->command_close_pipe($inpipe, $ctx);
$rc = system('git', 'checkout-index', '--all', "--prefix=$ldir/");
exit($rc | ($rc >> 8)) if ($rc != 0); my $rc = system('git', 'checkout-index', '--all', "--prefix=$ldir/");
exit_cleanup($tmpdir, $rc) if $rc != 0;
$ENV{GIT_INDEX_FILE} = "$tmpdir/rindex"; $ENV{GIT_INDEX_FILE} = "$tmpdir/rindex";
($inpipe, $ctx) = $repo->command_input_pipe(qw/update-index -z --index-info/); ($inpipe, $ctx) =
$repo->command_input_pipe(qw(update-index -z --index-info));
print($inpipe $rindex); print($inpipe $rindex);
$repo->command_close_pipe($inpipe, $ctx); $repo->command_close_pipe($inpipe, $ctx);
$rc = system('git', 'checkout-index', '--all', "--prefix=$rdir/"); $rc = system('git', 'checkout-index', '--all', "--prefix=$rdir/");
exit($rc | ($rc >> 8)) if ($rc != 0); exit_cleanup($tmpdir, $rc) if $rc != 0;
# If $GIT_DIR was explicitly set just for the update/checkout # If $GIT_DIR was explicitly set just for the update/checkout
# commands, then it should be unset before continuing. # commands, then it should be unset before continuing.
@ -223,37 +254,55 @@ sub setup_dir_diff
for my $file (@working_tree) { for my $file (@working_tree) {
my $dir = dirname($file); my $dir = dirname($file);
unless (-d "$rdir/$dir") { unless (-d "$rdir/$dir") {
mkpath("$rdir/$dir") or die $!; mkpath("$rdir/$dir") or
exit_cleanup($tmpdir, 1);
}
if ($symlinks) {
symlink("$workdir/$file", "$rdir/$file") or
exit_cleanup($tmpdir, 1);
} else {
copy("$workdir/$file", "$rdir/$file") or
exit_cleanup($tmpdir, 1);
my $mode = stat("$workdir/$file")->mode;
chmod($mode, "$rdir/$file") or
exit_cleanup($tmpdir, 1);
} }
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 # Changes to submodules require special treatment. This loop writes a
# temporary file to both the left and right directories to show the # temporary file to both the left and right directories to show the
# change in the recorded SHA1 for the submodule. # change in the recorded SHA1 for the submodule.
for my $path (keys %submodule) { for my $path (keys %submodule) {
my $ok;
if (defined($submodule{$path}{left})) { if (defined($submodule{$path}{left})) {
write_to_file("$ldir/$path", "Subproject commit $submodule{$path}{left}"); $ok = write_to_file("$ldir/$path",
"Subproject commit $submodule{$path}{left}");
} }
if (defined($submodule{$path}{right})) { if (defined($submodule{$path}{right})) {
write_to_file("$rdir/$path", "Subproject commit $submodule{$path}{right}"); $ok = write_to_file("$rdir/$path",
"Subproject commit $submodule{$path}{right}");
} }
exit_cleanup($tmpdir, 1) if not $ok;
} }
# Symbolic links require special treatment. The standard "git diff" # Symbolic links require special treatment. The standard "git diff"
# shows only the link itself, not the contents of the link target. # shows only the link itself, not the contents of the link target.
# This loop replicates that behavior. # This loop replicates that behavior.
for my $path (keys %symlink) { for my $path (keys %symlink) {
my $ok;
if (defined($symlink{$path}{left})) { if (defined($symlink{$path}{left})) {
write_to_file("$ldir/$path", $symlink{$path}{left}); $ok = write_to_file("$ldir/$path",
$symlink{$path}{left});
} }
if (defined($symlink{$path}{right})) { if (defined($symlink{$path}{right})) {
write_to_file("$rdir/$path", $symlink{$path}{right}); $ok = write_to_file("$rdir/$path",
$symlink{$path}{right});
} }
exit_cleanup($tmpdir, 1) if not $ok;
} }
return ($ldir, $rdir); return ($ldir, $rdir, $tmpdir, @working_tree);
} }
sub write_to_file sub write_to_file
@ -264,55 +313,70 @@ sub write_to_file
# Make sure the path to the file exists # Make sure the path to the file exists
my $dir = dirname($path); my $dir = dirname($path);
unless (-d "$dir") { unless (-d "$dir") {
mkpath("$dir") or die $!; mkpath("$dir") or return 0;
} }
# If the file already exists in that location, delete it. This # If the file already exists in that location, delete it. This
# is required in the case of symbolic links. # is required in the case of symbolic links.
unlink("$path"); unlink($path);
open(my $fh, '>', "$path") or die $!; open(my $fh, '>', $path) or return 0;
print($fh $value); print($fh $value);
close($fh); close($fh);
return 1;
} }
sub main
{
# parse command-line options. all unrecognized options and arguments # parse command-line options. all unrecognized options and arguments
# are passed through to the 'git diff' command. # are passed through to the 'git diff' command.
my ($difftool_cmd, $dirdiff, $extcmd, $gui, $help, $prompt, $tool_help); my %opts = (
GetOptions('g|gui!' => \$gui, difftool_cmd => undef,
'd|dir-diff' => \$dirdiff, dirdiff => undef,
'h' => \$help, extcmd => undef,
'prompt!' => \$prompt, gui => undef,
'y' => sub { $prompt = 0; }, help => undef,
't|tool:s' => \$difftool_cmd, prompt => undef,
'tool-help' => \$tool_help, symlinks => $^O ne 'cygwin' &&
'x|extcmd:s' => \$extcmd); $^O ne 'MSWin32' && $^O ne 'msys',
tool_help => undef,
);
GetOptions('g|gui!' => \$opts{gui},
'd|dir-diff' => \$opts{dirdiff},
'h' => \$opts{help},
'prompt!' => \$opts{prompt},
'y' => sub { $opts{prompt} = 0; },
'symlinks' => \$opts{symlinks},
'no-symlinks' => sub { $opts{symlinks} = 0; },
't|tool:s' => \$opts{difftool_cmd},
'tool-help' => \$opts{tool_help},
'x|extcmd:s' => \$opts{extcmd});
if (defined($help)) { if (defined($opts{help})) {
usage(0); usage(0);
} }
if (defined($tool_help)) { if (defined($opts{tool_help})) {
print_tool_help(); print_tool_help();
} }
if (defined($difftool_cmd)) { if (defined($opts{difftool_cmd})) {
if (length($difftool_cmd) > 0) { if (length($opts{difftool_cmd}) > 0) {
$ENV{GIT_DIFF_TOOL} = $difftool_cmd; $ENV{GIT_DIFF_TOOL} = $opts{difftool_cmd};
} else { } else {
print "No <tool> given for --tool=<tool>\n"; print "No <tool> given for --tool=<tool>\n";
usage(1); usage(1);
} }
} }
if (defined($extcmd)) { if (defined($opts{extcmd})) {
if (length($extcmd) > 0) { if (length($opts{extcmd}) > 0) {
$ENV{GIT_DIFFTOOL_EXTCMD} = $extcmd; $ENV{GIT_DIFFTOOL_EXTCMD} = $opts{extcmd};
} else { } else {
print "No <cmd> given for --extcmd=<cmd>\n"; print "No <cmd> given for --extcmd=<cmd>\n";
usage(1); usage(1);
} }
} }
if ($gui) { if ($opts{gui}) {
my $guitool = ''; my $guitool = Git::config('diff.guitool');
$guitool = Git::config('diff.guitool');
if (length($guitool) > 0) { if (length($guitool) > 0) {
$ENV{GIT_DIFF_TOOL} = $guitool; $ENV{GIT_DIFF_TOOL} = $guitool;
} }
@ -322,27 +386,68 @@ if ($gui) {
# to compare the a/b directories. In file diff mode, 'git diff' # to compare the a/b directories. In file diff mode, 'git diff'
# will invoke a separate instance of 'git-difftool--helper' for # will invoke a separate instance of 'git-difftool--helper' for
# each file that changed. # each file that changed.
if (defined($dirdiff)) { if (defined($opts{dirdiff})) {
my ($a, $b) = setup_dir_diff(); dir_diff($opts{extcmd}, $opts{symlinks});
} else {
file_diff($opts{prompt});
}
}
sub dir_diff
{
my ($extcmd, $symlinks) = @_;
my $rc;
my $error = 0;
my $repo = Git->repository();
my $workdir = find_worktree($repo);
my ($a, $b, $tmpdir, @worktree) =
setup_dir_diff($repo, $workdir, $symlinks);
if (defined($extcmd)) { if (defined($extcmd)) {
$rc = system($extcmd, $a, $b); $rc = system($extcmd, $a, $b);
} else { } else {
$ENV{GIT_DIFFTOOL_DIRDIFF} = 'true'; $ENV{GIT_DIFFTOOL_DIRDIFF} = 'true';
$rc = system('git', 'difftool--helper', $a, $b); $rc = system('git', 'difftool--helper', $a, $b);
} }
exit($rc | ($rc >> 8)) if ($rc != 0);
# If the diff including working copy files and those # If the diff including working copy files and those
# files were modified during the diff, then the changes # files were modified during the diff, then the changes
# should be copied back to the working tree # should be copied back to the working tree.
for my $file (@working_tree) { # Do not copy back files when symlinks are used and the
if (-e "$b/$file" && compare("$b/$file", "$workdir/$file")) { # external tool did not replace the original link with a file.
copy("$b/$file", "$workdir/$file") or die $!; for my $file (@worktree) {
chmod(stat("$b/$file")->mode, "$workdir/$file") or die $!; next if $symlinks && -l "$b/$file";
next if ! -f "$b/$file";
my $diff = compare("$b/$file", "$workdir/$file");
if ($diff == 0) {
next;
} elsif ($diff == -1) {
my $errmsg = "warning: Could not compare ";
$errmsg += "'$b/$file' with '$workdir/$file'\n";
warn $errmsg;
$error = 1;
} elsif ($diff == 1) {
my $mode = stat("$b/$file")->mode;
copy("$b/$file", "$workdir/$file") or
exit_cleanup($tmpdir, 1);
chmod($mode, "$workdir/$file") or
exit_cleanup($tmpdir, 1);
} }
} }
if ($error) {
warn "warning: Temporary files exist in '$tmpdir'.\n";
warn "warning: You may want to cleanup or recover these.\n";
exit(1);
} else { } else {
exit_cleanup($tmpdir, $rc);
}
}
sub file_diff
{
my ($prompt) = @_;
if (defined($prompt)) { if (defined($prompt)) {
if ($prompt) { if ($prompt) {
$ENV{GIT_DIFFTOOL_PROMPT} = 'true'; $ENV{GIT_DIFFTOOL_PROMPT} = 'true';
@ -362,3 +467,5 @@ if (defined($dirdiff)) {
my $rc = system('git', 'diff', @ARGV); my $rc = system('git', 'diff', @ARGV);
exit($rc | ($rc >> 8)); exit($rc | ($rc >> 8));
} }
main();

View File

@ -126,7 +126,7 @@ list_merge_tool_candidates () {
else else
tools="opendiff kdiff3 tkdiff xxdiff meld $tools" tools="opendiff kdiff3 tkdiff xxdiff meld $tools"
fi fi
tools="$tools gvimdiff diffuse ecmerge p4merge araxis bc3" tools="$tools gvimdiff diffuse ecmerge p4merge araxis bc3 codecompare"
fi fi
case "${VISUAL:-$EDITOR}" in case "${VISUAL:-$EDITOR}" in
*vim*) *vim*)

25
mergetools/codecompare Normal file
View File

@ -0,0 +1,25 @@
diff_cmd () {
"$merge_tool_path" "$LOCAL" "$REMOTE"
}
merge_cmd () {
touch "$BACKUP"
if $base_present
then
"$merge_tool_path" -MF="$LOCAL" -TF="$REMOTE" -BF="$BASE" \
-RF="$MERGED"
else
"$merge_tool_path" -MF="$LOCAL" -TF="$REMOTE" \
-RF="$MERGED"
fi
check_unchanged
}
translate_merge_tool_path() {
if merge_mode
then
echo CodeMerge
else
echo CodeCompare
fi
}