{"thread":{"id":"30311","subject":"[PATCH v15 8/9] difftool: teach difftool to handle directory diffs","startedAt":"2012-04-23T18:23:41Z","lastAt":"2012-04-23T19:04:04Z","messageCount":2,"participants":["Tim Henigan","Junio C Hamano"],"isPatch":true,"patchVersion":15,"patchTotal":9},"messages":[{"id":"189904","messageId":"1335205421-19295-1-git-send-email-tim.henigan@gmail.com","threadId":"30311","inReplyTo":null,"subject":"[PATCH v15 8/9] difftool: teach difftool to handle directory diffs","fromName":"Tim Henigan","fromEmail":"tim.henigan@gmail.com","sentAt":"2012-04-23T18:23:41Z","receivedAt":"2012-04-23T18:23:41Z","isPatch":true,"sender":{"key":"tim.henigan@gmail.com","avatar":"https://avatars.githubusercontent.com/u/42022?v=4"},"body":"When 'difftool' is called to compare a range of commits that modify\nmore than one file, it opens a separate instance of the diff tool for\neach file that changed.\n\nThe new '--dir-diff' option copies all the modified files to a temporary\nlocation and runs a directory diff on them in a single instance of the\ndiff tool.\n\nSigned-off-by: Tim Henigan <tim.henigan@gmail.com>\n---\n\nThis replaces 7fbdc6c on the th/difftool-diffall branch [1].\n\nChanges in v15:\n  - The output of 'git diff --raw' now is now split on the NULL tokens\n    first, instead of ':'.  As pointed out by Junio Hamano on the Git\n    list [2], ':' is valid filename character on some systems.\n  - Replaced double quotes with single quotes where appropriate. This\n    change was requested by David Aguilar on the Git list [3].\n  - The source of a copied file is no longer shown on the left-hand side\n    of the diff.\n\n[1]: https://github.com/gitster/git/tree/th/difftool-diffall\n[2]: http://thread.gmane.org/gmane.comp.version-control.git/196009/focus=196024\n[3]: http://thread.gmane.org/gmane.comp.version-control.git/196009/focus=196062\n\n\n Documentation/git-difftool.txt |    6 +\n git-difftool--helper.sh        |   19 ++-\n git-difftool.perl              |  266 ++++++++++++++++++++++++++++++++++++----\n t/t7800-difftool.sh            |   39 ++++++\n 4 files changed, 303 insertions(+), 27 deletions(-)\n\ndiff --git a/Documentation/git-difftool.txt b/Documentation/git-difftool.txt\nindex fe38f66..aba5e76 100644\n--- a/Documentation/git-difftool.txt\n+++ b/Documentation/git-difftool.txt\n@@ -19,6 +19,12 @@ linkgit:git-diff[1].\n \n OPTIONS\n -------\n+-d::\n+--dir-diff::\n+\tCopy the modified files to a temporary location and perform\n+\ta directory diff on them. This mode never prompts before\n+\tlaunching the diff tool.\n+\n -y::\n --no-prompt::\n \tDo not prompt before launching a diff tool.\ndiff --git a/git-difftool--helper.sh b/git-difftool--helper.sh\nindex e6558d1..3d0fe0c 100755\n--- a/git-difftool--helper.sh\n+++ b/git-difftool--helper.sh\n@@ -73,9 +73,16 @@ then\n \tfi\n fi\n \n-# Launch the merge tool on each path provided by 'git diff'\n-while test $# -gt 6\n-do\n-\tlaunch_merge_tool \"$1\" \"$2\" \"$5\"\n-\tshift 7\n-done\n+if test -n \"$GIT_DIFFTOOL_DIRDIFF\"\n+then\n+\tLOCAL=\"$1\"\n+\tREMOTE=\"$2\"\n+\trun_merge_tool \"$merge_tool\" false\n+else\n+\t# Launch the merge tool on each path provided by 'git diff'\n+\twhile test $# -gt 6\n+\tdo\n+\t\tlaunch_merge_tool \"$1\" \"$2\" \"$5\"\n+\t\tshift 7\n+\tdone\n+fi\ndiff --git a/git-difftool.perl b/git-difftool.perl\nindex aba3d2f..850ebbe 100755\n--- a/git-difftool.perl\n+++ b/git-difftool.perl\n@@ -1,21 +1,31 @@\n-#!/usr/bin/env perl\n+#!/usr/bin/perl\n # Copyright (c) 2009, 2010 David Aguilar\n+# Copyright (c) 2012 Tim Henigan\n #\n # This is a wrapper around the GIT_EXTERNAL_DIFF-compatible\n # git-difftool--helper script.\n #\n # This script exports GIT_EXTERNAL_DIFF and GIT_PAGER for use by git.\n-# GIT_DIFFTOOL_NO_PROMPT, GIT_DIFFTOOL_PROMPT, and GIT_DIFF_TOOL\n-# are exported for use by git-difftool--helper.\n+# The GIT_DIFF* variables are exported for use by git-difftool--helper.\n #\n # Any arguments that are unknown to this script are forwarded to 'git diff'.\n \n use 5.008;\n use strict;\n use warnings;\n+use File::Basename qw(dirname);\n+use File::Copy;\n+use File::stat;\n+use File::Path qw(mkpath);\n+use File::Temp qw(tempdir);\n use Getopt::Long qw(:config pass_through);\n use Git;\n \n+my @working_tree;\n+my $rc;\n+my $repo = Git->repository();\n+my $repo_path = $repo->repo_path();\n+\n sub usage\n {\n \tmy $exitcode = shift;\n@@ -24,15 +34,205 @@ usage: git difftool [-t|--tool=<tool>]\n                     [-x|--extcmd=<cmd>]\n                     [-g|--gui] [--no-gui]\n                     [--prompt] [-y|--no-prompt]\n+                    [-d|--dir-diff]\n                     ['git diff' options]\n USAGE\n \texit($exitcode);\n }\n \n+sub find_worktree\n+{\n+\t# Git->repository->wc_path() does not honor changes to the working\n+\t# tree location made by $ENV{GIT_WORK_TREE} or the 'core.worktree'\n+\t# config variable.\n+\tmy $worktree;\n+\tmy $env_worktree = $ENV{GIT_WORK_TREE};\n+\tmy $core_worktree = Git::config('core.worktree');\n+\n+\tif (defined($env_worktree) and (length($env_worktree) > 0)) {\n+\t\t$worktree = $env_worktree;\n+\t} elsif (defined($core_worktree) and (length($core_worktree) > 0)) {\n+\t\t$worktree = $core_worktree;\n+\t} else {\n+\t\t$worktree = $repo->wc_path();\n+\t}\n+\n+\treturn $worktree;\n+}\n+\n+my $workdir = find_worktree();\n+\n+sub setup_dir_diff\n+{\n+\t# Run the diff; exit immediately if no diff found\n+\t# 'Repository' and 'WorkingCopy' must be explicitly set to insure that\n+\t# if $GIT_DIR and $GIT_WORK_TREE are set in ENV, they are actually used\n+\t# by Git->repository->command*.\n+\tmy $diffrepo = Git->repository(Repository => $repo_path, WorkingCopy => $workdir);\n+\tmy $diffrtn = $diffrepo->command_oneline('diff', '--raw', '--no-abbrev', '-z', @ARGV);\n+\texit(0) if (length($diffrtn) == 0);\n+\n+\t# Setup temp directories\n+\tmy $tmpdir = tempdir('git-diffall.XXXXX', CLEANUP => 1, TMPDIR => 1);\n+\tmy $ldir = \"$tmpdir/left\";\n+\tmy $rdir = \"$tmpdir/right\";\n+\tmkpath($ldir) or die $!;\n+\tmkpath($rdir) or die $!;\n+\n+\t# Build index info for left and right sides of the diff\n+\tmy $submodule_mode = '160000';\n+\tmy $symlink_mode = '120000';\n+\tmy $null_mode = '0' x 6;\n+\tmy $null_sha1 = '0' x 40;\n+\tmy $lindex = '';\n+\tmy $rindex = '';\n+\tmy %submodule;\n+\tmy %symlink;\n+\tmy @rawdiff = split('\\0', $diffrtn);\n+\n+\tmy $i = 0;\n+\twhile ($i < $#rawdiff) {\n+\t\tif ($rawdiff[$i] =~ /^::/) {\n+\t\t\tprint \"Combined diff formats ('-c' and '--cc') are not supported in directory diff mode.\\n\";\n+\t\t\texit(1);\n+\t\t}\n+\n+\t\tmy ($lmode, $rmode, $lsha1, $rsha1, $status) = split(' ', substr($rawdiff[$i], 1));\n+\t\tmy $src_path = $rawdiff[$i + 1];\n+\t\tmy $dst_path;\n+\n+\t\tif ($status =~ /^[CR]/) {\n+\t\t\t$dst_path = $rawdiff[$i + 2];\n+\t\t\t$i += 3;\n+\t\t} else {\n+\t\t\t$dst_path = $src_path;\n+\t\t\t$i += 2;\n+\t\t}\n+\n+\t\tif (($lmode eq $submodule_mode) or ($rmode eq $submodule_mode)) {\n+\t\t\t$submodule{$src_path}{left} = $lsha1;\n+\t\t\tif ($lsha1 ne $rsha1) {\n+\t\t\t\t$submodule{$dst_path}{right} = $rsha1;\n+\t\t\t} else {\n+\t\t\t\t$submodule{$dst_path}{right} = \"$rsha1-dirty\";\n+\t\t\t}\n+\t\t\tnext;\n+\t\t}\n+\n+\t\tif ($lmode eq $symlink_mode) {\n+\t\t\t$symlink{$src_path}{left} = $diffrepo->command_oneline('show', \"$lsha1\");\n+\t\t}\n+\n+\t\tif ($rmode eq $symlink_mode) {\n+\t\t\t$symlink{$dst_path}{right} = $diffrepo->command_oneline('show', \"$rsha1\");\n+\t\t}\n+\n+\t\tif (($lmode ne $null_mode) and ($status !~ /^C/)) {\n+\t\t\t$lindex .= \"$lmode $lsha1\\t$src_path\\0\";\n+\t\t}\n+\n+\t\tif ($rmode ne $null_mode) {\n+\t\t\tif ($rsha1 ne $null_sha1) {\n+\t\t\t\t$rindex .= \"$rmode $rsha1\\t$dst_path\\0\";\n+\t\t\t} else {\n+\t\t\t\tpush(@working_tree, $dst_path);\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\t# If $GIT_DIR is not set prior to calling 'git update-index' and\n+\t# 'git checkout-index', then those commands will fail if difftool\n+\t# is called from a directory other than the repo root.\n+\tmy $must_unset_git_dir = 0;\n+\tif (not defined($ENV{GIT_DIR})) {\n+\t\t$must_unset_git_dir = 1;\n+\t\t$ENV{GIT_DIR} = $repo_path;\n+\t}\n+\n+\t# Populate the left and right directories based on each index file\n+\tmy ($inpipe, $ctx);\n+\t$ENV{GIT_INDEX_FILE} = \"$tmpdir/lindex\";\n+\t($inpipe, $ctx) = $repo->command_input_pipe(qw/update-index -z --index-info/);\n+\tprint($inpipe $lindex);\n+\t$repo->command_close_pipe($inpipe, $ctx);\n+\t$rc = system('git', 'checkout-index', '--all', \"--prefix=$ldir/\");\n+\texit($rc | ($rc >> 8)) if ($rc != 0);\n+\n+\t$ENV{GIT_INDEX_FILE} = \"$tmpdir/rindex\";\n+\t($inpipe, $ctx) = $repo->command_input_pipe(qw/update-index -z --index-info/);\n+\tprint($inpipe $rindex);\n+\t$repo->command_close_pipe($inpipe, $ctx);\n+\t$rc = system('git', 'checkout-index', '--all', \"--prefix=$rdir/\");\n+\texit($rc | ($rc >> 8)) if ($rc != 0);\n+\n+\t# If $GIT_DIR was explicitly set just for the update/checkout\n+\t# commands, then it should be unset before continuing.\n+\tdelete($ENV{GIT_DIR}) if ($must_unset_git_dir);\n+\tdelete($ENV{GIT_INDEX_FILE});\n+\n+\t# Changes in the working tree need special treatment since they are\n+\t# not part of the index\n+\tfor my $file (@working_tree) {\n+\t\tmy $dir = dirname($file);\n+\t\tunless (-d \"$rdir/$dir\") {\n+\t\t\tmkpath(\"$rdir/$dir\") or die $!;\n+\t\t}\n+\t\tcopy(\"$workdir/$file\", \"$rdir/$file\") or die $!;\n+\t\tchmod(stat(\"$workdir/$file\")->mode, \"$rdir/$file\") or die $!;\n+\t}\n+\n+\t# Changes to submodules require special treatment. This loop writes a\n+\t# temporary file to both the left and right directories to show the\n+\t# change in the recorded SHA1 for the submodule.\n+\tfor my $path (keys %submodule) {\n+\t\tif (defined($submodule{$path}{left})) {\n+\t\t\twrite_to_file(\"$ldir/$path\", \"Subproject commit $submodule{$path}{left}\");\n+\t\t}\n+\t\tif (defined($submodule{$path}{right})) {\n+\t\t\twrite_to_file(\"$rdir/$path\", \"Subproject commit $submodule{$path}{right}\");\n+\t\t}\n+\t}\n+\n+\t# Symbolic links require special treatment. The standard \"git diff\"\n+\t# shows only the link itself, not the contents of the link target.\n+\t# This loop replicates that behavior.\n+\tfor my $path (keys %symlink) {\n+\t\tif (defined($symlink{$path}{left})) {\n+\t\t\twrite_to_file(\"$ldir/$path\", $symlink{$path}{left});\n+\t\t}\n+\t\tif (defined($symlink{$path}{right})) {\n+\t\t\twrite_to_file(\"$rdir/$path\", $symlink{$path}{right});\n+\t\t}\n+\t}\n+\n+\treturn ($ldir, $rdir);\n+}\n+\n+sub write_to_file\n+{\n+\tmy $path = shift;\n+\tmy $value = shift;\n+\n+\t# Make sure the path to the file exists\n+\tmy $dir = dirname($path);\n+\tunless (-d \"$dir\") {\n+\t\tmkpath(\"$dir\") or die $!;\n+\t}\n+\n+\t# If the file already exists in that location, delete it.  This\n+\t# is required in the case of symbolic links.\n+\tunlink(\"$path\");\n+\n+\topen(my $fh, '>', \"$path\") or die $!;\n+\tprint($fh $value);\n+\tclose($fh);\n+}\n+\n # parse command-line options. all unrecognized options and arguments\n # are passed through to the 'git diff' command.\n-my ($difftool_cmd, $extcmd, $gui, $help, $prompt);\n+my ($difftool_cmd, $dirdiff, $extcmd, $gui, $help, $prompt);\n GetOptions('g|gui!' => \\$gui,\n+\t'd|dir-diff' => \\$dirdiff,\n \t'h' => \\$help,\n \t'prompt!' => \\$prompt,\n \t'y' => sub { $prompt = 0; },\n@@ -59,28 +259,52 @@ if (defined($extcmd)) {\n \t}\n }\n if ($gui) {\n-\tmy $guitool = \"\";\n+\tmy $guitool = '';\n \t$guitool = Git::config('diff.guitool');\n \tif (length($guitool) > 0) {\n \t\t$ENV{GIT_DIFF_TOOL} = $guitool;\n \t}\n }\n-if (defined($prompt)) {\n-\tif ($prompt) {\n-\t\t$ENV{GIT_DIFFTOOL_PROMPT} = 'true';\n+\n+# In directory diff mode, 'git-difftool--helper' is called once\n+# to compare the a/b directories.  In file diff mode, 'git diff'\n+# will invoke a separate instance of 'git-difftool--helper' for\n+# each file that changed.\n+if (defined($dirdiff)) {\n+\tmy ($a, $b) = setup_dir_diff();\n+\tif (defined($extcmd)) {\n+\t\t$rc = system($extcmd, $a, $b);\n \t} else {\n-\t\t$ENV{GIT_DIFFTOOL_NO_PROMPT} = 'true';\n+\t\t$ENV{GIT_DIFFTOOL_DIRDIFF} = 'true';\n+\t\t$rc = system('git', 'difftool--helper', $a, $b);\n \t}\n-}\n \n-$ENV{GIT_PAGER} = '';\n-$ENV{GIT_EXTERNAL_DIFF} = 'git-difftool--helper';\n-my @command = ('git', 'diff', @ARGV);\n-\n-# ActiveState Perl for Win32 does not implement POSIX semantics of\n-# exec* system call. It just spawns the given executable and finishes\n-# the starting program, exiting with code 0.\n-# system will at least catch the errors returned by git diff,\n-# allowing the caller of git difftool better handling of failures.\n-my $rc = system(@command);\n-exit($rc | ($rc >> 8));\n+\texit($rc | ($rc >> 8)) if ($rc != 0);\n+\n+\t# If the diff including working copy files and those\n+\t# files were modified during the diff, then the changes\n+\t# should be copied back to the working tree\n+\tfor my $file (@working_tree) {\n+\t\tcopy(\"$b/$file\", \"$workdir/$file\") or die $!;\n+\t\tchmod(stat(\"$b/$file\")->mode, \"$workdir/$file\") or die $!;\n+\t}\n+} else {\n+\tif (defined($prompt)) {\n+\t\tif ($prompt) {\n+\t\t\t$ENV{GIT_DIFFTOOL_PROMPT} = 'true';\n+\t\t} else {\n+\t\t\t$ENV{GIT_DIFFTOOL_NO_PROMPT} = 'true';\n+\t\t}\n+\t}\n+\n+\t$ENV{GIT_PAGER} = '';\n+\t$ENV{GIT_EXTERNAL_DIFF} = 'git-difftool--helper';\n+\n+\t# ActiveState Perl for Win32 does not implement POSIX semantics of\n+\t# exec* system call. It just spawns the given executable and finishes\n+\t# the starting program, exiting with code 0.\n+\t# system will at least catch the errors returned by git diff,\n+\t# allowing the caller of git difftool better handling of failures.\n+\tmy $rc = system('git', 'diff', @ARGV);\n+\texit($rc | ($rc >> 8));\n+}\ndiff --git a/t/t7800-difftool.sh b/t/t7800-difftool.sh\nindex e716d06..478c1be 100755\n--- a/t/t7800-difftool.sh\n+++ b/t/t7800-difftool.sh\n@@ -319,4 +319,43 @@ test_expect_success PERL 'say no to the second file' '\n \techo \"$diff\" | stdin_doesnot_contain br2\n '\n \n+test_expect_success PERL 'setup change in subdirectory' '\n+\tgit checkout master &&\n+\tmkdir sub &&\n+\techo master >sub/sub &&\n+\tgit add sub/sub &&\n+\tgit commit -m \"added sub/sub\" &&\n+\techo test >>file &&\n+\techo test >>sub/sub &&\n+\tgit add . &&\n+\tgit commit -m \"modified both\"\n+'\n+\n+test_expect_success PERL 'difftool -d' '\n+\tdiff=$(git difftool -d --extcmd ls branch) &&\n+\techo \"$diff\" | stdin_contains sub &&\n+\techo \"$diff\" | stdin_contains file\n+'\n+\n+test_expect_success PERL 'difftool --dir-diff' '\n+\tdiff=$(git difftool --dir-diff --extcmd ls branch) &&\n+\techo \"$diff\" | stdin_contains sub &&\n+\techo \"$diff\" | stdin_contains file\n+'\n+\n+test_expect_success PERL 'difftool --dir-diff ignores --prompt' '\n+\tdiff=$(git difftool --dir-diff --prompt --extcmd ls branch) &&\n+\techo \"$diff\" | stdin_contains sub &&\n+\techo \"$diff\" | stdin_contains file\n+'\n+\n+test_expect_success PERL 'difftool --dir-diff from subdirectory' '\n+\t(\n+\t\tcd sub &&\n+\t\tdiff=$(git difftool --dir-diff --extcmd ls branch) &&\n+\t\techo \"$diff\" | stdin_contains sub &&\n+\t\techo \"$diff\" | stdin_contains file\n+\t)\n+'\n+\n test_done\n-- \n1.7.10.rc1.29.geead\n"},{"id":"189908","messageId":"xmqq1unejmmz.fsf@junio.mtv.corp.google.com","threadId":"30311","inReplyTo":"1335205421-19295-1-git-send-email-tim.henigan@gmail.com","subject":"Re: [PATCH v15 8/9] difftool: teach difftool to handle directory diffs","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2012-04-23T19:04:04Z","receivedAt":"2012-04-23T19:04:04Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Tim Henigan <tim.henigan@gmail.com> writes:\n\n> +\twhile ($i < $#rawdiff) {\n> +\t\tif ($rawdiff[$i] =~ /^::/) {\n> +\t\t\tprint \"Combined diff formats ('-c' and '--cc') are not supported in directory diff mode.\\n\";\n> +\t\t\texit(1);\n> +\t\t}\n> +\n> +\t\tmy ($lmode, $rmode, $lsha1, $rsha1, $status) = split(' ', substr($rawdiff[$i], 1));\n> +\t\tmy $src_path = $rawdiff[$i + 1];\n> +\t\tmy $dst_path;\n> +\n> +\t\tif ($status =~ /^[CR]/) {\n> +\t\t\t$dst_path = $rawdiff[$i + 2];\n> +\t\t\t$i += 3;\n> +\t\t} else {\n> +\t\t\t$dst_path = $src_path;\n> +\t\t\t$i += 2;\n> +\t\t}\n\nThe update to this part looks good to me ;-)\n\nThanks.  Will replace what is queued.\n"}]}