Imported Upstream version 2.10.3
[platform/upstream/git.git] / git-difftool.perl
index edd0493..a5790d0 100755 (executable)
@@ -13,9 +13,9 @@
 use 5.008;
 use strict;
 use warnings;
+use Error qw(:try);
 use File::Basename qw(dirname);
 use File::Copy;
-use File::Compare;
 use File::Find;
 use File::stat;
 use File::Path qw(mkpath rmtree);
@@ -37,79 +37,12 @@ USAGE
        exit($exitcode);
 }
 
-sub find_worktree
-{
-       my ($repo) = @_;
-
-       # Git->repository->wc_path() does not honor changes to the working
-       # tree location made by $ENV{GIT_WORK_TREE} or the 'core.worktree'
-       # config variable.
-       my $worktree;
-       my $env_worktree = $ENV{GIT_WORK_TREE};
-       my $core_worktree = Git::config('core.worktree');
-
-       if (defined($env_worktree) and (length($env_worktree) > 0)) {
-               $worktree = $env_worktree;
-       } elsif (defined($core_worktree) and (length($core_worktree) > 0)) {
-               $worktree = $core_worktree;
-       } else {
-               $worktree = $repo->wc_path();
-       }
-
-       return $worktree;
-}
-
-sub filter_tool_scripts
-{
-       my ($tools) = @_;
-       if (-d $_) {
-               if ($_ ne ".") {
-                       # Ignore files in subdirectories
-                       $File::Find::prune = 1;
-               }
-       } else {
-               if ((-f $_) && ($_ ne "defaults")) {
-                       push(@$tools, $_);
-               }
-       }
-}
-
 sub print_tool_help
 {
-       my ($cmd, @found, @notfound, @tools);
-       my $gitpath = Git::exec_path();
-
-       find(sub { filter_tool_scripts(\@tools) }, "$gitpath/mergetools");
-
-       foreach my $tool (@tools) {
-               $cmd  = "TOOL_MODE=diff";
-               $cmd .= ' && . "$(git --exec-path)/git-mergetool--lib"';
-               $cmd .= " && get_merge_tool_path $tool >/dev/null 2>&1";
-               $cmd .= " && can_diff >/dev/null 2>&1";
-               if (system('sh', '-c', $cmd) == 0) {
-                       push(@found, $tool);
-               } else {
-                       push(@notfound, $tool);
-               }
-       }
-
-       print << 'EOF';
-'git difftool --tool=<tool>' may be set to one of the following:
-EOF
-       print "\t$_\n" for (sort(@found));
-
-       print << 'EOF';
-
-The following tools are valid, but not currently available:
-EOF
-       print "\t$_\n" for (sort(@notfound));
-
-       print << 'EOF';
-
-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);
+       # 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
@@ -124,20 +57,52 @@ sub exit_cleanup
        exit($status | ($status >> 8));
 }
 
-sub setup_dir_diff
+sub use_wt_file
 {
-       my ($repo, $workdir, $symlinks) = @_;
+       my ($workdir, $file, $sha1) = @_;
+       my $null_sha1 = '0' x 40;
 
-       # Run the diff; exit immediately if no diff found
-       # 'Repository' and 'WorkingCopy' must be explicitly set to insure that
-       # if $GIT_DIR and $GIT_WORK_TREE are set in ENV, they are actually used
-       # by Git->repository->command*.
-       my $repo_path = $repo->repo_path();
-       my %repo_args = (Repository => $repo_path, WorkingCopy => $workdir);
-       my $diffrepo = Git->repository(%repo_args);
+       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 = $diffrepo->command_oneline(@gitargs);
+       my $diffrtn = Git::command_oneline(@gitargs);
        exit(0) unless defined($diffrtn);
 
        # Build index info for left and right sides of the diff
@@ -147,9 +112,11 @@ sub setup_dir_diff
        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;
@@ -187,12 +154,12 @@ EOF
 
                if ($lmode eq $symlink_mode) {
                        $symlink{$src_path}{left} =
-                               $diffrepo->command_oneline('show', "$lsha1");
+                               Git::command_oneline('show', $lsha1);
                }
 
                if ($rmode eq $symlink_mode) {
                        $symlink{$dst_path}{right} =
-                               $diffrepo->command_oneline('show', "$rsha1");
+                               Git::command_oneline('show', $rsha1);
                }
 
                if ($lmode ne $null_mode and $status !~ /^C/) {
@@ -200,10 +167,17 @@ EOF
                }
 
                if ($rmode ne $null_mode) {
-                       if ($rsha1 ne $null_sha1) {
-                               $rindex .= "$rmode $rsha1\t$dst_path\0";
+                       # 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 {
-                               push(@working_tree, $dst_path);
+                               $rindex .= "$rmode $rsha1\t$dst_path\0";
                        }
                }
        }
@@ -215,42 +189,40 @@ EOF
        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
-       # 'git checkout-index', then those commands will fail if difftool
-       # is called from a directory other than the repo root.
-       my $must_unset_git_dir = 0;
-       if (not defined($ENV{GIT_DIR})) {
-               $must_unset_git_dir = 1;
-               $ENV{GIT_DIR} = $repo_path;
-       }
-
        # Populate the left and right directories based on each index file
        my ($inpipe, $ctx);
        $ENV{GIT_INDEX_FILE} = "$tmpdir/lindex";
        ($inpipe, $ctx) =
-               $repo->command_input_pipe(qw(update-index -z --index-info));
+               Git::command_input_pipe('update-index', '-z', '--index-info');
        print($inpipe $lindex);
-       $repo->command_close_pipe($inpipe, $ctx);
+       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) =
-               $repo->command_input_pipe(qw(update-index -z --index-info));
+               Git::command_input_pipe('update-index', '-z', '--index-info');
        print($inpipe $rindex);
-       $repo->command_close_pipe($inpipe, $ctx);
+       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_DIR}) if ($must_unset_git_dir);
        delete($ENV{GIT_INDEX_FILE});
 
        # Changes in the working tree need special treatment since they are
-       # not part of the index
+       # 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") {
@@ -274,7 +246,7 @@ EOF
        # 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;
+               my $ok = 0;
                if (defined($submodule{$path}{left})) {
                        $ok = write_to_file("$ldir/$path",
                                "Subproject commit $submodule{$path}{left}");
@@ -290,7 +262,7 @@ EOF
        # shows only the link itself, not the contents of the link target.
        # This loop replicates that behavior.
        for my $path (keys %symlink) {
-               my $ok;
+               my $ok = 0;
                if (defined($symlink{$path}{left})) {
                        $ok = write_to_file("$ldir/$path",
                                        $symlink{$path}{left});
@@ -341,6 +313,7 @@ sub main
                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},
@@ -351,6 +324,8 @@ sub main
                '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})) {
@@ -377,11 +352,20 @@ sub main
        }
        if ($opts{gui}) {
                my $guitool = Git::config('diff.guitool');
-               if (length($guitool) > 0) {
+               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
@@ -399,9 +383,9 @@ sub dir_diff
        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);
+       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);
@@ -414,19 +398,34 @@ sub dir_diff
        # 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";
 
-               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";
+               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 ($diff == 1) {
+               } elsif (exists $tmp_modified{$file}) {
                        my $mode = stat("$b/$file")->mode;
                        copy("$b/$file", "$workdir/$file") or
                        exit_cleanup($tmpdir, 1);