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