1#!/usr/bin/perl 2# Copyright (c) 2009, 2010 David Aguilar 3# Copyright (c) 2012 Tim Henigan 4# 5# This is a wrapper around the GIT_EXTERNAL_DIFF-compatible 6# git-difftool--helper script. 7# 8# This script exports GIT_EXTERNAL_DIFF and GIT_PAGER for use by git. 9# The GIT_DIFF* variables are exported for use by git-difftool--helper. 10# 11# Any arguments that are unknown to this script are forwarded to 'git diff'. 12 13use5.008; 14use strict; 15use warnings; 16use File::Basename qw(dirname); 17use File::Copy; 18use File::stat; 19use File::Path qw(mkpath); 20use File::Temp qw(tempdir); 21use Getopt::Long qw(:config pass_through); 22use Git; 23 24my@working_tree; 25my$rc; 26my$repo= Git->repository(); 27my$repo_path=$repo->repo_path(); 28 29sub usage 30{ 31my$exitcode=shift; 32print<<'USAGE'; 33usage: git difftool [-t|--tool=<tool>] 34[-x|--extcmd=<cmd>] 35[-g|--gui] [--no-gui] 36[--prompt] [-y|--no-prompt] 37[-d|--dir-diff] 38['git diff' options] 39USAGE 40exit($exitcode); 41} 42 43sub 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. 48my$worktree; 49my$env_worktree=$ENV{GIT_WORK_TREE}; 50my$core_worktree= Git::config('core.worktree'); 51 52if(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 60return$worktree; 61} 62 63my$workdir= find_worktree(); 64 65sub 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*. 71my$diffrepo= Git->repository(Repository =>$repo_path, WorkingCopy =>$workdir); 72my$diffrtn=$diffrepo->command_oneline('diff','--raw','--no-abbrev','-z',@ARGV); 73exit(0)if(length($diffrtn) ==0); 74 75# Setup temp directories 76my$tmpdir= tempdir('git-diffall.XXXXX', CLEANUP =>1, TMPDIR =>1); 77my$ldir="$tmpdir/left"; 78my$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 83my$submodule_mode='160000'; 84my$symlink_mode='120000'; 85my$null_mode='0' x 6; 86my$null_sha1='0' x 40; 87my$lindex=''; 88my$rindex=''; 89my%submodule; 90my%symlink; 91my@rawdiff=split('\0',$diffrtn); 92 93my$i=0; 94while($i<$#rawdiff) { 95if($rawdiff[$i] =~/^::/) { 96print"Combined diff formats ('-c' and '--cc') are not supported in directory diff mode.\n"; 97exit(1); 98} 99 100my($lmode,$rmode,$lsha1,$rsha1,$status) =split(' ',substr($rawdiff[$i],1)); 101my$src_path=$rawdiff[$i+1]; 102my$dst_path; 103 104if($status=~/^[CR]/) { 105$dst_path=$rawdiff[$i+2]; 106$i+=3; 107}else{ 108$dst_path=$src_path; 109$i+=2; 110} 111 112if(($lmodeeq$submodule_mode)or($rmodeeq$submodule_mode)) { 113$submodule{$src_path}{left} =$lsha1; 114if($lsha1ne$rsha1) { 115$submodule{$dst_path}{right} =$rsha1; 116}else{ 117$submodule{$dst_path}{right} ="$rsha1-dirty"; 118} 119next; 120} 121 122if($lmodeeq$symlink_mode) { 123$symlink{$src_path}{left} =$diffrepo->command_oneline('show',"$lsha1"); 124} 125 126if($rmodeeq$symlink_mode) { 127$symlink{$dst_path}{right} =$diffrepo->command_oneline('show',"$rsha1"); 128} 129 130if(($lmodene$null_mode)and($status!~/^C/)) { 131$lindex.="$lmode$lsha1\t$src_path\0"; 132} 133 134if($rmodene$null_mode) { 135if($rsha1ne$null_sha1) { 136$rindex.="$rmode$rsha1\t$dst_path\0"; 137}else{ 138push(@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. 146my$must_unset_git_dir=0; 147if(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 153my($inpipe,$ctx); 154$ENV{GIT_INDEX_FILE} ="$tmpdir/lindex"; 155($inpipe,$ctx) =$repo->command_input_pipe(qw/update-index -z --index-info/); 156print($inpipe $lindex); 157$repo->command_close_pipe($inpipe,$ctx); 158$rc=system('git','checkout-index','--all',"--prefix=$ldir/"); 159exit($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/); 163print($inpipe $rindex); 164$repo->command_close_pipe($inpipe,$ctx); 165$rc=system('git','checkout-index','--all',"--prefix=$rdir/"); 166exit($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. 170delete($ENV{GIT_DIR})if($must_unset_git_dir); 171delete($ENV{GIT_INDEX_FILE}); 172 173# Changes in the working tree need special treatment since they are 174# not part of the index 175formy$file(@working_tree) { 176my$dir= dirname($file); 177unless(-d "$rdir/$dir") { 178 mkpath("$rdir/$dir")or die$!; 179} 180 copy("$workdir/$file","$rdir/$file")or die$!; 181chmod(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. 187formy$path(keys%submodule) { 188if(defined($submodule{$path}{left})) { 189 write_to_file("$ldir/$path","Subproject commit$submodule{$path}{left}"); 190} 191if(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. 199formy$path(keys%symlink) { 200if(defined($symlink{$path}{left})) { 201 write_to_file("$ldir/$path",$symlink{$path}{left}); 202} 203if(defined($symlink{$path}{right})) { 204 write_to_file("$rdir/$path",$symlink{$path}{right}); 205} 206} 207 208return($ldir,$rdir); 209} 210 211sub write_to_file 212{ 213my$path=shift; 214my$value=shift; 215 216# Make sure the path to the file exists 217my$dir= dirname($path); 218unless(-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. 224unlink("$path"); 225 226open(my$fh,'>',"$path")or die$!; 227print($fh $value); 228close($fh); 229} 230 231# parse command-line options. all unrecognized options and arguments 232# are passed through to the 'git diff' command. 233my($difftool_cmd,$dirdiff,$extcmd,$gui,$help,$prompt); 234GetOptions('g|gui!'=> \$gui, 235'd|dir-diff'=> \$dirdiff, 236'h'=> \$help, 237'prompt!'=> \$prompt, 238'y'=>sub{$prompt=0; }, 239't|tool:s'=> \$difftool_cmd, 240'x|extcmd:s'=> \$extcmd); 241 242if(defined($help)) { 243 usage(0); 244} 245if(defined($difftool_cmd)) { 246if(length($difftool_cmd) >0) { 247$ENV{GIT_DIFF_TOOL} =$difftool_cmd; 248}else{ 249print"No <tool> given for --tool=<tool>\n"; 250 usage(1); 251} 252} 253if(defined($extcmd)) { 254if(length($extcmd) >0) { 255$ENV{GIT_DIFFTOOL_EXTCMD} =$extcmd; 256}else{ 257print"No <cmd> given for --extcmd=<cmd>\n"; 258 usage(1); 259} 260} 261if($gui) { 262my$guitool=''; 263$guitool= Git::config('diff.guitool'); 264if(length($guitool) >0) { 265$ENV{GIT_DIFF_TOOL} =$guitool; 266} 267} 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. 273if(defined($dirdiff)) { 274my($a,$b) = setup_dir_diff(); 275if(defined($extcmd)) { 276$rc=system($extcmd,$a,$b); 277}else{ 278$ENV{GIT_DIFFTOOL_DIRDIFF} ='true'; 279$rc=system('git','difftool--helper',$a,$b); 280} 281 282exit($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 287formy$file(@working_tree) { 288 copy("$b/$file","$workdir/$file")or die$!; 289chmod(stat("$b/$file")->mode,"$workdir/$file")or die$!; 290} 291}else{ 292if(defined($prompt)) { 293if($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. 308my$rc=system('git','diff',@ARGV); 309exit($rc| ($rc>>8)); 310}