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 File::Basename qw(dirname); 17use File::Copy; 18use File::Compare; 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,$symlinks) =@_; 89my$null_sha1='0' x 40; 90 91if($sha1eq$null_sha1) { 92return1; 93}elsif(not$symlinks) { 94return0; 95} 96 97my$wt_sha1=$repo->command_oneline('hash-object',"$workdir/$file"); 98return$sha1eq$wt_sha1; 99} 100 101sub setup_dir_diff 102{ 103my($repo,$workdir,$symlinks) =@_; 104 105# Run the diff; exit immediately if no diff found 106# 'Repository' and 'WorkingCopy' must be explicitly set to insure that 107# if $GIT_DIR and $GIT_WORK_TREE are set in ENV, they are actually used 108# by Git->repository->command*. 109my$repo_path=$repo->repo_path(); 110my%repo_args= (Repository =>$repo_path, WorkingCopy =>$workdir); 111my$diffrepo= Git->repository(%repo_args); 112 113my@gitargs= ('diff','--raw','--no-abbrev','-z',@ARGV); 114my$diffrtn=$diffrepo->command_oneline(@gitargs); 115exit(0)unlessdefined($diffrtn); 116 117# Build index info for left and right sides of the diff 118my$submodule_mode='160000'; 119my$symlink_mode='120000'; 120my$null_mode='0' x 6; 121my$null_sha1='0' x 40; 122my$lindex=''; 123my$rindex=''; 124my%submodule; 125my%symlink; 126my@working_tree= (); 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$diffrepo->command_oneline('show',"$lsha1"); 165} 166 167if($rmodeeq$symlink_mode) { 168$symlink{$dst_path}{right} = 169$diffrepo->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) { 177if(use_wt_file($repo,$workdir,$dst_path,$rsha1,$symlinks)) { 178push(@working_tree,$dst_path); 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# If $GIT_DIR is not set prior to calling 'git update-index' and 193# 'git checkout-index', then those commands will fail if difftool 194# is called from a directory other than the repo root. 195my$must_unset_git_dir=0; 196if(not defined($ENV{GIT_DIR})) { 197$must_unset_git_dir=1; 198$ENV{GIT_DIR} =$repo_path; 199} 200 201# Populate the left and right directories based on each index file 202my($inpipe,$ctx); 203$ENV{GIT_INDEX_FILE} ="$tmpdir/lindex"; 204($inpipe,$ctx) = 205$repo->command_input_pipe(qw(update-index -z --index-info)); 206print($inpipe $lindex); 207$repo->command_close_pipe($inpipe,$ctx); 208 209my$rc=system('git','checkout-index','--all',"--prefix=$ldir/"); 210 exit_cleanup($tmpdir,$rc)if$rc!=0; 211 212$ENV{GIT_INDEX_FILE} ="$tmpdir/rindex"; 213($inpipe,$ctx) = 214$repo->command_input_pipe(qw(update-index -z --index-info)); 215print($inpipe $rindex); 216$repo->command_close_pipe($inpipe,$ctx); 217 218$rc=system('git','checkout-index','--all',"--prefix=$rdir/"); 219 exit_cleanup($tmpdir,$rc)if$rc!=0; 220 221# If $GIT_DIR was explicitly set just for the update/checkout 222# commands, then it should be unset before continuing. 223delete($ENV{GIT_DIR})if($must_unset_git_dir); 224delete($ENV{GIT_INDEX_FILE}); 225 226# Changes in the working tree need special treatment since they are 227# not part of the index. Remove any trailing slash from $workdir 228# before starting to avoid double slashes in symlink targets. 229$workdir=~ s|/$||; 230formy$file(@working_tree) { 231my$dir= dirname($file); 232unless(-d "$rdir/$dir") { 233 mkpath("$rdir/$dir")or 234 exit_cleanup($tmpdir,1); 235} 236if($symlinks) { 237symlink("$workdir/$file","$rdir/$file")or 238 exit_cleanup($tmpdir,1); 239}else{ 240 copy("$workdir/$file","$rdir/$file")or 241 exit_cleanup($tmpdir,1); 242 243my$mode=stat("$workdir/$file")->mode; 244chmod($mode,"$rdir/$file")or 245 exit_cleanup($tmpdir,1); 246} 247} 248 249# Changes to submodules require special treatment. This loop writes a 250# temporary file to both the left and right directories to show the 251# change in the recorded SHA1 for the submodule. 252formy$path(keys%submodule) { 253my$ok; 254if(defined($submodule{$path}{left})) { 255$ok= write_to_file("$ldir/$path", 256"Subproject commit$submodule{$path}{left}"); 257} 258if(defined($submodule{$path}{right})) { 259$ok= write_to_file("$rdir/$path", 260"Subproject commit$submodule{$path}{right}"); 261} 262 exit_cleanup($tmpdir,1)ifnot$ok; 263} 264 265# Symbolic links require special treatment. The standard "git diff" 266# shows only the link itself, not the contents of the link target. 267# This loop replicates that behavior. 268formy$path(keys%symlink) { 269my$ok; 270if(defined($symlink{$path}{left})) { 271$ok= write_to_file("$ldir/$path", 272$symlink{$path}{left}); 273} 274if(defined($symlink{$path}{right})) { 275$ok= write_to_file("$rdir/$path", 276$symlink{$path}{right}); 277} 278 exit_cleanup($tmpdir,1)ifnot$ok; 279} 280 281return($ldir,$rdir,$tmpdir,@working_tree); 282} 283 284sub write_to_file 285{ 286my$path=shift; 287my$value=shift; 288 289# Make sure the path to the file exists 290my$dir= dirname($path); 291unless(-d "$dir") { 292 mkpath("$dir")orreturn0; 293} 294 295# If the file already exists in that location, delete it. This 296# is required in the case of symbolic links. 297unlink($path); 298 299open(my$fh,'>',$path)orreturn0; 300print($fh $value); 301close($fh); 302 303return1; 304} 305 306sub main 307{ 308# parse command-line options. all unrecognized options and arguments 309# are passed through to the 'git diff' command. 310my%opts= ( 311 difftool_cmd =>undef, 312 dirdiff =>undef, 313 extcmd =>undef, 314 gui =>undef, 315 help =>undef, 316 prompt =>undef, 317 symlinks =>$^One'cygwin'&& 318$^One'MSWin32'&&$^One'msys', 319 tool_help =>undef, 320); 321 GetOptions('g|gui!'=> \$opts{gui}, 322'd|dir-diff'=> \$opts{dirdiff}, 323'h'=> \$opts{help}, 324'prompt!'=> \$opts{prompt}, 325'y'=>sub{$opts{prompt} =0; }, 326'symlinks'=> \$opts{symlinks}, 327'no-symlinks'=>sub{$opts{symlinks} =0; }, 328't|tool:s'=> \$opts{difftool_cmd}, 329'tool-help'=> \$opts{tool_help}, 330'x|extcmd:s'=> \$opts{extcmd}); 331 332if(defined($opts{help})) { 333 usage(0); 334} 335if(defined($opts{tool_help})) { 336 print_tool_help(); 337} 338if(defined($opts{difftool_cmd})) { 339if(length($opts{difftool_cmd}) >0) { 340$ENV{GIT_DIFF_TOOL} =$opts{difftool_cmd}; 341}else{ 342print"No <tool> given for --tool=<tool>\n"; 343 usage(1); 344} 345} 346if(defined($opts{extcmd})) { 347if(length($opts{extcmd}) >0) { 348$ENV{GIT_DIFFTOOL_EXTCMD} =$opts{extcmd}; 349}else{ 350print"No <cmd> given for --extcmd=<cmd>\n"; 351 usage(1); 352} 353} 354if($opts{gui}) { 355my$guitool= Git::config('diff.guitool'); 356if(defined($guitool) &&length($guitool) >0) { 357$ENV{GIT_DIFF_TOOL} =$guitool; 358} 359} 360 361# In directory diff mode, 'git-difftool--helper' is called once 362# to compare the a/b directories. In file diff mode, 'git diff' 363# will invoke a separate instance of 'git-difftool--helper' for 364# each file that changed. 365if(defined($opts{dirdiff})) { 366 dir_diff($opts{extcmd},$opts{symlinks}); 367}else{ 368 file_diff($opts{prompt}); 369} 370} 371 372sub dir_diff 373{ 374my($extcmd,$symlinks) =@_; 375my$rc; 376my$error=0; 377my$repo= Git->repository(); 378my$workdir= find_worktree($repo); 379my($a,$b,$tmpdir,@worktree) = 380 setup_dir_diff($repo,$workdir,$symlinks); 381 382if(defined($extcmd)) { 383$rc=system($extcmd,$a,$b); 384}else{ 385$ENV{GIT_DIFFTOOL_DIRDIFF} ='true'; 386$rc=system('git','difftool--helper',$a,$b); 387} 388# If the diff including working copy files and those 389# files were modified during the diff, then the changes 390# should be copied back to the working tree. 391# Do not copy back files when symlinks are used and the 392# external tool did not replace the original link with a file. 393formy$file(@worktree) { 394next if$symlinks&& -l "$b/$file"; 395next if! -f "$b/$file"; 396 397my$diff= compare("$b/$file","$workdir/$file"); 398if($diff==0) { 399next; 400}elsif($diff== -1) { 401my$errmsg="warning: Could not compare "; 402$errmsg+="'$b/$file' with '$workdir/$file'\n"; 403warn$errmsg; 404$error=1; 405}elsif($diff==1) { 406my$mode=stat("$b/$file")->mode; 407 copy("$b/$file","$workdir/$file")or 408 exit_cleanup($tmpdir,1); 409 410chmod($mode,"$workdir/$file")or 411 exit_cleanup($tmpdir,1); 412} 413} 414if($error) { 415warn"warning: Temporary files exist in '$tmpdir'.\n"; 416warn"warning: You may want to cleanup or recover these.\n"; 417exit(1); 418}else{ 419 exit_cleanup($tmpdir,$rc); 420} 421} 422 423sub file_diff 424{ 425my($prompt) =@_; 426 427if(defined($prompt)) { 428if($prompt) { 429$ENV{GIT_DIFFTOOL_PROMPT} ='true'; 430}else{ 431$ENV{GIT_DIFFTOOL_NO_PROMPT} ='true'; 432} 433} 434 435$ENV{GIT_PAGER} =''; 436$ENV{GIT_EXTERNAL_DIFF} ='git-difftool--helper'; 437 438# ActiveState Perl for Win32 does not implement POSIX semantics of 439# exec* system call. It just spawns the given executable and finishes 440# the starting program, exiting with code 0. 441# system will at least catch the errors returned by git diff, 442# allowing the caller of git difftool better handling of failures. 443my$rc=system('git','diff',@ARGV); 444exit($rc| ($rc>>8)); 445} 446 447main();