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