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