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)unlessdefined($diffrtn); 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'cygwin'&& 342$^One'MSWin32'&&$^One'msys', 343 tool_help =>undef, 344); 345 GetOptions('g|gui!'=> \$opts{gui}, 346'd|dir-diff'=> \$opts{dirdiff}, 347'h'=> \$opts{help}, 348'prompt!'=> \$opts{prompt}, 349'y'=>sub{$opts{prompt} =0; }, 350'symlinks'=> \$opts{symlinks}, 351'no-symlinks'=>sub{$opts{symlinks} =0; }, 352't|tool:s'=> \$opts{difftool_cmd}, 353'tool-help'=> \$opts{tool_help}, 354'x|extcmd:s'=> \$opts{extcmd}); 355 356if(defined($opts{help})) { 357 usage(0); 358} 359if(defined($opts{tool_help})) { 360 print_tool_help(); 361} 362if(defined($opts{difftool_cmd})) { 363if(length($opts{difftool_cmd}) >0) { 364$ENV{GIT_DIFF_TOOL} =$opts{difftool_cmd}; 365}else{ 366print"No <tool> given for --tool=<tool>\n"; 367 usage(1); 368} 369} 370if(defined($opts{extcmd})) { 371if(length($opts{extcmd}) >0) { 372$ENV{GIT_DIFFTOOL_EXTCMD} =$opts{extcmd}; 373}else{ 374print"No <cmd> given for --extcmd=<cmd>\n"; 375 usage(1); 376} 377} 378if($opts{gui}) { 379my$guitool= Git::config('diff.guitool'); 380if(length($guitool) >0) { 381$ENV{GIT_DIFF_TOOL} =$guitool; 382} 383} 384 385# In directory diff mode, 'git-difftool--helper' is called once 386# to compare the a/b directories. In file diff mode, 'git diff' 387# will invoke a separate instance of 'git-difftool--helper' for 388# each file that changed. 389if(defined($opts{dirdiff})) { 390 dir_diff($opts{extcmd},$opts{symlinks}); 391}else{ 392 file_diff($opts{prompt}); 393} 394} 395 396sub dir_diff 397{ 398my($extcmd,$symlinks) =@_; 399my$rc; 400my$error=0; 401my$repo= Git->repository(); 402my$workdir= find_worktree($repo); 403my($a,$b,$tmpdir,@worktree) = 404 setup_dir_diff($repo,$workdir,$symlinks); 405 406if(defined($extcmd)) { 407$rc=system($extcmd,$a,$b); 408}else{ 409$ENV{GIT_DIFFTOOL_DIRDIFF} ='true'; 410$rc=system('git','difftool--helper',$a,$b); 411} 412# If the diff including working copy files and those 413# files were modified during the diff, then the changes 414# should be copied back to the working tree. 415# Do not copy back files when symlinks are used and the 416# external tool did not replace the original link with a file. 417formy$file(@worktree) { 418next if$symlinks&& -l "$b/$file"; 419next if! -f "$b/$file"; 420 421my$diff= compare("$b/$file","$workdir/$file"); 422if($diff==0) { 423next; 424}elsif($diff== -1) { 425my$errmsg="warning: Could not compare "; 426$errmsg+="'$b/$file' with '$workdir/$file'\n"; 427warn$errmsg; 428$error=1; 429}elsif($diff==1) { 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"warning: Temporary files exist in '$tmpdir'.\n"; 440warn"warning: You may want to cleanup or recover these.\n"; 441exit(1); 442}else{ 443 exit_cleanup($tmpdir,$rc); 444} 445} 446 447sub file_diff 448{ 449my($prompt) =@_; 450 451if(defined($prompt)) { 452if($prompt) { 453$ENV{GIT_DIFFTOOL_PROMPT} ='true'; 454}else{ 455$ENV{GIT_DIFFTOOL_NO_PROMPT} ='true'; 456} 457} 458 459$ENV{GIT_PAGER} =''; 460$ENV{GIT_EXTERNAL_DIFF} ='git-difftool--helper'; 461 462# ActiveState Perl for Win32 does not implement POSIX semantics of 463# exec* system call. It just spawns the given executable and finishes 464# the starting program, exiting with code 0. 465# system will at least catch the errors returned by git diff, 466# allowing the caller of git difftool better handling of failures. 467my$rc=system('git','diff',@ARGV); 468exit($rc| ($rc>>8)); 469} 470 471main();