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