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