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