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