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