From: Junio C Hamano Date: Tue, 31 Jan 2017 21:15:00 +0000 (-0800) Subject: Merge branch 'js/difftool-builtin' X-Git-Tag: v2.12.0-rc0~28 X-Git-Url: https://git.lorimer.id.au/gitweb.git/diff_plain/b7786bb4b09463641f3e39eca18aefc630ee4f38?ds=inline;hp=-c Merge branch 'js/difftool-builtin' Rewrite a scripted porcelain "git difftool" in C. * js/difftool-builtin: difftool: hack around -Wzero-length-format warning difftool: retire the scripted version difftool: implement the functionality in the builtin difftool: add a skeleton for the upcoming builtin --- b7786bb4b09463641f3e39eca18aefc630ee4f38 diff --combined Makefile index 53ecc84e28,28b6440b3c..052dce087c --- a/Makefile +++ b/Makefile @@@ -279,9 -279,6 +279,9 @@@ all: # is a simplified version of the merge sort used in glibc. This is # recommended if Git triggers O(n^2) behavior in your platform's qsort(). # +# Define HAVE_ISO_QSORT_S if your platform provides a qsort_s() that's +# compatible with the one described in C11 Annex K. +# # Define UNRELIABLE_FSTAT if your system's fstat does not return the same # information on a not yet closed file that lstat would return for the same # file after it was closed. @@@ -341,6 -338,11 +341,6 @@@ # # Define NATIVE_CRLF if your platform uses CRLF for line endings. # -# Define XDL_FAST_HASH to use an alternative line-hashing method in -# the diff algorithm. It gives a nice speedup if your processor has -# fast unaligned word loads. Does NOT work on big-endian systems! -# Enabled by default on x86_64. -# # Define GIT_USER_AGENT if you want to change how git identifies itself during # network interactions. The default is "git/$(GIT_VERSION)". # @@@ -525,7 -527,6 +525,6 @@@ SCRIPT_LIB += git-sh-setu SCRIPT_LIB += git-sh-i18n SCRIPT_PERL += git-add--interactive.perl - SCRIPT_PERL += git-difftool.perl SCRIPT_PERL += git-archimport.perl SCRIPT_PERL += git-cvsexportcommit.perl SCRIPT_PERL += git-cvsimport.perl @@@ -886,6 -887,7 +885,7 @@@ BUILTIN_OBJS += builtin/diff-files. BUILTIN_OBJS += builtin/diff-index.o BUILTIN_OBJS += builtin/diff-tree.o BUILTIN_OBJS += builtin/diff.o + BUILTIN_OBJS += builtin/difftool.o BUILTIN_OBJS += builtin/fast-export.o BUILTIN_OBJS += builtin/fetch-pack.o BUILTIN_OBJS += builtin/fetch.o @@@ -1421,11 -1423,6 +1421,11 @@@ ifdef INTERNAL_QSOR COMPAT_CFLAGS += -DINTERNAL_QSORT COMPAT_OBJS += compat/qsort.o endif +ifdef HAVE_ISO_QSORT_S + COMPAT_CFLAGS += -DHAVE_ISO_QSORT_S +else + COMPAT_OBJS += compat/qsort_s.o +endif ifdef RUNTIME_PREFIX COMPAT_CFLAGS += -DRUNTIME_PREFIX endif @@@ -1488,6 -1485,10 +1488,6 @@@ ifndef NO_MSGFMT_EXTENDED_OPTION MSGFMT += --check --statistics endif -ifneq (,$(XDL_FAST_HASH)) - BASIC_CFLAGS += -DXDL_FAST_HASH -endif - ifdef GMTIME_UNRELIABLE_ERRORS COMPAT_OBJS += compat/gmtime.o BASIC_CFLAGS += -DGMTIME_UNRELIABLE_ERRORS @@@ -1824,7 -1825,7 +1824,7 @@@ $(SCRIPT_LIB) : % : %.sh GIT-SCRIPT-DEF git.res: git.rc GIT-VERSION-FILE $(QUIET_RC)$(RC) \ $(join -DMAJOR= -DMINOR=, $(wordlist 1,2,$(subst -, ,$(subst ., ,$(GIT_VERSION))))) \ - -DGIT_VERSION="\\\"$(GIT_VERSION)\\\"" $< -o $@ + -DGIT_VERSION="\\\"$(GIT_VERSION)\\\"" -i $< -o $@ # This makes sure we depend on the NO_PERL setting itself. $(SCRIPT_PERL_GEN): GIT-BUILD-OPTIONS @@@ -2054,7 -2055,7 +2054,7 @@@ git-%$X: %.o GIT-LDFLAGS $(GITLIBS git-imap-send$X: imap-send.o $(IMAP_SEND_BUILDDEPS) GIT-LDFLAGS $(GITLIBS) $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \ - $(LIBS) $(IMAP_SEND_LDFLAGS) + $(IMAP_SEND_LDFLAGS) $(LIBS) git-http-fetch$X: http.o http-walker.o http-fetch.o GIT-LDFLAGS $(GITLIBS) $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) \ @@@ -2113,8 -2114,7 +2113,8 @@@ XGETTEXT_FLAGS_C = $(XGETTEXT_FLAGS) -- --keyword=_ --keyword=N_ --keyword="Q_:1,2" XGETTEXT_FLAGS_SH = $(XGETTEXT_FLAGS) --language=Shell \ --keyword=gettextln --keyword=eval_gettextln -XGETTEXT_FLAGS_PERL = $(XGETTEXT_FLAGS) --keyword=__ --language=Perl +XGETTEXT_FLAGS_PERL = $(XGETTEXT_FLAGS) --language=Perl \ + --keyword=__ --keyword=N__ --keyword="__n:1,2" LOCALIZED_C = $(C_OBJ:o=c) $(LIB_H) $(GENERATED_H) LOCALIZED_SH = $(SCRIPT_SH) LOCALIZED_SH += git-parse-remote.sh @@@ -2149,22 -2149,9 +2149,22 @@@ endi po/build/locale/%/LC_MESSAGES/git.mo: po/%.po $(QUIET_MSGFMT)mkdir -p $(dir $@) && $(MSGFMT) -o $@ $< -FIND_SOURCE_FILES = ( git ls-files '*.[hcS]' 2>/dev/null || \ - $(FIND) . \( -name .git -type d -prune \) \ - -o \( -name '*.[hcS]' -type f -print \) ) +FIND_SOURCE_FILES = ( \ + git ls-files \ + '*.[hcS]' \ + '*.sh' \ + ':!*[tp][0-9][0-9][0-9][0-9]*' \ + ':!contrib' \ + 2>/dev/null || \ + $(FIND) . \ + \( -name .git -type d -prune \) \ + -o \( -name '[tp][0-9][0-9][0-9][0-9]*' -prune \) \ + -o \( -name contrib -type d -prune \) \ + -o \( -name build -type d -prune \) \ + -o \( -name 'trash*' -type d -prune \) \ + -o \( -name '*.[hcS]' -type f -print \) \ + -o \( -name '*.sh' -type f -print \) \ + ) $(ETAGS_TARGET): FORCE $(RM) $(ETAGS_TARGET) diff --combined contrib/examples/git-difftool.perl index 0000000000,a5790d03a0..df59bdfe97 mode 000000,100755..100755 --- a/contrib/examples/git-difftool.perl +++ b/contrib/examples/git-difftool.perl @@@ -1,0 -1,470 +1,481 @@@ + #!/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; ++use Git::I18N; + + 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 ($file, $sha1) = @_; + my $null_sha1 = '0' x 40; + - if (-l "$workdir/$file" || ! -e _) { ++ if (-l $file || ! -e _) { + return (0, $null_sha1); + } + - my $wt_sha1 = Git::command_oneline('hash-object', "$workdir/$file"); ++ my $wt_sha1 = Git::command_oneline('hash-object', $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 ($worktree, $symlinks) = @_; + my @gitargs = ('diff', '--raw', '--no-abbrev', '-z', @ARGV); + my $diffrtn = Git::command_oneline(@gitargs); + exit(0) unless defined($diffrtn); + ++ # Go to the root of the worktree now that we've captured the list of ++ # changed files. The paths returned by diff --raw are relative to the ++ # top-level of the repository, but we defer changing directories so ++ # that @ARGV can perform pathspec limiting in the current directory. ++ chdir($worktree); ++ + # 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 @files = (); + my %working_tree_dups = (); + my @rawdiff = split('\0', $diffrtn); + + my $i = 0; + while ($i < $#rawdiff) { + if ($rawdiff[$i] =~ /^::/) { - warn << 'EOF'; ++ 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 ++ # Avoid duplicate entries + if ($working_tree_dups{$dst_path}++) { + next; + } + my ($use, $wt_sha1) = - use_wt_file($workdir, $dst_path, $rsha1); ++ use_wt_file($dst_path, $rsha1); + if ($use) { - push @working_tree, $dst_path; ++ push @files, $dst_path; + $wtindex .= "$rmode $wt_sha1\t$dst_path\0"; + } else { + $rindex .= "$rmode $rsha1\t$dst_path\0"; + } + } + } + ++ # Go to the root of the worktree so that the left index files ++ # are properly setup -- the index is toplevel-relative. ++ chdir($worktree); ++ + # 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) { ++ # not part of the index. ++ for my $file (@files) { + my $dir = dirname($file); + unless (-d "$rdir/$dir") { + mkpath("$rdir/$dir") or + exit_cleanup($tmpdir, 1); + } + if ($symlinks) { - symlink("$workdir/$file", "$rdir/$file") or ++ symlink("$worktree/$file", "$rdir/$file") or + exit_cleanup($tmpdir, 1); + } else { - copy("$workdir/$file", "$rdir/$file") or ++ copy($file, "$rdir/$file") or + exit_cleanup($tmpdir, 1); + - my $mode = stat("$workdir/$file")->mode; ++ my $mode = stat($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); ++ return ($ldir, $rdir, $tmpdir, @files); + } + + 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"; ++ 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"; ++ 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); ++ my $worktree = $repo->wc_path(); ++ $worktree =~ s|/$||; # Avoid double slashes in symlink targets ++ my ($a, $b, $tmpdir, @files) = setup_dir_diff($worktree, $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) { ++ for my $file (@files) { + next if $symlinks && -l "$b/$file"; + next if ! -f "$b/$file"; + + if (!$indices_loaded) { + %wt_modified = changed_files( - $repo_path, "$tmpdir/wtindex", $workdir); ++ $repo_path, "$tmpdir/wtindex", $worktree); + %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; ++ warn sprintf(__( ++ "warning: Both files modified:\n" . ++ "'%s/%s' and '%s/%s'.\n" . ++ "warning: Working tree file has been left.\n" . ++ "warning:\n"), $worktree, $file, $b, $file); + $error = 1; + } elsif (exists $tmp_modified{$file}) { + my $mode = stat("$b/$file")->mode; - copy("$b/$file", "$workdir/$file") or ++ copy("$b/$file", $file) or + exit_cleanup($tmpdir, 1); + - chmod($mode, "$workdir/$file") or ++ chmod($mode, $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"; ++ warn sprintf(__( ++ "warning: Temporary files exist in '%s'.\n" . ++ "warning: You may want to cleanup or recover these.\n"), $tmpdir); + 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 --combined git.c index b367cf6686,e68b6ebec6..1cf125cd28 --- a/git.c +++ b/git.c @@@ -424,6 -424,7 +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 }, + { "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 }, @@@ -434,7 -435,7 +435,7 @@@ { "fsck-objects", cmd_fsck, RUN_SETUP }, { "gc", cmd_gc, RUN_SETUP }, { "get-tar-commit-id", cmd_get_tar_commit_id }, - { "grep", cmd_grep, RUN_SETUP_GENTLY }, + { "grep", cmd_grep, RUN_SETUP_GENTLY | SUPPORT_SUPER_PREFIX }, { "hash-object", cmd_hash_object }, { "help", cmd_help }, { "index-pack", cmd_index_pack, RUN_SETUP_GENTLY }, @@@ -493,7 -494,7 +494,7 @@@ { "stage", cmd_add, RUN_SETUP | NEED_WORK_TREE }, { "status", cmd_status, RUN_SETUP | NEED_WORK_TREE }, { "stripspace", cmd_stripspace }, - { "submodule--helper", cmd_submodule__helper, RUN_SETUP }, + { "submodule--helper", cmd_submodule__helper, RUN_SETUP | SUPPORT_SUPER_PREFIX}, { "symbolic-ref", cmd_symbolic_ref, RUN_SETUP }, { "tag", cmd_tag, RUN_SETUP }, { "unpack-file", cmd_unpack_file, RUN_SETUP }, @@@ -575,7 -576,8 +576,7 @@@ static void handle_builtin(int argc, co static void execv_dashed_external(const char **argv) { - struct strbuf cmd = STRBUF_INIT; - const char *tmp; + struct child_process cmd = CHILD_PROCESS_INIT; int status; if (get_super_prefix()) @@@ -585,25 -587,30 +586,25 @@@ use_pager = check_pager_config(argv[0]); commit_pager_choice(); - strbuf_addf(&cmd, "git-%s", argv[0]); + argv_array_pushf(&cmd.args, "git-%s", argv[0]); + argv_array_pushv(&cmd.args, argv + 1); + cmd.clean_on_exit = 1; + cmd.wait_after_clean = 1; + cmd.silent_exec_failure = 1; - /* - * argv[0] must be the git command, but the argv array - * belongs to the caller, and may be reused in - * subsequent loop iterations. Save argv[0] and - * restore it on error. - */ - tmp = argv[0]; - argv[0] = cmd.buf; - - trace_argv_printf(argv, "trace: exec:"); + trace_argv_printf(cmd.args.argv, "trace: exec:"); /* - * if we fail because the command is not found, it is - * OK to return. Otherwise, we just pass along the status code. + * If we fail because the command is not found, it is + * OK to return. Otherwise, we just pass along the status code, + * or our usual generic code if we were not even able to exec + * the program. */ - status = run_command_v_opt(argv, RUN_SILENT_EXEC_FAILURE | RUN_CLEAN_ON_EXIT); - if (status >= 0 || errno != ENOENT) + status = run_command(&cmd); + if (status >= 0) exit(status); - - argv[0] = tmp; - - strbuf_release(&cmd); + else if (errno != ENOENT) + exit(128); } static int run_argv(int *argcp, const char ***argv) @@@ -648,11 -655,6 +649,11 @@@ int cmd_main(int argc, const char **arg cmd = argv[0]; if (!cmd) cmd = "git-help"; + else { + const char *slash = find_last_dir_sep(cmd); + if (slash) + cmd = slash + 1; + } trace_command_performance(argv); diff --combined t/t7800-difftool.sh index 99d4123461,81a2de2b9d..aa0ef02597 --- a/t/t7800-difftool.sh +++ b/t/t7800-difftool.sh @@@ -24,7 -24,7 +24,7 @@@ prompt_given ( } # 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" && @@@ -36,7 -36,7 +36,7 @@@ ' # 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 && @@@ -49,21 -49,21 +49,21 @@@ 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 && @@@ -76,40 -76,40 +76,40 @@@ 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 && @@@ -124,13 -124,13 +124,13 @@@ 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 && @@@ -141,7 -141,7 +141,7 @@@ 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 && @@@ -155,7 -155,7 +155,7 @@@ 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 && @@@ -163,7 -163,7 +163,7 @@@ ' # 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 && @@@ -173,7 -173,7 +173,7 @@@ # 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 && @@@ -191,7 -191,7 +191,7 @@@ # 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 && @@@ -200,7 -200,7 +200,7 @@@ # 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 && @@@ -210,7 -210,7 +210,7 @@@ ' # 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 && @@@ -219,7 -219,7 +219,7 @@@ ' # 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 && @@@ -229,7 -229,7 +229,7 @@@ ' # 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 && @@@ -238,7 -238,7 +238,7 @@@ ' # 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 && @@@ -248,7 -248,7 +248,7 @@@ ' # 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 && @@@ -261,7 -261,7 +261,7 @@@ # 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 && @@@ -275,49 -275,49 +275,49 @@@ 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 && @@@ -325,7 -325,7 +325,7 @@@ ' # 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" && @@@ -337,7 -337,7 +337,7 @@@ 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 && @@@ -346,7 -346,7 +346,7 @@@ ! 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 && @@@ -355,7 -355,7 +355,7 @@@ ! 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 && @@@ -363,18 -363,17 +363,18 @@@ ! 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 && git add sub/sub && git commit -m "added sub/sub" && + git tag v1 && echo test >>file && echo test >>sub/sub && git add file sub/sub && @@@ -382,11 -381,11 +382,11 @@@ ' 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 " @@@ -410,49 -409,12 +410,49 @@@ run_dir_diff_test 'difftool --dir-diff grep file output ' -run_dir_diff_test 'difftool --dir-diff from subdirectory' ' +run_dir_diff_test 'difftool --dir-diff branch from subdirectory' ' ( cd sub && git difftool --dir-diff $symlinks --extcmd ls branch >output && - grep sub output && - grep file output + # "sub" must only exist in "right" + # "file" and "file2" must be listed in both "left" and "right" + test "1" = $(grep sub output | wc -l) && + test "2" = $(grep file"$" output | wc -l) && + test "2" = $(grep file2 output | wc -l) + ) +' + +run_dir_diff_test 'difftool --dir-diff v1 from subdirectory' ' + ( + cd sub && + git difftool --dir-diff $symlinks --extcmd ls v1 >output && + # "sub" and "file" exist in both v1 and HEAD. + # "file2" is unchanged. + test "2" = $(grep sub output | wc -l) && + test "2" = $(grep file output | wc -l) && + test "0" = $(grep file2 output | wc -l) + ) +' + +run_dir_diff_test 'difftool --dir-diff branch from subdirectory w/ pathspec' ' + ( + cd sub && + git difftool --dir-diff $symlinks --extcmd ls branch -- .>output && + # "sub" only exists in "right" + # "file" and "file2" must not be listed + test "1" = $(grep sub output | wc -l) && + test "0" = $(grep file output | wc -l) + ) +' + +run_dir_diff_test 'difftool --dir-diff v1 from subdirectory w/ pathspec' ' + ( + cd sub && + git difftool --dir-diff $symlinks --extcmd ls v1 -- .>output && + # "sub" exists in v1 and HEAD + # "file" is filtered out by the pathspec + test "2" = $(grep sub output | wc -l) && + test "0" = $(grep file output | wc -l) ) ' @@@ -508,7 -470,7 +508,7 @@@ d 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 @@@ -545,7 -507,7 +545,7 @@@ write_script modify-file <<\EO 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 && @@@ -558,7 -520,7 +558,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 && @@@ -571,7 -533,7 +571,7 @@@ ) ' - 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 '\'' @@@ -585,7 -547,7 +585,7 @@@ ) ' - test_expect_success PERL,SYMLINKS 'difftool --dir-diff symlinked directories' ' + test_expect_success SYMLINKS 'difftool --dir-diff symlinked directories' ' git init dirlinks && ( cd dirlinks &&