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