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