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