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