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