From: Johannes Schindelin Date: Thu, 19 Jan 2017 20:30:40 +0000 (+0100) Subject: difftool: retire the scripted version X-Git-Tag: v2.12.0-rc0~28^2~1 X-Git-Url: https://git.lorimer.id.au/gitweb.git/diff_plain/019678d6b1f83419cdc35e3d455175adce3a379c?hp=--cc difftool: retire the scripted version It served its purpose, but now we have a builtin difftool. Time for the Perl script to enjoy Florida. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- 019678d6b1f83419cdc35e3d455175adce3a379c diff --git a/.gitignore b/.gitignore index f96e50ed40..05cb58a3d4 100644 --- a/.gitignore +++ b/.gitignore @@ -76,7 +76,6 @@ /git-init-db /git-interpret-trailers /git-instaweb -/git-legacy-difftool /git-log /git-ls-files /git-ls-remote diff --git a/Makefile b/Makefile index 7863bc2f3d..28b6440b3c 100644 --- a/Makefile +++ b/Makefile @@ -527,7 +527,6 @@ SCRIPT_LIB += git-sh-setup SCRIPT_LIB += git-sh-i18n SCRIPT_PERL += git-add--interactive.perl -SCRIPT_PERL += git-legacy-difftool.perl SCRIPT_PERL += git-archimport.perl SCRIPT_PERL += git-cvsexportcommit.perl SCRIPT_PERL += git-cvsimport.perl diff --git a/builtin/difftool.c b/builtin/difftool.c index 2115e548a5..42ad9e804a 100644 --- a/builtin/difftool.c +++ b/builtin/difftool.c @@ -616,30 +616,6 @@ static int run_file_diff(int prompt, const char *prefix, exit(ret); } -/* - * NEEDSWORK: this function can go once the legacy-difftool Perl script is - * retired. - * - * We intentionally avoid reading the config directly here, to avoid messing up - * the GIT_* environment variables when we need to fall back to exec()ing the - * Perl script. - */ -static int use_builtin_difftool(void) { - struct child_process cp = CHILD_PROCESS_INIT; - struct strbuf out = STRBUF_INIT; - int ret; - - argv_array_pushl(&cp.args, - "config", "--bool", "difftool.usebuiltin", NULL); - cp.git_cmd = 1; - if (capture_command(&cp, &out, 6)) - return 0; - strbuf_trim(&out); - ret = !strcmp("true", out.buf); - strbuf_release(&out); - return ret; -} - int cmd_difftool(int argc, const char **argv, const char *prefix) { int use_gui_tool = 0, dir_diff = 0, prompt = -1, symlinks = 0, @@ -671,23 +647,6 @@ int cmd_difftool(int argc, const char **argv, const char *prefix) OPT_END() }; - /* - * NEEDSWORK: Once the builtin difftool has been tested enough - * and git-legacy-difftool.perl is retired to contrib/, this preamble - * can be removed. - */ - if (!use_builtin_difftool()) { - const char *path = mkpath("%s/git-legacy-difftool", - git_exec_path()); - - if (sane_execvp(path, (char **)argv) < 0) - die_errno("could not exec %s", path); - - return 0; - } - prefix = setup_git_directory(); - trace_repo_setup(prefix); - setup_work_tree(); /* NEEDSWORK: once we no longer spawn anything, remove this */ setenv(GIT_DIR_ENVIRONMENT, absolute_path(get_git_dir()), 1); setenv(GIT_WORK_TREE_ENVIRONMENT, absolute_path(get_git_work_tree()), 1); diff --git a/contrib/examples/git-difftool.perl b/contrib/examples/git-difftool.perl new file mode 100755 index 0000000000..a5790d03a0 --- /dev/null +++ b/contrib/examples/git-difftool.perl @@ -0,0 +1,470 @@ +#!/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. +# 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 Error qw(:try); +use File::Basename qw(dirname); +use File::Copy; +use File::Find; +use File::stat; +use File::Path qw(mkpath rmtree); +use File::Temp qw(tempdir); +use Getopt::Long qw(:config pass_through); +use Git; + +sub usage +{ + my $exitcode = shift; + print << 'USAGE'; +usage: git difftool [-t|--tool=] [--tool-help] + [-x|--extcmd=] + [-g|--gui] [--no-gui] + [--prompt] [-y|--no-prompt] + [-d|--dir-diff] + ['git diff' options] +USAGE + exit($exitcode); +} + +sub print_tool_help +{ + # See the comment at the bottom of file_diff() for the reason behind + # using system() followed by exit() instead of exec(). + my $rc = system(qw(git mergetool --tool-help=diff)); + exit($rc | ($rc >> 8)); +} + +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 use_wt_file +{ + my ($workdir, $file, $sha1) = @_; + my $null_sha1 = '0' x 40; + + if (-l "$workdir/$file" || ! -e _) { + return (0, $null_sha1); + } + + my $wt_sha1 = Git::command_oneline('hash-object', "$workdir/$file"); + my $use = ($sha1 eq $null_sha1) || ($sha1 eq $wt_sha1); + return ($use, $wt_sha1); +} + +sub changed_files +{ + my ($repo_path, $index, $worktree) = @_; + $ENV{GIT_INDEX_FILE} = $index; + + my @gitargs = ('--git-dir', $repo_path, '--work-tree', $worktree); + my @refreshargs = ( + @gitargs, 'update-index', + '--really-refresh', '-q', '--unmerged'); + try { + Git::command_oneline(@refreshargs); + } catch Git::Error::Command with {}; + + my @diffargs = (@gitargs, 'diff-files', '--name-only', '-z'); + my $line = Git::command_oneline(@diffargs); + my @files; + if (defined $line) { + @files = split('\0', $line); + } else { + @files = (); + } + + delete($ENV{GIT_INDEX_FILE}); + + return map { $_ => 1 } @files; +} + +sub setup_dir_diff +{ + my ($workdir, $symlinks) = @_; + my @gitargs = ('diff', '--raw', '--no-abbrev', '-z', @ARGV); + my $diffrtn = Git::command_oneline(@gitargs); + exit(0) unless defined($diffrtn); + + # 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 $wtindex = ''; + my %submodule; + my %symlink; + my @working_tree = (); + my %working_tree_dups = (); + my @rawdiff = split('\0', $diffrtn); + + my $i = 0; + while ($i < $#rawdiff) { + if ($rawdiff[$i] =~ /^::/) { + warn << 'EOF'; +Combined diff formats ('-c' and '--cc') are not supported in +directory diff mode ('-d' and '--dir-diff'). +EOF + 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} = + Git::command_oneline('show', $lsha1); + } + + if ($rmode eq $symlink_mode) { + $symlink{$dst_path}{right} = + Git::command_oneline('show', $rsha1); + } + + if ($lmode ne $null_mode and $status !~ /^C/) { + $lindex .= "$lmode $lsha1\t$src_path\0"; + } + + if ($rmode ne $null_mode) { + # Avoid duplicate working_tree entries + if ($working_tree_dups{$dst_path}++) { + next; + } + my ($use, $wt_sha1) = + use_wt_file($workdir, $dst_path, $rsha1); + if ($use) { + push @working_tree, $dst_path; + $wtindex .= "$rmode $wt_sha1\t$dst_path\0"; + } else { + $rindex .= "$rmode $rsha1\t$dst_path\0"; + } + } + } + + # 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); + + # Populate the left and right directories based on each index file + my ($inpipe, $ctx); + $ENV{GIT_INDEX_FILE} = "$tmpdir/lindex"; + ($inpipe, $ctx) = + Git::command_input_pipe('update-index', '-z', '--index-info'); + print($inpipe $lindex); + Git::command_close_pipe($inpipe, $ctx); + + my $rc = system('git', 'checkout-index', '--all', "--prefix=$ldir/"); + exit_cleanup($tmpdir, $rc) if $rc != 0; + + $ENV{GIT_INDEX_FILE} = "$tmpdir/rindex"; + ($inpipe, $ctx) = + Git::command_input_pipe('update-index', '-z', '--index-info'); + print($inpipe $rindex); + Git::command_close_pipe($inpipe, $ctx); + + $rc = system('git', 'checkout-index', '--all', "--prefix=$rdir/"); + exit_cleanup($tmpdir, $rc) if $rc != 0; + + $ENV{GIT_INDEX_FILE} = "$tmpdir/wtindex"; + ($inpipe, $ctx) = + Git::command_input_pipe('update-index', '--info-only', '-z', '--index-info'); + print($inpipe $wtindex); + Git::command_close_pipe($inpipe, $ctx); + + # If $GIT_DIR was explicitly set just for the update/checkout + # commands, then it should be unset before continuing. + delete($ENV{GIT_INDEX_FILE}); + + # Changes in the working tree need special treatment since they are + # not part of the index. Remove any trailing slash from $workdir + # before starting to avoid double slashes in symlink targets. + $workdir =~ s|/$||; + for my $file (@working_tree) { + my $dir = dirname($file); + unless (-d "$rdir/$dir") { + 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); + } + } + + # 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) { + my $ok = 0; + if (defined($submodule{$path}{left})) { + $ok = write_to_file("$ldir/$path", + "Subproject commit $submodule{$path}{left}"); + } + if (defined($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" + # shows only the link itself, not the contents of the link target. + # This loop replicates that behavior. + for my $path (keys %symlink) { + my $ok = 0; + if (defined($symlink{$path}{left})) { + $ok = write_to_file("$ldir/$path", + $symlink{$path}{left}); + } + if (defined($symlink{$path}{right})) { + $ok = write_to_file("$rdir/$path", + $symlink{$path}{right}); + } + exit_cleanup($tmpdir, 1) if not $ok; + } + + return ($ldir, $rdir, $tmpdir, @working_tree); +} + +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 return 0; + } + + # 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 return 0; + print($fh $value); + close($fh); + + return 1; +} + +sub main +{ + # parse command-line options. all unrecognized options and arguments + # are passed through to the 'git diff' command. + my %opts = ( + difftool_cmd => undef, + dirdiff => undef, + extcmd => undef, + gui => undef, + help => undef, + prompt => undef, + symlinks => $^O ne 'cygwin' && + $^O ne 'MSWin32' && $^O ne 'msys', + tool_help => undef, + trust_exit_code => 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}, + 'trust-exit-code' => \$opts{trust_exit_code}, + 'no-trust-exit-code' => sub { $opts{trust_exit_code} = 0; }, + 'x|extcmd:s' => \$opts{extcmd}); + + if (defined($opts{help})) { + usage(0); + } + if (defined($opts{tool_help})) { + print_tool_help(); + } + if (defined($opts{difftool_cmd})) { + if (length($opts{difftool_cmd}) > 0) { + $ENV{GIT_DIFF_TOOL} = $opts{difftool_cmd}; + } else { + print "No given for --tool=\n"; + usage(1); + } + } + if (defined($opts{extcmd})) { + if (length($opts{extcmd}) > 0) { + $ENV{GIT_DIFFTOOL_EXTCMD} = $opts{extcmd}; + } else { + print "No given for --extcmd=\n"; + usage(1); + } + } + if ($opts{gui}) { + my $guitool = Git::config('diff.guitool'); + if (defined($guitool) && length($guitool) > 0) { + $ENV{GIT_DIFF_TOOL} = $guitool; + } + } + + if (!defined $opts{trust_exit_code}) { + $opts{trust_exit_code} = Git::config_bool('difftool.trustExitCode'); + } + if ($opts{trust_exit_code}) { + $ENV{GIT_DIFFTOOL_TRUST_EXIT_CODE} = 'true'; + } else { + $ENV{GIT_DIFFTOOL_TRUST_EXIT_CODE} = 'false'; + } + + # 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($opts{dirdiff})) { + 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 $repo_path = $repo->repo_path(); + my $workdir = $repo->wc_path(); + my ($a, $b, $tmpdir, @worktree) = setup_dir_diff($workdir, $symlinks); + + if (defined($extcmd)) { + $rc = system($extcmd, $a, $b); + } else { + $ENV{GIT_DIFFTOOL_DIRDIFF} = 'true'; + $rc = system('git', 'difftool--helper', $a, $b); + } + # 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. + # Do not copy back files when symlinks are used and the + # external tool did not replace the original link with a file. + # + # These hashes are loaded lazily since they aren't needed + # in the common case of --symlinks and the difftool updating + # files through the symlink. + my %wt_modified; + my %tmp_modified; + my $indices_loaded = 0; + + for my $file (@worktree) { + next if $symlinks && -l "$b/$file"; + next if ! -f "$b/$file"; + + if (!$indices_loaded) { + %wt_modified = changed_files( + $repo_path, "$tmpdir/wtindex", $workdir); + %tmp_modified = changed_files( + $repo_path, "$tmpdir/wtindex", $b); + $indices_loaded = 1; + } + + if (exists $wt_modified{$file} and exists $tmp_modified{$file}) { + my $errmsg = "warning: Both files modified: "; + $errmsg .= "'$workdir/$file' and '$b/$file'.\n"; + $errmsg .= "warning: Working tree file has been left.\n"; + $errmsg .= "warning:\n"; + warn $errmsg; + $error = 1; + } elsif (exists $tmp_modified{$file}) { + 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 { + exit_cleanup($tmpdir, $rc); + } +} + +sub file_diff +{ + my ($prompt) = @_; + + 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)); +} + +main(); diff --git a/git-legacy-difftool.perl b/git-legacy-difftool.perl deleted file mode 100755 index a5790d03a0..0000000000 --- a/git-legacy-difftool.perl +++ /dev/null @@ -1,470 +0,0 @@ -#!/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. -# 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 Error qw(:try); -use File::Basename qw(dirname); -use File::Copy; -use File::Find; -use File::stat; -use File::Path qw(mkpath rmtree); -use File::Temp qw(tempdir); -use Getopt::Long qw(:config pass_through); -use Git; - -sub usage -{ - my $exitcode = shift; - print << 'USAGE'; -usage: git difftool [-t|--tool=] [--tool-help] - [-x|--extcmd=] - [-g|--gui] [--no-gui] - [--prompt] [-y|--no-prompt] - [-d|--dir-diff] - ['git diff' options] -USAGE - exit($exitcode); -} - -sub print_tool_help -{ - # See the comment at the bottom of file_diff() for the reason behind - # using system() followed by exit() instead of exec(). - my $rc = system(qw(git mergetool --tool-help=diff)); - exit($rc | ($rc >> 8)); -} - -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 use_wt_file -{ - my ($workdir, $file, $sha1) = @_; - my $null_sha1 = '0' x 40; - - if (-l "$workdir/$file" || ! -e _) { - return (0, $null_sha1); - } - - my $wt_sha1 = Git::command_oneline('hash-object', "$workdir/$file"); - my $use = ($sha1 eq $null_sha1) || ($sha1 eq $wt_sha1); - return ($use, $wt_sha1); -} - -sub changed_files -{ - my ($repo_path, $index, $worktree) = @_; - $ENV{GIT_INDEX_FILE} = $index; - - my @gitargs = ('--git-dir', $repo_path, '--work-tree', $worktree); - my @refreshargs = ( - @gitargs, 'update-index', - '--really-refresh', '-q', '--unmerged'); - try { - Git::command_oneline(@refreshargs); - } catch Git::Error::Command with {}; - - my @diffargs = (@gitargs, 'diff-files', '--name-only', '-z'); - my $line = Git::command_oneline(@diffargs); - my @files; - if (defined $line) { - @files = split('\0', $line); - } else { - @files = (); - } - - delete($ENV{GIT_INDEX_FILE}); - - return map { $_ => 1 } @files; -} - -sub setup_dir_diff -{ - my ($workdir, $symlinks) = @_; - my @gitargs = ('diff', '--raw', '--no-abbrev', '-z', @ARGV); - my $diffrtn = Git::command_oneline(@gitargs); - exit(0) unless defined($diffrtn); - - # 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 $wtindex = ''; - my %submodule; - my %symlink; - my @working_tree = (); - my %working_tree_dups = (); - my @rawdiff = split('\0', $diffrtn); - - my $i = 0; - while ($i < $#rawdiff) { - if ($rawdiff[$i] =~ /^::/) { - warn << 'EOF'; -Combined diff formats ('-c' and '--cc') are not supported in -directory diff mode ('-d' and '--dir-diff'). -EOF - 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} = - Git::command_oneline('show', $lsha1); - } - - if ($rmode eq $symlink_mode) { - $symlink{$dst_path}{right} = - Git::command_oneline('show', $rsha1); - } - - if ($lmode ne $null_mode and $status !~ /^C/) { - $lindex .= "$lmode $lsha1\t$src_path\0"; - } - - if ($rmode ne $null_mode) { - # Avoid duplicate working_tree entries - if ($working_tree_dups{$dst_path}++) { - next; - } - my ($use, $wt_sha1) = - use_wt_file($workdir, $dst_path, $rsha1); - if ($use) { - push @working_tree, $dst_path; - $wtindex .= "$rmode $wt_sha1\t$dst_path\0"; - } else { - $rindex .= "$rmode $rsha1\t$dst_path\0"; - } - } - } - - # 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); - - # Populate the left and right directories based on each index file - my ($inpipe, $ctx); - $ENV{GIT_INDEX_FILE} = "$tmpdir/lindex"; - ($inpipe, $ctx) = - Git::command_input_pipe('update-index', '-z', '--index-info'); - print($inpipe $lindex); - Git::command_close_pipe($inpipe, $ctx); - - my $rc = system('git', 'checkout-index', '--all', "--prefix=$ldir/"); - exit_cleanup($tmpdir, $rc) if $rc != 0; - - $ENV{GIT_INDEX_FILE} = "$tmpdir/rindex"; - ($inpipe, $ctx) = - Git::command_input_pipe('update-index', '-z', '--index-info'); - print($inpipe $rindex); - Git::command_close_pipe($inpipe, $ctx); - - $rc = system('git', 'checkout-index', '--all', "--prefix=$rdir/"); - exit_cleanup($tmpdir, $rc) if $rc != 0; - - $ENV{GIT_INDEX_FILE} = "$tmpdir/wtindex"; - ($inpipe, $ctx) = - Git::command_input_pipe('update-index', '--info-only', '-z', '--index-info'); - print($inpipe $wtindex); - Git::command_close_pipe($inpipe, $ctx); - - # If $GIT_DIR was explicitly set just for the update/checkout - # commands, then it should be unset before continuing. - delete($ENV{GIT_INDEX_FILE}); - - # Changes in the working tree need special treatment since they are - # not part of the index. Remove any trailing slash from $workdir - # before starting to avoid double slashes in symlink targets. - $workdir =~ s|/$||; - for my $file (@working_tree) { - my $dir = dirname($file); - unless (-d "$rdir/$dir") { - 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); - } - } - - # 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) { - my $ok = 0; - if (defined($submodule{$path}{left})) { - $ok = write_to_file("$ldir/$path", - "Subproject commit $submodule{$path}{left}"); - } - if (defined($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" - # shows only the link itself, not the contents of the link target. - # This loop replicates that behavior. - for my $path (keys %symlink) { - my $ok = 0; - if (defined($symlink{$path}{left})) { - $ok = write_to_file("$ldir/$path", - $symlink{$path}{left}); - } - if (defined($symlink{$path}{right})) { - $ok = write_to_file("$rdir/$path", - $symlink{$path}{right}); - } - exit_cleanup($tmpdir, 1) if not $ok; - } - - return ($ldir, $rdir, $tmpdir, @working_tree); -} - -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 return 0; - } - - # 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 return 0; - print($fh $value); - close($fh); - - return 1; -} - -sub main -{ - # parse command-line options. all unrecognized options and arguments - # are passed through to the 'git diff' command. - my %opts = ( - difftool_cmd => undef, - dirdiff => undef, - extcmd => undef, - gui => undef, - help => undef, - prompt => undef, - symlinks => $^O ne 'cygwin' && - $^O ne 'MSWin32' && $^O ne 'msys', - tool_help => undef, - trust_exit_code => 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}, - 'trust-exit-code' => \$opts{trust_exit_code}, - 'no-trust-exit-code' => sub { $opts{trust_exit_code} = 0; }, - 'x|extcmd:s' => \$opts{extcmd}); - - if (defined($opts{help})) { - usage(0); - } - if (defined($opts{tool_help})) { - print_tool_help(); - } - if (defined($opts{difftool_cmd})) { - if (length($opts{difftool_cmd}) > 0) { - $ENV{GIT_DIFF_TOOL} = $opts{difftool_cmd}; - } else { - print "No given for --tool=\n"; - usage(1); - } - } - if (defined($opts{extcmd})) { - if (length($opts{extcmd}) > 0) { - $ENV{GIT_DIFFTOOL_EXTCMD} = $opts{extcmd}; - } else { - print "No given for --extcmd=\n"; - usage(1); - } - } - if ($opts{gui}) { - my $guitool = Git::config('diff.guitool'); - if (defined($guitool) && length($guitool) > 0) { - $ENV{GIT_DIFF_TOOL} = $guitool; - } - } - - if (!defined $opts{trust_exit_code}) { - $opts{trust_exit_code} = Git::config_bool('difftool.trustExitCode'); - } - if ($opts{trust_exit_code}) { - $ENV{GIT_DIFFTOOL_TRUST_EXIT_CODE} = 'true'; - } else { - $ENV{GIT_DIFFTOOL_TRUST_EXIT_CODE} = 'false'; - } - - # 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($opts{dirdiff})) { - 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 $repo_path = $repo->repo_path(); - my $workdir = $repo->wc_path(); - my ($a, $b, $tmpdir, @worktree) = setup_dir_diff($workdir, $symlinks); - - if (defined($extcmd)) { - $rc = system($extcmd, $a, $b); - } else { - $ENV{GIT_DIFFTOOL_DIRDIFF} = 'true'; - $rc = system('git', 'difftool--helper', $a, $b); - } - # 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. - # Do not copy back files when symlinks are used and the - # external tool did not replace the original link with a file. - # - # These hashes are loaded lazily since they aren't needed - # in the common case of --symlinks and the difftool updating - # files through the symlink. - my %wt_modified; - my %tmp_modified; - my $indices_loaded = 0; - - for my $file (@worktree) { - next if $symlinks && -l "$b/$file"; - next if ! -f "$b/$file"; - - if (!$indices_loaded) { - %wt_modified = changed_files( - $repo_path, "$tmpdir/wtindex", $workdir); - %tmp_modified = changed_files( - $repo_path, "$tmpdir/wtindex", $b); - $indices_loaded = 1; - } - - if (exists $wt_modified{$file} and exists $tmp_modified{$file}) { - my $errmsg = "warning: Both files modified: "; - $errmsg .= "'$workdir/$file' and '$b/$file'.\n"; - $errmsg .= "warning: Working tree file has been left.\n"; - $errmsg .= "warning:\n"; - warn $errmsg; - $error = 1; - } elsif (exists $tmp_modified{$file}) { - 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 { - exit_cleanup($tmpdir, $rc); - } -} - -sub file_diff -{ - my ($prompt) = @_; - - 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)); -} - -main(); diff --git a/git.c b/git.c index a8e6a15657..e68b6ebec6 100644 --- a/git.c +++ b/git.c @@ -424,12 +424,7 @@ static struct cmd_struct commands[] = { { "diff-files", cmd_diff_files, RUN_SETUP | NEED_WORK_TREE }, { "diff-index", cmd_diff_index, RUN_SETUP }, { "diff-tree", cmd_diff_tree, RUN_SETUP }, - /* - * NEEDSWORK: Once the redirection to git-legacy-difftool.perl in - * builtin/difftool.c has been removed, this entry should be changed to - * RUN_SETUP | NEED_WORK_TREE - */ - { "difftool", cmd_difftool }, + { "difftool", cmd_difftool, RUN_SETUP | NEED_WORK_TREE }, { "fast-export", cmd_fast_export, RUN_SETUP }, { "fetch", cmd_fetch, RUN_SETUP }, { "fetch-pack", cmd_fetch_pack, RUN_SETUP }, diff --git a/t/t7800-difftool.sh b/t/t7800-difftool.sh index b6a6c30f24..81a2de2b9d 100755 --- a/t/t7800-difftool.sh +++ b/t/t7800-difftool.sh @@ -23,10 +23,8 @@ prompt_given () test "$prompt" = "Launch 'test-tool' [Y/n]? branch" } -# NEEDSWORK: lose all the PERL prereqs once legacy-difftool is retired. - # Create a file on master and change it on branch -test_expect_success PERL 'setup' ' +test_expect_success 'setup' ' echo master >file && git add file && git commit -m "added file" && @@ -38,7 +36,7 @@ test_expect_success PERL 'setup' ' ' # Configure a custom difftool..cmd and use it -test_expect_success PERL 'custom commands' ' +test_expect_success 'custom commands' ' difftool_test_setup && test_config difftool.test-tool.cmd "cat \"\$REMOTE\"" && echo master >expect && @@ -51,21 +49,21 @@ test_expect_success PERL 'custom commands' ' test_cmp expect actual ' -test_expect_success PERL 'custom tool commands override built-ins' ' +test_expect_success 'custom tool commands override built-ins' ' test_config difftool.vimdiff.cmd "cat \"\$REMOTE\"" && echo master >expect && git difftool --tool vimdiff --no-prompt branch >actual && test_cmp expect actual ' -test_expect_success PERL 'difftool ignores bad --tool values' ' +test_expect_success 'difftool ignores bad --tool values' ' : >expect && test_must_fail \ git difftool --no-prompt --tool=bad-tool branch >actual && test_cmp expect actual ' -test_expect_success PERL 'difftool forwards arguments to diff' ' +test_expect_success 'difftool forwards arguments to diff' ' difftool_test_setup && >for-diff && git add for-diff && @@ -78,40 +76,40 @@ test_expect_success PERL 'difftool forwards arguments to diff' ' rm for-diff ' -test_expect_success PERL 'difftool ignores exit code' ' +test_expect_success 'difftool ignores exit code' ' test_config difftool.error.cmd false && git difftool -y -t error branch ' -test_expect_success PERL 'difftool forwards exit code with --trust-exit-code' ' +test_expect_success 'difftool forwards exit code with --trust-exit-code' ' test_config difftool.error.cmd false && test_must_fail git difftool -y --trust-exit-code -t error branch ' -test_expect_success PERL 'difftool forwards exit code with --trust-exit-code for built-ins' ' +test_expect_success 'difftool forwards exit code with --trust-exit-code for built-ins' ' test_config difftool.vimdiff.path false && test_must_fail git difftool -y --trust-exit-code -t vimdiff branch ' -test_expect_success PERL 'difftool honors difftool.trustExitCode = true' ' +test_expect_success 'difftool honors difftool.trustExitCode = true' ' test_config difftool.error.cmd false && test_config difftool.trustExitCode true && test_must_fail git difftool -y -t error branch ' -test_expect_success PERL 'difftool honors difftool.trustExitCode = false' ' +test_expect_success 'difftool honors difftool.trustExitCode = false' ' test_config difftool.error.cmd false && test_config difftool.trustExitCode false && git difftool -y -t error branch ' -test_expect_success PERL 'difftool ignores exit code with --no-trust-exit-code' ' +test_expect_success 'difftool ignores exit code with --no-trust-exit-code' ' test_config difftool.error.cmd false && test_config difftool.trustExitCode true && git difftool -y --no-trust-exit-code -t error branch ' -test_expect_success PERL 'difftool stops on error with --trust-exit-code' ' +test_expect_success 'difftool stops on error with --trust-exit-code' ' test_when_finished "rm -f for-diff .git/fail-right-file" && test_when_finished "git reset -- for-diff" && write_script .git/fail-right-file <<-\EOF && @@ -126,13 +124,13 @@ test_expect_success PERL 'difftool stops on error with --trust-exit-code' ' test_cmp expect actual ' -test_expect_success PERL 'difftool honors exit status if command not found' ' +test_expect_success 'difftool honors exit status if command not found' ' test_config difftool.nonexistent.cmd i-dont-exist && test_config difftool.trustExitCode false && test_must_fail git difftool -y -t nonexistent branch ' -test_expect_success PERL 'difftool honors --gui' ' +test_expect_success 'difftool honors --gui' ' difftool_test_setup && test_config merge.tool bogus-tool && test_config diff.tool bogus-tool && @@ -143,7 +141,7 @@ test_expect_success PERL 'difftool honors --gui' ' test_cmp expect actual ' -test_expect_success PERL 'difftool --gui last setting wins' ' +test_expect_success 'difftool --gui last setting wins' ' difftool_test_setup && : >expect && git difftool --no-prompt --gui --no-gui >actual && @@ -157,7 +155,7 @@ test_expect_success PERL 'difftool --gui last setting wins' ' test_cmp expect actual ' -test_expect_success PERL 'difftool --gui works without configured diff.guitool' ' +test_expect_success 'difftool --gui works without configured diff.guitool' ' difftool_test_setup && echo branch >expect && git difftool --no-prompt --gui branch >actual && @@ -165,7 +163,7 @@ test_expect_success PERL 'difftool --gui works without configured diff.guitool' ' # Specify the diff tool using $GIT_DIFF_TOOL -test_expect_success PERL 'GIT_DIFF_TOOL variable' ' +test_expect_success 'GIT_DIFF_TOOL variable' ' difftool_test_setup && git config --unset diff.tool && echo branch >expect && @@ -175,7 +173,7 @@ test_expect_success PERL 'GIT_DIFF_TOOL variable' ' # Test the $GIT_*_TOOL variables and ensure # that $GIT_DIFF_TOOL always wins unless --tool is specified -test_expect_success PERL 'GIT_DIFF_TOOL overrides' ' +test_expect_success 'GIT_DIFF_TOOL overrides' ' difftool_test_setup && test_config diff.tool bogus-tool && test_config merge.tool bogus-tool && @@ -193,7 +191,7 @@ test_expect_success PERL 'GIT_DIFF_TOOL overrides' ' # Test that we don't have to pass --no-prompt to difftool # when $GIT_DIFFTOOL_NO_PROMPT is true -test_expect_success PERL 'GIT_DIFFTOOL_NO_PROMPT variable' ' +test_expect_success 'GIT_DIFFTOOL_NO_PROMPT variable' ' difftool_test_setup && echo branch >expect && GIT_DIFFTOOL_NO_PROMPT=true git difftool branch >actual && @@ -202,7 +200,7 @@ test_expect_success PERL 'GIT_DIFFTOOL_NO_PROMPT variable' ' # git-difftool supports the difftool.prompt variable. # Test that GIT_DIFFTOOL_PROMPT can override difftool.prompt = false -test_expect_success PERL 'GIT_DIFFTOOL_PROMPT variable' ' +test_expect_success 'GIT_DIFFTOOL_PROMPT variable' ' difftool_test_setup && test_config difftool.prompt false && echo >input && @@ -212,7 +210,7 @@ test_expect_success PERL 'GIT_DIFFTOOL_PROMPT variable' ' ' # Test that we don't have to pass --no-prompt when difftool.prompt is false -test_expect_success PERL 'difftool.prompt config variable is false' ' +test_expect_success 'difftool.prompt config variable is false' ' difftool_test_setup && test_config difftool.prompt false && echo branch >expect && @@ -221,7 +219,7 @@ test_expect_success PERL 'difftool.prompt config variable is false' ' ' # Test that we don't have to pass --no-prompt when mergetool.prompt is false -test_expect_success PERL 'difftool merge.prompt = false' ' +test_expect_success 'difftool merge.prompt = false' ' difftool_test_setup && test_might_fail git config --unset difftool.prompt && test_config mergetool.prompt false && @@ -231,7 +229,7 @@ test_expect_success PERL 'difftool merge.prompt = false' ' ' # Test that the -y flag can override difftool.prompt = true -test_expect_success PERL 'difftool.prompt can overridden with -y' ' +test_expect_success 'difftool.prompt can overridden with -y' ' difftool_test_setup && test_config difftool.prompt true && echo branch >expect && @@ -240,7 +238,7 @@ test_expect_success PERL 'difftool.prompt can overridden with -y' ' ' # Test that the --prompt flag can override difftool.prompt = false -test_expect_success PERL 'difftool.prompt can overridden with --prompt' ' +test_expect_success 'difftool.prompt can overridden with --prompt' ' difftool_test_setup && test_config difftool.prompt false && echo >input && @@ -250,7 +248,7 @@ test_expect_success PERL 'difftool.prompt can overridden with --prompt' ' ' # Test that the last flag passed on the command-line wins -test_expect_success PERL 'difftool last flag wins' ' +test_expect_success 'difftool last flag wins' ' difftool_test_setup && echo branch >expect && git difftool --prompt --no-prompt branch >actual && @@ -263,7 +261,7 @@ test_expect_success PERL 'difftool last flag wins' ' # git-difftool falls back to git-mergetool config variables # so test that behavior here -test_expect_success PERL 'difftool + mergetool config variables' ' +test_expect_success 'difftool + mergetool config variables' ' test_config merge.tool test-tool && test_config mergetool.test-tool.cmd "cat \$LOCAL" && echo branch >expect && @@ -277,49 +275,49 @@ test_expect_success PERL 'difftool + mergetool config variables' ' test_cmp expect actual ' -test_expect_success PERL 'difftool..path' ' +test_expect_success 'difftool..path' ' test_config difftool.tkdiff.path echo && git difftool --tool=tkdiff --no-prompt branch >output && lines=$(grep file output | wc -l) && test "$lines" -eq 1 ' -test_expect_success PERL 'difftool --extcmd=cat' ' +test_expect_success 'difftool --extcmd=cat' ' echo branch >expect && echo master >>expect && git difftool --no-prompt --extcmd=cat branch >actual && test_cmp expect actual ' -test_expect_success PERL 'difftool --extcmd cat' ' +test_expect_success 'difftool --extcmd cat' ' echo branch >expect && echo master >>expect && git difftool --no-prompt --extcmd=cat branch >actual && test_cmp expect actual ' -test_expect_success PERL 'difftool -x cat' ' +test_expect_success 'difftool -x cat' ' echo branch >expect && echo master >>expect && git difftool --no-prompt -x cat branch >actual && test_cmp expect actual ' -test_expect_success PERL 'difftool --extcmd echo arg1' ' +test_expect_success 'difftool --extcmd echo arg1' ' echo file >expect && git difftool --no-prompt \ --extcmd sh\ -c\ \"echo\ \$1\" branch >actual && test_cmp expect actual ' -test_expect_success PERL 'difftool --extcmd cat arg1' ' +test_expect_success 'difftool --extcmd cat arg1' ' echo master >expect && git difftool --no-prompt \ --extcmd sh\ -c\ \"cat\ \$1\" branch >actual && test_cmp expect actual ' -test_expect_success PERL 'difftool --extcmd cat arg2' ' +test_expect_success 'difftool --extcmd cat arg2' ' echo branch >expect && git difftool --no-prompt \ --extcmd sh\ -c\ \"cat\ \$2\" branch >actual && @@ -327,7 +325,7 @@ test_expect_success PERL 'difftool --extcmd cat arg2' ' ' # Create a second file on master and a different version on branch -test_expect_success PERL 'setup with 2 files different' ' +test_expect_success 'setup with 2 files different' ' echo m2 >file2 && git add file2 && git commit -m "added file2" && @@ -339,7 +337,7 @@ test_expect_success PERL 'setup with 2 files different' ' git checkout master ' -test_expect_success PERL 'say no to the first file' ' +test_expect_success 'say no to the first file' ' (echo n && echo) >input && git difftool -x cat branch output && grep m2 output && @@ -348,7 +346,7 @@ test_expect_success PERL 'say no to the first file' ' ! grep branch output ' -test_expect_success PERL 'say no to the second file' ' +test_expect_success 'say no to the second file' ' (echo && echo n) >input && git difftool -x cat branch output && grep master output && @@ -357,7 +355,7 @@ test_expect_success PERL 'say no to the second file' ' ! grep br2 output ' -test_expect_success PERL 'ending prompt input with EOF' ' +test_expect_success 'ending prompt input with EOF' ' git difftool -x cat branch output && ! grep master output && ! grep branch output && @@ -365,12 +363,12 @@ test_expect_success PERL 'ending prompt input with EOF' ' ! grep br2 output ' -test_expect_success PERL 'difftool --tool-help' ' +test_expect_success 'difftool --tool-help' ' git difftool --tool-help >output && grep tool output ' -test_expect_success PERL 'setup change in subdirectory' ' +test_expect_success 'setup change in subdirectory' ' git checkout master && mkdir sub && echo master >sub/sub && @@ -383,11 +381,11 @@ test_expect_success PERL 'setup change in subdirectory' ' ' run_dir_diff_test () { - test_expect_success PERL "$1 --no-symlinks" " + test_expect_success "$1 --no-symlinks" " symlinks=--no-symlinks && $2 " - test_expect_success PERL,SYMLINKS "$1 --symlinks" " + test_expect_success SYMLINKS "$1 --symlinks" " symlinks=--symlinks && $2 " @@ -472,7 +470,7 @@ do done >actual EOF -test_expect_success PERL,SYMLINKS 'difftool --dir-diff --symlink without unstaged changes' ' +test_expect_success SYMLINKS 'difftool --dir-diff --symlink without unstaged changes' ' cat >expect <<-EOF && file $PWD/file @@ -509,7 +507,7 @@ write_script modify-file <<\EOF echo "new content" >file EOF -test_expect_success PERL 'difftool --no-symlinks does not overwrite working tree file ' ' +test_expect_success 'difftool --no-symlinks does not overwrite working tree file ' ' echo "orig content" >file && git difftool --dir-diff --no-symlinks --extcmd "$PWD/modify-file" branch && echo "new content" >expect && @@ -522,7 +520,7 @@ echo "tmp content" >"$2/file" && echo "$2" >tmpdir EOF -test_expect_success PERL 'difftool --no-symlinks detects conflict ' ' +test_expect_success 'difftool --no-symlinks detects conflict ' ' ( TMPDIR=$TRASH_DIRECTORY && export TMPDIR && @@ -535,7 +533,7 @@ test_expect_success PERL 'difftool --no-symlinks detects conflict ' ' ) ' -test_expect_success PERL 'difftool properly honors gitlink and core.worktree' ' +test_expect_success 'difftool properly honors gitlink and core.worktree' ' git submodule add ./. submod/ule && test_config -C submod/ule diff.tool checktrees && test_config -C submod/ule difftool.checktrees.cmd '\'' @@ -549,7 +547,7 @@ test_expect_success PERL 'difftool properly honors gitlink and core.worktree' ' ) ' -test_expect_success PERL,SYMLINKS 'difftool --dir-diff symlinked directories' ' +test_expect_success SYMLINKS 'difftool --dir-diff symlinked directories' ' git init dirlinks && ( cd dirlinks &&