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 Error qw(:try); 17use File::Basename qw(dirname); 18use File::Copy; 19use File::Find; 20use File::stat; 21use File::Path qw(mkpath rmtree); 22use File::Temp qw(tempdir); 23use Getopt::Long qw(:config pass_through); 24use Git; 25 26sub usage 27{ 28my$exitcode=shift; 29print<<'USAGE'; 30usage: git difftool [-t|--tool=<tool>] [--tool-help] 31[-x|--extcmd=<cmd>] 32[-g|--gui] [--no-gui] 33[--prompt] [-y|--no-prompt] 34[-d|--dir-diff] 35['git diff' options] 36USAGE 37exit($exitcode); 38} 39 40sub find_worktree 41{ 42# Git->repository->wc_path() does not honor changes to the working 43# tree location made by $ENV{GIT_WORK_TREE} or the 'core.worktree' 44# config variable. 45return Git::command_oneline('rev-parse','--show-toplevel'); 46} 47 48sub print_tool_help 49{ 50# See the comment at the bottom of file_diff() for the reason behind 51# using system() followed by exit() instead of exec(). 52my$rc=system(qw(git mergetool --tool-help=diff)); 53exit($rc| ($rc>>8)); 54} 55 56sub exit_cleanup 57{ 58my($tmpdir,$status) =@_; 59my$errno=$!; 60 rmtree($tmpdir); 61if($statusand$errno) { 62my($package,$file,$line) =caller(); 63warn"$fileline$line:$errno\n"; 64} 65exit($status| ($status>>8)); 66} 67 68sub use_wt_file 69{ 70my($repo,$workdir,$file,$sha1) =@_; 71my$null_sha1='0' x 40; 72 73if(! -e "$workdir/$file") { 74# If the file doesn't exist in the working tree, we cannot 75# use it. 76return(0,$null_sha1); 77} 78 79my$wt_sha1=$repo->command_oneline('hash-object',"$workdir/$file"); 80my$use= ($sha1eq$null_sha1) || ($sha1eq$wt_sha1); 81return($use,$wt_sha1); 82} 83 84sub changed_files 85{ 86my($repo_path,$index,$worktree) =@_; 87$ENV{GIT_INDEX_FILE} =$index; 88$ENV{GIT_WORK_TREE} =$worktree; 89my$must_unset_git_dir=0; 90if(not defined($ENV{GIT_DIR})) { 91$must_unset_git_dir=1; 92$ENV{GIT_DIR} =$repo_path; 93} 94 95my@refreshargs= qw/update-index --really-refresh -q --unmerged/; 96my@gitargs= qw/diff-files --name-only -z/; 97try{ 98 Git::command_oneline(@refreshargs); 99} catch Git::Error::Command with {}; 100 101my$line= Git::command_oneline(@gitargs); 102my@files; 103if(defined$line) { 104@files=split('\0',$line); 105}else{ 106@files= (); 107} 108 109delete($ENV{GIT_INDEX_FILE}); 110delete($ENV{GIT_WORK_TREE}); 111delete($ENV{GIT_DIR})if($must_unset_git_dir); 112 113returnmap{$_=>1}@files; 114} 115 116sub setup_dir_diff 117{ 118my($repo,$workdir,$symlinks) =@_; 119 120# Run the diff; exit immediately if no diff found 121# 'Repository' and 'WorkingCopy' must be explicitly set to insure that 122# if $GIT_DIR and $GIT_WORK_TREE are set in ENV, they are actually used 123# by Git->repository->command*. 124my$repo_path=$repo->repo_path(); 125my%repo_args= (Repository =>$repo_path, WorkingCopy =>$workdir); 126my$diffrepo= Git->repository(%repo_args); 127 128my@gitargs= ('diff','--raw','--no-abbrev','-z',@ARGV); 129my$diffrtn=$diffrepo->command_oneline(@gitargs); 130exit(0)unlessdefined($diffrtn); 131 132# Build index info for left and right sides of the diff 133my$submodule_mode='160000'; 134my$symlink_mode='120000'; 135my$null_mode='0' x 6; 136my$null_sha1='0' x 40; 137my$lindex=''; 138my$rindex=''; 139my$wtindex=''; 140my%submodule; 141my%symlink; 142my@working_tree= (); 143my@rawdiff=split('\0',$diffrtn); 144 145my$i=0; 146while($i<$#rawdiff) { 147if($rawdiff[$i] =~/^::/) { 148warn<<'EOF'; 149Combined diff formats ('-c'and'--cc') are not supported in 150directory diff mode ('-d'and'--dir-diff'). 151EOF 152exit(1); 153} 154 155my($lmode,$rmode,$lsha1,$rsha1,$status) = 156split(' ',substr($rawdiff[$i],1)); 157my$src_path=$rawdiff[$i+1]; 158my$dst_path; 159 160if($status=~/^[CR]/) { 161$dst_path=$rawdiff[$i+2]; 162$i+=3; 163}else{ 164$dst_path=$src_path; 165$i+=2; 166} 167 168if($lmodeeq$submodule_modeor$rmodeeq$submodule_mode) { 169$submodule{$src_path}{left} =$lsha1; 170if($lsha1ne$rsha1) { 171$submodule{$dst_path}{right} =$rsha1; 172}else{ 173$submodule{$dst_path}{right} ="$rsha1-dirty"; 174} 175next; 176} 177 178if($lmodeeq$symlink_mode) { 179$symlink{$src_path}{left} = 180$diffrepo->command_oneline('show',"$lsha1"); 181} 182 183if($rmodeeq$symlink_mode) { 184$symlink{$dst_path}{right} = 185$diffrepo->command_oneline('show',"$rsha1"); 186} 187 188if($lmodene$null_modeand$status!~/^C/) { 189$lindex.="$lmode$lsha1\t$src_path\0"; 190} 191 192if($rmodene$null_mode) { 193my($use,$wt_sha1) = use_wt_file($repo,$workdir, 194$dst_path,$rsha1); 195if($use) { 196push@working_tree,$dst_path; 197$wtindex.="$rmode$wt_sha1\t$dst_path\0"; 198}else{ 199$rindex.="$rmode$rsha1\t$dst_path\0"; 200} 201} 202} 203 204# Setup temp directories 205my$tmpdir= tempdir('git-difftool.XXXXX', CLEANUP =>0, TMPDIR =>1); 206my$ldir="$tmpdir/left"; 207my$rdir="$tmpdir/right"; 208 mkpath($ldir)or exit_cleanup($tmpdir,1); 209 mkpath($rdir)or exit_cleanup($tmpdir,1); 210 211# If $GIT_DIR is not set prior to calling 'git update-index' and 212# 'git checkout-index', then those commands will fail if difftool 213# is called from a directory other than the repo root. 214my$must_unset_git_dir=0; 215if(not defined($ENV{GIT_DIR})) { 216$must_unset_git_dir=1; 217$ENV{GIT_DIR} =$repo_path; 218} 219 220# Populate the left and right directories based on each index file 221my($inpipe,$ctx); 222$ENV{GIT_INDEX_FILE} ="$tmpdir/lindex"; 223($inpipe,$ctx) = 224$repo->command_input_pipe(qw(update-index -z --index-info)); 225print($inpipe $lindex); 226$repo->command_close_pipe($inpipe,$ctx); 227 228my$rc=system('git','checkout-index','--all',"--prefix=$ldir/"); 229 exit_cleanup($tmpdir,$rc)if$rc!=0; 230 231$ENV{GIT_INDEX_FILE} ="$tmpdir/rindex"; 232($inpipe,$ctx) = 233$repo->command_input_pipe(qw(update-index -z --index-info)); 234print($inpipe $rindex); 235$repo->command_close_pipe($inpipe,$ctx); 236 237$rc=system('git','checkout-index','--all',"--prefix=$rdir/"); 238 exit_cleanup($tmpdir,$rc)if$rc!=0; 239 240$ENV{GIT_INDEX_FILE} ="$tmpdir/wtindex"; 241($inpipe,$ctx) = 242$repo->command_input_pipe(qw(update-index --info-only -z --index-info)); 243print($inpipe $wtindex); 244$repo->command_close_pipe($inpipe,$ctx); 245 246# If $GIT_DIR was explicitly set just for the update/checkout 247# commands, then it should be unset before continuing. 248delete($ENV{GIT_DIR})if($must_unset_git_dir); 249delete($ENV{GIT_INDEX_FILE}); 250 251# Changes in the working tree need special treatment since they are 252# not part of the index. Remove any trailing slash from $workdir 253# before starting to avoid double slashes in symlink targets. 254$workdir=~ s|/$||; 255formy$file(@working_tree) { 256my$dir= dirname($file); 257unless(-d "$rdir/$dir") { 258 mkpath("$rdir/$dir")or 259 exit_cleanup($tmpdir,1); 260} 261if($symlinks) { 262symlink("$workdir/$file","$rdir/$file")or 263 exit_cleanup($tmpdir,1); 264}else{ 265 copy("$workdir/$file","$rdir/$file")or 266 exit_cleanup($tmpdir,1); 267 268my$mode=stat("$workdir/$file")->mode; 269chmod($mode,"$rdir/$file")or 270 exit_cleanup($tmpdir,1); 271} 272} 273 274# Changes to submodules require special treatment. This loop writes a 275# temporary file to both the left and right directories to show the 276# change in the recorded SHA1 for the submodule. 277formy$path(keys%submodule) { 278my$ok; 279if(defined($submodule{$path}{left})) { 280$ok= write_to_file("$ldir/$path", 281"Subproject commit$submodule{$path}{left}"); 282} 283if(defined($submodule{$path}{right})) { 284$ok= write_to_file("$rdir/$path", 285"Subproject commit$submodule{$path}{right}"); 286} 287 exit_cleanup($tmpdir,1)ifnot$ok; 288} 289 290# Symbolic links require special treatment. The standard "git diff" 291# shows only the link itself, not the contents of the link target. 292# This loop replicates that behavior. 293formy$path(keys%symlink) { 294my$ok; 295if(defined($symlink{$path}{left})) { 296$ok= write_to_file("$ldir/$path", 297$symlink{$path}{left}); 298} 299if(defined($symlink{$path}{right})) { 300$ok= write_to_file("$rdir/$path", 301$symlink{$path}{right}); 302} 303 exit_cleanup($tmpdir,1)ifnot$ok; 304} 305 306return($ldir,$rdir,$tmpdir,@working_tree); 307} 308 309sub write_to_file 310{ 311my$path=shift; 312my$value=shift; 313 314# Make sure the path to the file exists 315my$dir= dirname($path); 316unless(-d "$dir") { 317 mkpath("$dir")orreturn0; 318} 319 320# If the file already exists in that location, delete it. This 321# is required in the case of symbolic links. 322unlink($path); 323 324open(my$fh,'>',$path)orreturn0; 325print($fh $value); 326close($fh); 327 328return1; 329} 330 331sub main 332{ 333# parse command-line options. all unrecognized options and arguments 334# are passed through to the 'git diff' command. 335my%opts= ( 336 difftool_cmd =>undef, 337 dirdiff =>undef, 338 extcmd =>undef, 339 gui =>undef, 340 help =>undef, 341 prompt =>undef, 342 symlinks =>$^One'cygwin'&& 343$^One'MSWin32'&&$^One'msys', 344 tool_help =>undef, 345 trust_exit_code =>undef, 346); 347 GetOptions('g|gui!'=> \$opts{gui}, 348'd|dir-diff'=> \$opts{dirdiff}, 349'h'=> \$opts{help}, 350'prompt!'=> \$opts{prompt}, 351'y'=>sub{$opts{prompt} =0; }, 352'symlinks'=> \$opts{symlinks}, 353'no-symlinks'=>sub{$opts{symlinks} =0; }, 354't|tool:s'=> \$opts{difftool_cmd}, 355'tool-help'=> \$opts{tool_help}, 356'trust-exit-code'=> \$opts{trust_exit_code}, 357'no-trust-exit-code'=>sub{$opts{trust_exit_code} =0; }, 358'x|extcmd:s'=> \$opts{extcmd}); 359 360if(defined($opts{help})) { 361 usage(0); 362} 363if(defined($opts{tool_help})) { 364 print_tool_help(); 365} 366if(defined($opts{difftool_cmd})) { 367if(length($opts{difftool_cmd}) >0) { 368$ENV{GIT_DIFF_TOOL} =$opts{difftool_cmd}; 369}else{ 370print"No <tool> given for --tool=<tool>\n"; 371 usage(1); 372} 373} 374if(defined($opts{extcmd})) { 375if(length($opts{extcmd}) >0) { 376$ENV{GIT_DIFFTOOL_EXTCMD} =$opts{extcmd}; 377}else{ 378print"No <cmd> given for --extcmd=<cmd>\n"; 379 usage(1); 380} 381} 382if($opts{gui}) { 383my$guitool= Git::config('diff.guitool'); 384if(defined($guitool) &&length($guitool) >0) { 385$ENV{GIT_DIFF_TOOL} =$guitool; 386} 387} 388 389if(!defined$opts{trust_exit_code}) { 390$opts{trust_exit_code} = Git::config_bool('difftool.trustExitCode'); 391} 392if($opts{trust_exit_code}) { 393$ENV{GIT_DIFFTOOL_TRUST_EXIT_CODE} ='true'; 394}else{ 395$ENV{GIT_DIFFTOOL_TRUST_EXIT_CODE} ='false'; 396} 397 398# In directory diff mode, 'git-difftool--helper' is called once 399# to compare the a/b directories. In file diff mode, 'git diff' 400# will invoke a separate instance of 'git-difftool--helper' for 401# each file that changed. 402if(defined($opts{dirdiff})) { 403 dir_diff($opts{extcmd},$opts{symlinks}); 404}else{ 405 file_diff($opts{prompt}); 406} 407} 408 409sub dir_diff 410{ 411my($extcmd,$symlinks) =@_; 412my$rc; 413my$error=0; 414my$repo= Git->repository(); 415my$workdir= find_worktree(); 416my($a,$b,$tmpdir,@worktree) = 417 setup_dir_diff($repo,$workdir,$symlinks); 418 419if(defined($extcmd)) { 420$rc=system($extcmd,$a,$b); 421}else{ 422$ENV{GIT_DIFFTOOL_DIRDIFF} ='true'; 423$rc=system('git','difftool--helper',$a,$b); 424} 425# If the diff including working copy files and those 426# files were modified during the diff, then the changes 427# should be copied back to the working tree. 428# Do not copy back files when symlinks are used and the 429# external tool did not replace the original link with a file. 430# 431# These hashes are loaded lazily since they aren't needed 432# in the common case of --symlinks and the difftool updating 433# files through the symlink. 434my%wt_modified; 435my%tmp_modified; 436my$indices_loaded=0; 437 438formy$file(@worktree) { 439next if$symlinks&& -l "$b/$file"; 440next if! -f "$b/$file"; 441 442if(!$indices_loaded) { 443%wt_modified= changed_files($repo->repo_path(), 444"$tmpdir/wtindex","$workdir"); 445%tmp_modified= changed_files($repo->repo_path(), 446"$tmpdir/wtindex","$b"); 447$indices_loaded=1; 448} 449 450if(exists$wt_modified{$file}and exists$tmp_modified{$file}) { 451my$errmsg="warning: Both files modified: "; 452$errmsg.="'$workdir/$file' and '$b/$file'.\n"; 453$errmsg.="warning: Working tree file has been left.\n"; 454$errmsg.="warning:\n"; 455warn$errmsg; 456$error=1; 457}elsif(exists$tmp_modified{$file}) { 458my$mode=stat("$b/$file")->mode; 459 copy("$b/$file","$workdir/$file")or 460 exit_cleanup($tmpdir,1); 461 462chmod($mode,"$workdir/$file")or 463 exit_cleanup($tmpdir,1); 464} 465} 466if($error) { 467warn"warning: Temporary files exist in '$tmpdir'.\n"; 468warn"warning: You may want to cleanup or recover these.\n"; 469exit(1); 470}else{ 471 exit_cleanup($tmpdir,$rc); 472} 473} 474 475sub file_diff 476{ 477my($prompt) =@_; 478 479if(defined($prompt)) { 480if($prompt) { 481$ENV{GIT_DIFFTOOL_PROMPT} ='true'; 482}else{ 483$ENV{GIT_DIFFTOOL_NO_PROMPT} ='true'; 484} 485} 486 487$ENV{GIT_PAGER} =''; 488$ENV{GIT_EXTERNAL_DIFF} ='git-difftool--helper'; 489 490# ActiveState Perl for Win32 does not implement POSIX semantics of 491# exec* system call. It just spawns the given executable and finishes 492# the starting program, exiting with code 0. 493# system will at least catch the errors returned by git diff, 494# allowing the caller of git difftool better handling of failures. 495my$rc=system('git','diff',@ARGV); 496exit($rc| ($rc>>8)); 497} 498 499main();