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 26my@tools; 27my@working_tree; 28my$rc; 29my$repo= Git->repository(); 30my$repo_path=$repo->repo_path(); 31 32sub usage 33{ 34my$exitcode=shift; 35print<<'USAGE'; 36usage: git difftool [-t|--tool=<tool>] [--tool-help] 37[-x|--extcmd=<cmd>] 38[-g|--gui] [--no-gui] 39[--prompt] [-y|--no-prompt] 40[-d|--dir-diff] 41['git diff' options] 42USAGE 43exit($exitcode); 44} 45 46sub find_worktree 47{ 48# Git->repository->wc_path() does not honor changes to the working 49# tree location made by $ENV{GIT_WORK_TREE} or the 'core.worktree' 50# config variable. 51my$worktree; 52my$env_worktree=$ENV{GIT_WORK_TREE}; 53my$core_worktree= Git::config('core.worktree'); 54 55if(defined($env_worktree)and(length($env_worktree) >0)) { 56$worktree=$env_worktree; 57}elsif(defined($core_worktree)and(length($core_worktree) >0)) { 58$worktree=$core_worktree; 59}else{ 60$worktree=$repo->wc_path(); 61} 62 63return$worktree; 64} 65 66my$workdir= find_worktree(); 67 68sub filter_tool_scripts 69{ 70if(-d $_) { 71if($_ne".") { 72# Ignore files in subdirectories 73$File::Find::prune =1; 74} 75}else{ 76if((-f $_) && ($_ne"defaults")) { 77push(@tools,$_); 78} 79} 80} 81 82sub print_tool_help 83{ 84my($cmd,@found,@notfound); 85my$gitpath= Git::exec_path(); 86 87 find(\&filter_tool_scripts,"$gitpath/mergetools"); 88 89foreachmy$tool(@tools) { 90$cmd="TOOL_MODE=diff"; 91$cmd.=' && . "$(git --exec-path)/git-mergetool--lib"'; 92$cmd.=" && get_merge_tool_path$tool>/dev/null 2>&1"; 93$cmd.=" && can_diff >/dev/null 2>&1"; 94if(system('sh','-c',$cmd) ==0) { 95push(@found,$tool); 96}else{ 97push(@notfound,$tool); 98} 99} 100 101print"'git difftool --tool=<tool>' may be set to one of the following:\n"; 102print"\t$_\n"for(sort(@found)); 103 104print"\nThe following tools are valid, but not currently available:\n"; 105print"\t$_\n"for(sort(@notfound)); 106 107print"\nNOTE: Some of the tools listed above only work in a windowed\n"; 108print"environment. If run in a terminal-only session, they will fail.\n"; 109 110exit(0); 111} 112 113sub setup_dir_diff 114{ 115# Run the diff; exit immediately if no diff found 116# 'Repository' and 'WorkingCopy' must be explicitly set to insure that 117# if $GIT_DIR and $GIT_WORK_TREE are set in ENV, they are actually used 118# by Git->repository->command*. 119my$diffrepo= Git->repository(Repository =>$repo_path, WorkingCopy =>$workdir); 120my$diffrtn=$diffrepo->command_oneline('diff','--raw','--no-abbrev','-z',@ARGV); 121exit(0)if(length($diffrtn) ==0); 122 123# Setup temp directories 124my$tmpdir= tempdir('git-diffall.XXXXX', CLEANUP =>1, TMPDIR =>1); 125my$ldir="$tmpdir/left"; 126my$rdir="$tmpdir/right"; 127 mkpath($ldir)or die$!; 128 mkpath($rdir)or die$!; 129 130# Build index info for left and right sides of the diff 131my$submodule_mode='160000'; 132my$symlink_mode='120000'; 133my$null_mode='0' x 6; 134my$null_sha1='0' x 40; 135my$lindex=''; 136my$rindex=''; 137my%submodule; 138my%symlink; 139my@rawdiff=split('\0',$diffrtn); 140 141my$i=0; 142while($i<$#rawdiff) { 143if($rawdiff[$i] =~/^::/) { 144print"Combined diff formats ('-c' and '--cc') are not supported in directory diff mode.\n"; 145exit(1); 146} 147 148my($lmode,$rmode,$lsha1,$rsha1,$status) =split(' ',substr($rawdiff[$i],1)); 149my$src_path=$rawdiff[$i+1]; 150my$dst_path; 151 152if($status=~/^[CR]/) { 153$dst_path=$rawdiff[$i+2]; 154$i+=3; 155}else{ 156$dst_path=$src_path; 157$i+=2; 158} 159 160if(($lmodeeq$submodule_mode)or($rmodeeq$submodule_mode)) { 161$submodule{$src_path}{left} =$lsha1; 162if($lsha1ne$rsha1) { 163$submodule{$dst_path}{right} =$rsha1; 164}else{ 165$submodule{$dst_path}{right} ="$rsha1-dirty"; 166} 167next; 168} 169 170if($lmodeeq$symlink_mode) { 171$symlink{$src_path}{left} =$diffrepo->command_oneline('show',"$lsha1"); 172} 173 174if($rmodeeq$symlink_mode) { 175$symlink{$dst_path}{right} =$diffrepo->command_oneline('show',"$rsha1"); 176} 177 178if(($lmodene$null_mode)and($status!~/^C/)) { 179$lindex.="$lmode$lsha1\t$src_path\0"; 180} 181 182if($rmodene$null_mode) { 183if($rsha1ne$null_sha1) { 184$rindex.="$rmode$rsha1\t$dst_path\0"; 185}else{ 186push(@working_tree,$dst_path); 187} 188} 189} 190 191# If $GIT_DIR is not set prior to calling 'git update-index' and 192# 'git checkout-index', then those commands will fail if difftool 193# is called from a directory other than the repo root. 194my$must_unset_git_dir=0; 195if(not defined($ENV{GIT_DIR})) { 196$must_unset_git_dir=1; 197$ENV{GIT_DIR} =$repo_path; 198} 199 200# Populate the left and right directories based on each index file 201my($inpipe,$ctx); 202$ENV{GIT_INDEX_FILE} ="$tmpdir/lindex"; 203($inpipe,$ctx) =$repo->command_input_pipe(qw/update-index -z --index-info/); 204print($inpipe $lindex); 205$repo->command_close_pipe($inpipe,$ctx); 206$rc=system('git','checkout-index','--all',"--prefix=$ldir/"); 207exit($rc| ($rc>>8))if($rc!=0); 208 209$ENV{GIT_INDEX_FILE} ="$tmpdir/rindex"; 210($inpipe,$ctx) =$repo->command_input_pipe(qw/update-index -z --index-info/); 211print($inpipe $rindex); 212$repo->command_close_pipe($inpipe,$ctx); 213$rc=system('git','checkout-index','--all',"--prefix=$rdir/"); 214exit($rc| ($rc>>8))if($rc!=0); 215 216# If $GIT_DIR was explicitly set just for the update/checkout 217# commands, then it should be unset before continuing. 218delete($ENV{GIT_DIR})if($must_unset_git_dir); 219delete($ENV{GIT_INDEX_FILE}); 220 221# Changes in the working tree need special treatment since they are 222# not part of the index 223formy$file(@working_tree) { 224my$dir= dirname($file); 225unless(-d "$rdir/$dir") { 226 mkpath("$rdir/$dir")or die$!; 227} 228 copy("$workdir/$file","$rdir/$file")or die$!; 229chmod(stat("$workdir/$file")->mode,"$rdir/$file")or die$!; 230} 231 232# Changes to submodules require special treatment. This loop writes a 233# temporary file to both the left and right directories to show the 234# change in the recorded SHA1 for the submodule. 235formy$path(keys%submodule) { 236if(defined($submodule{$path}{left})) { 237 write_to_file("$ldir/$path","Subproject commit$submodule{$path}{left}"); 238} 239if(defined($submodule{$path}{right})) { 240 write_to_file("$rdir/$path","Subproject commit$submodule{$path}{right}"); 241} 242} 243 244# Symbolic links require special treatment. The standard "git diff" 245# shows only the link itself, not the contents of the link target. 246# This loop replicates that behavior. 247formy$path(keys%symlink) { 248if(defined($symlink{$path}{left})) { 249 write_to_file("$ldir/$path",$symlink{$path}{left}); 250} 251if(defined($symlink{$path}{right})) { 252 write_to_file("$rdir/$path",$symlink{$path}{right}); 253} 254} 255 256return($ldir,$rdir); 257} 258 259sub write_to_file 260{ 261my$path=shift; 262my$value=shift; 263 264# Make sure the path to the file exists 265my$dir= dirname($path); 266unless(-d "$dir") { 267 mkpath("$dir")or die$!; 268} 269 270# If the file already exists in that location, delete it. This 271# is required in the case of symbolic links. 272unlink("$path"); 273 274open(my$fh,'>',"$path")or die$!; 275print($fh $value); 276close($fh); 277} 278 279# parse command-line options. all unrecognized options and arguments 280# are passed through to the 'git diff' command. 281my($difftool_cmd,$dirdiff,$extcmd,$gui,$help,$prompt,$tool_help); 282GetOptions('g|gui!'=> \$gui, 283'd|dir-diff'=> \$dirdiff, 284'h'=> \$help, 285'prompt!'=> \$prompt, 286'y'=>sub{$prompt=0; }, 287't|tool:s'=> \$difftool_cmd, 288'tool-help'=> \$tool_help, 289'x|extcmd:s'=> \$extcmd); 290 291if(defined($help)) { 292 usage(0); 293} 294if(defined($tool_help)) { 295 print_tool_help(); 296} 297if(defined($difftool_cmd)) { 298if(length($difftool_cmd) >0) { 299$ENV{GIT_DIFF_TOOL} =$difftool_cmd; 300}else{ 301print"No <tool> given for --tool=<tool>\n"; 302 usage(1); 303} 304} 305if(defined($extcmd)) { 306if(length($extcmd) >0) { 307$ENV{GIT_DIFFTOOL_EXTCMD} =$extcmd; 308}else{ 309print"No <cmd> given for --extcmd=<cmd>\n"; 310 usage(1); 311} 312} 313if($gui) { 314my$guitool=''; 315$guitool= Git::config('diff.guitool'); 316if(length($guitool) >0) { 317$ENV{GIT_DIFF_TOOL} =$guitool; 318} 319} 320 321# In directory diff mode, 'git-difftool--helper' is called once 322# to compare the a/b directories. In file diff mode, 'git diff' 323# will invoke a separate instance of 'git-difftool--helper' for 324# each file that changed. 325if(defined($dirdiff)) { 326my($a,$b) = setup_dir_diff(); 327if(defined($extcmd)) { 328$rc=system($extcmd,$a,$b); 329}else{ 330$ENV{GIT_DIFFTOOL_DIRDIFF} ='true'; 331$rc=system('git','difftool--helper',$a,$b); 332} 333 334exit($rc| ($rc>>8))if($rc!=0); 335 336# If the diff including working copy files and those 337# files were modified during the diff, then the changes 338# should be copied back to the working tree 339formy$file(@working_tree) { 340if(-e "$b/$file"&& compare("$b/$file","$workdir/$file")) { 341 copy("$b/$file","$workdir/$file")or die$!; 342chmod(stat("$b/$file")->mode,"$workdir/$file")or die$!; 343} 344} 345}else{ 346if(defined($prompt)) { 347if($prompt) { 348$ENV{GIT_DIFFTOOL_PROMPT} ='true'; 349}else{ 350$ENV{GIT_DIFFTOOL_NO_PROMPT} ='true'; 351} 352} 353 354$ENV{GIT_PAGER} =''; 355$ENV{GIT_EXTERNAL_DIFF} ='git-difftool--helper'; 356 357# ActiveState Perl for Win32 does not implement POSIX semantics of 358# exec* system call. It just spawns the given executable and finishes 359# the starting program, exiting with code 0. 360# system will at least catch the errors returned by git diff, 361# allowing the caller of git difftool better handling of failures. 362my$rc=system('git','diff',@ARGV); 363exit($rc| ($rc>>8)); 364}