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); 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 setup_dir_diff 116{ 117my($repo,$workdir,$symlinks) =@_; 118 119# Run the diff; exit immediately if no diff found 120# 'Repository' and 'WorkingCopy' must be explicitly set to insure that 121# if $GIT_DIR and $GIT_WORK_TREE are set in ENV, they are actually used 122# by Git->repository->command*. 123my$repo_path=$repo->repo_path(); 124my%repo_args= (Repository =>$repo_path, WorkingCopy =>$workdir); 125my$diffrepo= Git->repository(%repo_args); 126 127my@gitargs= ('diff','--raw','--no-abbrev','-z',@ARGV); 128my$diffrtn=$diffrepo->command_oneline(@gitargs); 129exit(0)if(length($diffrtn) ==0); 130 131# Setup temp directories 132my$tmpdir= tempdir('git-difftool.XXXXX', CLEANUP =>1, TMPDIR =>1); 133my$ldir="$tmpdir/left"; 134my$rdir="$tmpdir/right"; 135 mkpath($ldir)or die$!; 136 mkpath($rdir)or die$!; 137 138# Build index info for left and right sides of the diff 139my$submodule_mode='160000'; 140my$symlink_mode='120000'; 141my$null_mode='0' x 6; 142my$null_sha1='0' x 40; 143my$lindex=''; 144my$rindex=''; 145my%submodule; 146my%symlink; 147my@working_tree= (); 148my@rawdiff=split('\0',$diffrtn); 149 150my$i=0; 151while($i<$#rawdiff) { 152if($rawdiff[$i] =~/^::/) { 153warn<<'EOF'; 154Combined diff formats ('-c'and'--cc') are not supported in 155directory diff mode ('-d'and'--dir-diff'). 156EOF 157exit(1); 158} 159 160my($lmode,$rmode,$lsha1,$rsha1,$status) = 161split(' ',substr($rawdiff[$i],1)); 162my$src_path=$rawdiff[$i+1]; 163my$dst_path; 164 165if($status=~/^[CR]/) { 166$dst_path=$rawdiff[$i+2]; 167$i+=3; 168}else{ 169$dst_path=$src_path; 170$i+=2; 171} 172 173if($lmodeeq$submodule_modeor$rmodeeq$submodule_mode) { 174$submodule{$src_path}{left} =$lsha1; 175if($lsha1ne$rsha1) { 176$submodule{$dst_path}{right} =$rsha1; 177}else{ 178$submodule{$dst_path}{right} ="$rsha1-dirty"; 179} 180next; 181} 182 183if($lmodeeq$symlink_mode) { 184$symlink{$src_path}{left} = 185$diffrepo->command_oneline('show',"$lsha1"); 186} 187 188if($rmodeeq$symlink_mode) { 189$symlink{$dst_path}{right} = 190$diffrepo->command_oneline('show',"$rsha1"); 191} 192 193if($lmodene$null_modeand$status!~/^C/) { 194$lindex.="$lmode$lsha1\t$src_path\0"; 195} 196 197if($rmodene$null_mode) { 198if($rsha1ne$null_sha1) { 199$rindex.="$rmode$rsha1\t$dst_path\0"; 200}else{ 201push(@working_tree,$dst_path); 202} 203} 204} 205 206# If $GIT_DIR is not set prior to calling 'git update-index' and 207# 'git checkout-index', then those commands will fail if difftool 208# is called from a directory other than the repo root. 209my$must_unset_git_dir=0; 210if(not defined($ENV{GIT_DIR})) { 211$must_unset_git_dir=1; 212$ENV{GIT_DIR} =$repo_path; 213} 214 215# Populate the left and right directories based on each index file 216my($inpipe,$ctx); 217$ENV{GIT_INDEX_FILE} ="$tmpdir/lindex"; 218($inpipe,$ctx) = 219$repo->command_input_pipe(qw(update-index -z --index-info)); 220print($inpipe $lindex); 221$repo->command_close_pipe($inpipe,$ctx); 222my$rc=system('git','checkout-index','--all',"--prefix=$ldir/"); 223exit($rc| ($rc>>8))if($rc!=0); 224 225$ENV{GIT_INDEX_FILE} ="$tmpdir/rindex"; 226($inpipe,$ctx) = 227$repo->command_input_pipe(qw(update-index -z --index-info)); 228print($inpipe $rindex); 229$repo->command_close_pipe($inpipe,$ctx); 230$rc=system('git','checkout-index','--all',"--prefix=$rdir/"); 231exit($rc| ($rc>>8))if($rc!=0); 232 233# If $GIT_DIR was explicitly set just for the update/checkout 234# commands, then it should be unset before continuing. 235delete($ENV{GIT_DIR})if($must_unset_git_dir); 236delete($ENV{GIT_INDEX_FILE}); 237 238# Changes in the working tree need special treatment since they are 239# not part of the index 240formy$file(@working_tree) { 241my$dir= dirname($file); 242unless(-d "$rdir/$dir") { 243 mkpath("$rdir/$dir")or die$!; 244} 245if($symlinks) { 246symlink("$workdir/$file","$rdir/$file")or die$!; 247}else{ 248 copy("$workdir/$file","$rdir/$file")or die$!; 249my$mode=stat("$workdir/$file")->mode; 250chmod($mode,"$rdir/$file")or die$!; 251} 252} 253 254# Changes to submodules require special treatment. This loop writes a 255# temporary file to both the left and right directories to show the 256# change in the recorded SHA1 for the submodule. 257formy$path(keys%submodule) { 258if(defined($submodule{$path}{left})) { 259 write_to_file("$ldir/$path","Subproject commit$submodule{$path}{left}"); 260} 261if(defined($submodule{$path}{right})) { 262 write_to_file("$rdir/$path","Subproject commit$submodule{$path}{right}"); 263} 264} 265 266# Symbolic links require special treatment. The standard "git diff" 267# shows only the link itself, not the contents of the link target. 268# This loop replicates that behavior. 269formy$path(keys%symlink) { 270if(defined($symlink{$path}{left})) { 271 write_to_file("$ldir/$path",$symlink{$path}{left}); 272} 273if(defined($symlink{$path}{right})) { 274 write_to_file("$rdir/$path",$symlink{$path}{right}); 275} 276} 277 278return($ldir,$rdir,@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")or die$!; 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")or die$!; 297print($fh $value); 298close($fh); 299} 300 301sub main 302{ 303# parse command-line options. all unrecognized options and arguments 304# are passed through to the 'git diff' command. 305my%opts= ( 306 difftool_cmd =>undef, 307 dirdiff =>undef, 308 extcmd =>undef, 309 gui =>undef, 310 help =>undef, 311 prompt =>undef, 312 symlinks =>$^One'MSWin32'&&$^One'msys', 313 tool_help =>undef, 314); 315 GetOptions('g|gui!'=> \$opts{gui}, 316'd|dir-diff'=> \$opts{dirdiff}, 317'h'=> \$opts{help}, 318'prompt!'=> \$opts{prompt}, 319'y'=>sub{$opts{prompt} =0; }, 320'symlinks'=> \$opts{symlinks}, 321'no-symlinks'=>sub{$opts{symlinks} =0; }, 322't|tool:s'=> \$opts{difftool_cmd}, 323'tool-help'=> \$opts{tool_help}, 324'x|extcmd:s'=> \$opts{extcmd}); 325 326if(defined($opts{help})) { 327 usage(0); 328} 329if(defined($opts{tool_help})) { 330 print_tool_help(); 331} 332if(defined($opts{difftool_cmd})) { 333if(length($opts{difftool_cmd}) >0) { 334$ENV{GIT_DIFF_TOOL} =$opts{difftool_cmd}; 335}else{ 336print"No <tool> given for --tool=<tool>\n"; 337 usage(1); 338} 339} 340if(defined($opts{extcmd})) { 341if(length($opts{extcmd}) >0) { 342$ENV{GIT_DIFFTOOL_EXTCMD} =$opts{extcmd}; 343}else{ 344print"No <cmd> given for --extcmd=<cmd>\n"; 345 usage(1); 346} 347} 348if($opts{gui}) { 349my$guitool= Git::config('diff.guitool'); 350if(length($guitool) >0) { 351$ENV{GIT_DIFF_TOOL} =$guitool; 352} 353} 354 355# In directory diff mode, 'git-difftool--helper' is called once 356# to compare the a/b directories. In file diff mode, 'git diff' 357# will invoke a separate instance of 'git-difftool--helper' for 358# each file that changed. 359if(defined($opts{dirdiff})) { 360 dir_diff($opts{extcmd},$opts{symlinks}); 361}else{ 362 file_diff($opts{prompt}); 363} 364} 365 366sub dir_diff 367{ 368my($extcmd,$symlinks) =@_; 369 370my$rc; 371my$repo= Git->repository(); 372 373my$workdir= find_worktree($repo); 374my($a,$b,@worktree) = setup_dir_diff($repo,$workdir,$symlinks); 375if(defined($extcmd)) { 376$rc=system($extcmd,$a,$b); 377}else{ 378$ENV{GIT_DIFFTOOL_DIRDIFF} ='true'; 379$rc=system('git','difftool--helper',$a,$b); 380} 381 382exit($rc| ($rc>>8))if($rc!=0); 383 384# If the diff including working copy files and those 385# files were modified during the diff, then the changes 386# should be copied back to the working tree. 387# Do not copy back files when symlinks are used and the 388# external tool did not replace the original link with a file. 389formy$file(@worktree) { 390next if$symlinks&& -l "$b/$file"; 391next if! -f "$b/$file"; 392 393my$diff= compare("$b/$file","$workdir/$file"); 394if($diff==0) { 395next; 396}elsif($diff== -1) { 397my$errmsg="warning: Could not compare "; 398$errmsg+="'$b/$file' with '$workdir/$file'\n"; 399warn$errmsg; 400}elsif($diff==1) { 401 copy("$b/$file","$workdir/$file")or die$!; 402my$mode=stat("$b/$file")->mode; 403chmod($mode,"$workdir/$file")or die$!; 404} 405} 406exit(0); 407} 408 409sub file_diff 410{ 411my($prompt) =@_; 412 413if(defined($prompt)) { 414if($prompt) { 415$ENV{GIT_DIFFTOOL_PROMPT} ='true'; 416}else{ 417$ENV{GIT_DIFFTOOL_NO_PROMPT} ='true'; 418} 419} 420 421$ENV{GIT_PAGER} =''; 422$ENV{GIT_EXTERNAL_DIFF} ='git-difftool--helper'; 423 424# ActiveState Perl for Win32 does not implement POSIX semantics of 425# exec* system call. It just spawns the given executable and finishes 426# the starting program, exiting with code 0. 427# system will at least catch the errors returned by git diff, 428# allowing the caller of git difftool better handling of failures. 429my$rc=system('git','diff',@ARGV); 430exit($rc| ($rc>>8)); 431} 432 433main();