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