1- # !/usr/bin/env perl
1+ # !/usr/bin/perl
22# Copyright (c) 2009, 2010 David Aguilar
3+ # Copyright (c) 2012 Tim Henigan
34#
45# This is a wrapper around the GIT_EXTERNAL_DIFF-compatible
56# git-difftool--helper script.
67#
78# This script exports GIT_EXTERNAL_DIFF and GIT_PAGER for use by git.
8- # GIT_DIFFTOOL_NO_PROMPT, GIT_DIFFTOOL_PROMPT, and GIT_DIFF_TOOL
9- # are exported for use by git-difftool--helper.
9+ # The GIT_DIFF* variables are exported for use by git-difftool--helper.
1010#
1111# Any arguments that are unknown to this script are forwarded to 'git diff'.
1212
1313use 5.008;
1414use strict;
1515use warnings;
16+ use File::Basename qw( dirname) ;
17+ use File::Copy;
18+ use File::stat ;
19+ use File::Path qw( mkpath) ;
20+ use File::Temp qw( tempdir) ;
1621use Getopt::Long qw( :config pass_through) ;
1722use Git;
1823
24+ my @working_tree ;
25+ my $rc ;
26+ my $repo = Git-> repository();
27+ my $repo_path = $repo -> repo_path();
28+
1929sub usage
2030{
2131 my $exitcode = shift ;
@@ -24,15 +34,205 @@ sub usage
2434 [-x|--extcmd=<cmd>]
2535 [-g|--gui] [--no-gui]
2636 [--prompt] [-y|--no-prompt]
37+ [-d|--dir-diff]
2738 ['git diff' options]
2839USAGE
2940 exit ($exitcode );
3041}
3142
43+ sub find_worktree
44+ {
45+ # Git->repository->wc_path() does not honor changes to the working
46+ # tree location made by $ENV{GIT_WORK_TREE} or the 'core.worktree'
47+ # config variable.
48+ my $worktree ;
49+ my $env_worktree = $ENV {GIT_WORK_TREE };
50+ my $core_worktree = Git::config(' core.worktree' );
51+
52+ if (defined ($env_worktree ) and (length ($env_worktree ) > 0)) {
53+ $worktree = $env_worktree ;
54+ } elsif (defined ($core_worktree ) and (length ($core_worktree ) > 0)) {
55+ $worktree = $core_worktree ;
56+ } else {
57+ $worktree = $repo -> wc_path();
58+ }
59+
60+ return $worktree ;
61+ }
62+
63+ my $workdir = find_worktree();
64+
65+ sub setup_dir_diff
66+ {
67+ # Run the diff; exit immediately if no diff found
68+ # 'Repository' and 'WorkingCopy' must be explicitly set to insure that
69+ # if $GIT_DIR and $GIT_WORK_TREE are set in ENV, they are actually used
70+ # by Git->repository->command*.
71+ my $diffrepo = Git-> repository(Repository => $repo_path , WorkingCopy => $workdir );
72+ my $diffrtn = $diffrepo -> command_oneline(' diff' , ' --raw' , ' --no-abbrev' , ' -z' , @ARGV );
73+ exit (0) if (length ($diffrtn ) == 0);
74+
75+ # Setup temp directories
76+ my $tmpdir = tempdir(' git-diffall.XXXXX' , CLEANUP => 1, TMPDIR => 1);
77+ my $ldir = " $tmpdir /left" ;
78+ my $rdir = " $tmpdir /right" ;
79+ mkpath($ldir ) or die $! ;
80+ mkpath($rdir ) or die $! ;
81+
82+ # Build index info for left and right sides of the diff
83+ my $submodule_mode = ' 160000' ;
84+ my $symlink_mode = ' 120000' ;
85+ my $null_mode = ' 0' x 6 ;
86+ my $null_sha1 = ' 0' x 40 ;
87+ my $lindex = ' ' ;
88+ my $rindex = ' ' ;
89+ my %submodule ;
90+ my %symlink ;
91+ my @rawdiff = split (' \0' , $diffrtn );
92+
93+ my $i = 0;
94+ while ($i < $#rawdiff ) {
95+ if ($rawdiff [$i ] =~ / ^::/ ) {
96+ print " Combined diff formats ('-c' and '--cc') are not supported in directory diff mode.\n " ;
97+ exit (1);
98+ }
99+
100+ my ($lmode , $rmode , $lsha1 , $rsha1 , $status ) = split (' ' , substr ($rawdiff [$i ], 1));
101+ my $src_path = $rawdiff [$i + 1];
102+ my $dst_path ;
103+
104+ if ($status =~ / ^[CR]/ ) {
105+ $dst_path = $rawdiff [$i + 2];
106+ $i += 3;
107+ } else {
108+ $dst_path = $src_path ;
109+ $i += 2;
110+ }
111+
112+ if (($lmode eq $submodule_mode ) or ($rmode eq $submodule_mode )) {
113+ $submodule {$src_path }{left } = $lsha1 ;
114+ if ($lsha1 ne $rsha1 ) {
115+ $submodule {$dst_path }{right } = $rsha1 ;
116+ } else {
117+ $submodule {$dst_path }{right } = " $rsha1 -dirty" ;
118+ }
119+ next ;
120+ }
121+
122+ if ($lmode eq $symlink_mode ) {
123+ $symlink {$src_path }{left } = $diffrepo -> command_oneline(' show' , " $lsha1 " );
124+ }
125+
126+ if ($rmode eq $symlink_mode ) {
127+ $symlink {$dst_path }{right } = $diffrepo -> command_oneline(' show' , " $rsha1 " );
128+ }
129+
130+ if (($lmode ne $null_mode ) and ($status !~ / ^C/ )) {
131+ $lindex .= " $lmode $lsha1 \t $src_path \0 " ;
132+ }
133+
134+ if ($rmode ne $null_mode ) {
135+ if ($rsha1 ne $null_sha1 ) {
136+ $rindex .= " $rmode $rsha1 \t $dst_path \0 " ;
137+ } else {
138+ push (@working_tree , $dst_path );
139+ }
140+ }
141+ }
142+
143+ # If $GIT_DIR is not set prior to calling 'git update-index' and
144+ # 'git checkout-index', then those commands will fail if difftool
145+ # is called from a directory other than the repo root.
146+ my $must_unset_git_dir = 0;
147+ if (not defined ($ENV {GIT_DIR })) {
148+ $must_unset_git_dir = 1;
149+ $ENV {GIT_DIR } = $repo_path ;
150+ }
151+
152+ # Populate the left and right directories based on each index file
153+ my ($inpipe , $ctx );
154+ $ENV {GIT_INDEX_FILE } = " $tmpdir /lindex" ;
155+ ($inpipe , $ctx ) = $repo -> command_input_pipe(qw/ update-index -z --index-info/ );
156+ print ($inpipe $lindex );
157+ $repo -> command_close_pipe($inpipe , $ctx );
158+ $rc = system (' git' , ' checkout-index' , ' --all' , " --prefix=$ldir /" );
159+ exit ($rc | ($rc >> 8)) if ($rc != 0);
160+
161+ $ENV {GIT_INDEX_FILE } = " $tmpdir /rindex" ;
162+ ($inpipe , $ctx ) = $repo -> command_input_pipe(qw/ update-index -z --index-info/ );
163+ print ($inpipe $rindex );
164+ $repo -> command_close_pipe($inpipe , $ctx );
165+ $rc = system (' git' , ' checkout-index' , ' --all' , " --prefix=$rdir /" );
166+ exit ($rc | ($rc >> 8)) if ($rc != 0);
167+
168+ # If $GIT_DIR was explicitly set just for the update/checkout
169+ # commands, then it should be unset before continuing.
170+ delete ($ENV {GIT_DIR }) if ($must_unset_git_dir );
171+ delete ($ENV {GIT_INDEX_FILE });
172+
173+ # Changes in the working tree need special treatment since they are
174+ # not part of the index
175+ for my $file (@working_tree ) {
176+ my $dir = dirname($file );
177+ unless (-d " $rdir /$dir " ) {
178+ mkpath(" $rdir /$dir " ) or die $! ;
179+ }
180+ copy(" $workdir /$file " , " $rdir /$file " ) or die $! ;
181+ chmod (stat (" $workdir /$file " )-> mode, " $rdir /$file " ) or die $! ;
182+ }
183+
184+ # Changes to submodules require special treatment. This loop writes a
185+ # temporary file to both the left and right directories to show the
186+ # change in the recorded SHA1 for the submodule.
187+ for my $path (keys %submodule ) {
188+ if (defined ($submodule {$path }{left })) {
189+ write_to_file(" $ldir /$path " , " Subproject commit $submodule {$path }{left}" );
190+ }
191+ if (defined ($submodule {$path }{right })) {
192+ write_to_file(" $rdir /$path " , " Subproject commit $submodule {$path }{right}" );
193+ }
194+ }
195+
196+ # Symbolic links require special treatment. The standard "git diff"
197+ # shows only the link itself, not the contents of the link target.
198+ # This loop replicates that behavior.
199+ for my $path (keys %symlink ) {
200+ if (defined ($symlink {$path }{left })) {
201+ write_to_file(" $ldir /$path " , $symlink {$path }{left });
202+ }
203+ if (defined ($symlink {$path }{right })) {
204+ write_to_file(" $rdir /$path " , $symlink {$path }{right });
205+ }
206+ }
207+
208+ return ($ldir , $rdir );
209+ }
210+
211+ sub write_to_file
212+ {
213+ my $path = shift ;
214+ my $value = shift ;
215+
216+ # Make sure the path to the file exists
217+ my $dir = dirname($path );
218+ unless (-d " $dir " ) {
219+ mkpath(" $dir " ) or die $! ;
220+ }
221+
222+ # If the file already exists in that location, delete it. This
223+ # is required in the case of symbolic links.
224+ unlink (" $path " );
225+
226+ open (my $fh , ' >' , " $path " ) or die $! ;
227+ print ($fh $value );
228+ close ($fh );
229+ }
230+
32231# parse command-line options. all unrecognized options and arguments
33232# are passed through to the 'git diff' command.
34- my ($difftool_cmd , $extcmd , $gui , $help , $prompt );
233+ my ($difftool_cmd , $dirdiff , $ extcmd , $gui , $help , $prompt );
35234GetOptions(' g|gui!' => \$gui ,
235+ ' d|dir-diff' => \$dirdiff ,
36236 ' h' => \$help ,
37237 ' prompt!' => \$prompt ,
38238 ' y' => sub { $prompt = 0; },
@@ -59,28 +259,52 @@ sub usage
59259 }
60260}
61261if ($gui ) {
62- my $guitool = " " ;
262+ my $guitool = ' ' ;
63263 $guitool = Git::config(' diff.guitool' );
64264 if (length ($guitool ) > 0) {
65265 $ENV {GIT_DIFF_TOOL } = $guitool ;
66266 }
67267}
68- if (defined ($prompt )) {
69- if ($prompt ) {
70- $ENV {GIT_DIFFTOOL_PROMPT } = ' true' ;
268+
269+ # In directory diff mode, 'git-difftool--helper' is called once
270+ # to compare the a/b directories. In file diff mode, 'git diff'
271+ # will invoke a separate instance of 'git-difftool--helper' for
272+ # each file that changed.
273+ if (defined ($dirdiff )) {
274+ my ($a , $b ) = setup_dir_diff();
275+ if (defined ($extcmd )) {
276+ $rc = system ($extcmd , $a , $b );
71277 } else {
72- $ENV {GIT_DIFFTOOL_NO_PROMPT } = ' true' ;
278+ $ENV {GIT_DIFFTOOL_DIRDIFF } = ' true' ;
279+ $rc = system (' git' , ' difftool--helper' , $a , $b );
73280 }
74- }
75281
76- $ENV {GIT_PAGER } = ' ' ;
77- $ENV {GIT_EXTERNAL_DIFF } = ' git-difftool--helper' ;
78- my @command = (' git' , ' diff' , @ARGV );
79-
80- # ActiveState Perl for Win32 does not implement POSIX semantics of
81- # exec* system call. It just spawns the given executable and finishes
82- # the starting program, exiting with code 0.
83- # system will at least catch the errors returned by git diff,
84- # allowing the caller of git difftool better handling of failures.
85- my $rc = system (@command );
86- exit ($rc | ($rc >> 8));
282+ exit ($rc | ($rc >> 8)) if ($rc != 0);
283+
284+ # If the diff including working copy files and those
285+ # files were modified during the diff, then the changes
286+ # should be copied back to the working tree
287+ for my $file (@working_tree ) {
288+ copy(" $b /$file " , " $workdir /$file " ) or die $! ;
289+ chmod (stat (" $b /$file " )-> mode, " $workdir /$file " ) or die $! ;
290+ }
291+ } else {
292+ if (defined ($prompt )) {
293+ if ($prompt ) {
294+ $ENV {GIT_DIFFTOOL_PROMPT } = ' true' ;
295+ } else {
296+ $ENV {GIT_DIFFTOOL_NO_PROMPT } = ' true' ;
297+ }
298+ }
299+
300+ $ENV {GIT_PAGER } = ' ' ;
301+ $ENV {GIT_EXTERNAL_DIFF } = ' git-difftool--helper' ;
302+
303+ # ActiveState Perl for Win32 does not implement POSIX semantics of
304+ # exec* system call. It just spawns the given executable and finishes
305+ # the starting program, exiting with code 0.
306+ # system will at least catch the errors returned by git diff,
307+ # allowing the caller of git difftool better handling of failures.
308+ my $rc = system (' git' , ' diff' , @ARGV );
309+ exit ($rc | ($rc >> 8));
310+ }
0 commit comments