#!/usr/bin/perl ### Copyright (C) 2010 Will Manley ### This program is free software; you can redistribute it and/or modify ### it under the terms of the GNU General Public License as published by ### the Free Software Foundation; either version 2 of the License, or ### (at your option) any later version. ### This program is distributed in the hope that it will be useful, ### but WITHOUT ANY WARRANTY; without even the implied warranty of ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ### GNU General Public License for more details. ### You should have received a copy of the GNU General Public License ### along with this program; if not, write to the Free Software ### Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA use strict; use File::Basename; use Cwd; sub safe_cmd { my $cmd = shift; my $output = `$cmd`; if ($? != 0) { die("$cmd failed with exit code $?"); } return $output; } sub safe_system { system(@_) == 0 || die ("system(" . @_ . ") failed!"); return 0; } sub trim($) { my $string = shift; $string =~ s/^\s+//; $string =~ s/\s+$//; return $string; } # Gets the value of the given config name from git if it exists, otherwise # returns the default value given as the second argument sub get_config_or_default($$) { (my $key, my $default) = @_; my $value = trim(`git config --get $key`); return ($value eq "") ? $default : $value; } # The command line possibilities are: # Compare staging area to working directory: # git meld [--options...] [--] [...] # # Compare source to staging area (source defaults to HEAD): # git meld [--options...] --cached [--] [...] # # Show the differences between source and the working directory # git meld [--options...] [--] [...] # # Show the differences between source and dest: # git meld [--options...] [--] [...] # # Same as above: # git meld [--options...] .. [--] [...] # # Show all the changes in source that have occured since it was branched # from dest: # git meld [--options...] ... [--] [...] # # This function parses the command line and extracts source and dest and returns # them as two elements in a list. sub parse_cmd(@) { my @args = @_; my $diff_opts = ""; my %opts; # Get options to be sent to diff. These all start with -- while (my $arg = shift(@args)) { if ($arg =~ m/^-/ && $arg ne "--") { $arg =~ m/^-+([^=]+)(=(.*))?$/; $opts{$1} = $3; $diff_opts += " \"$arg\""; } else { unshift(@args, $arg); last; } } my $source_tree = ""; my $dest_tree = ""; # Get tree-ishes to compare if (scalar @args != 0 && $args[0] ne "--") { my $commit1 = shift(@args); if ($commit1 =~ m/^(.*)\.\.\.(.*)$/) { $source_tree = trim(safe_cmd("git merge-base $1 $2")); $dest_tree = $2; shift(@args); } elsif ($commit1 =~ m/^(.*)\.\.(.*)$/) { $source_tree = $1; $dest_tree = $2; } else { $source_tree = $commit1; if (scalar @args == 0) { } else { my $commit2 = shift(@args); if ($commit2 ne "--") { $dest_tree = $commit2; } } } } return ($source_tree, $dest_tree, \%opts); } sub nul_seperated_string_to_list($) { my $string = shift; my @list = split(/\0/, $string); return \@list; } sub shell_escape($) { $_ = shift; s/\\/\\\\/g; s/\$/\\\$/g; s/\`/\\\`/g; return "\"$_\""; } # Copies files from a named tree into a directory # Parameters: # tree - A tree-ish # file_list - A list ref giving a list of filenames to copy # out_dir - The directory under which this tree should be reconstructed sub copy_files_named_tree($$$) { (my $tree, my $file_list, my $out_dir) = @_; if (scalar @$file_list == 0) { return; } my $escaped_file_list = join(" ", map{shell_escape($_)} @$file_list); safe_cmd("git archive $tree $escaped_file_list | tar -x -C \"$out_dir\""); } # Links the files given as a list in the first argument from the working # directory to the directory in the second argument # # These are linked rather than copied to allow the user to edit the files in the # diff viewer sub link_files_working_dir($$) { (my $file_list, my $out_dir) = @_; # Because we're diffing against the working directory we wish to create a # tree of links in the dest folder mirroring that in the repo. # TODO: Fix this so we don't have to loop over each filename somehow foreach my $filename (@$file_list) { my $dir = $filename; safe_system("mkdir", "-p", dirname("$out_dir/$filename")); safe_system("ln", "-s", cwd() . "/$filename", "$out_dir/$filename"); } } # Copies the files given as a list in the first argument from the staging area # to the directory in the second argument sub copy_files_staging_area($$) { (my $filelist, my $outdir) = @_; safe_system("git", "checkout-index", "--prefix=$outdir/", "--", @$filelist); } my $all_args = join(" ", map{ shell_escape($_) } @ARGV); (my $source_tree, my $dest_tree, my $opts) = parse_cmd(@ARGV); if (exists($opts->{"cached"}) || exists($opts->{"staged"})) { ($dest_tree eq "") || die("Only one commit can be given with the option --cached. You gave \"" . $dest_tree . "\""); if ($source_tree eq "") { $source_tree = "HEAD"; } } # At this point we have parsed two commits and want to diff them my $git_dir = trim(safe_cmd("git rev-parse --show-cdup")); if ($git_dir eq "") { $git_dir = "."; } my $tmp_dir=trim(safe_cmd("mktemp -d -t git-meld.XXXXXX")); my $source_dir = "$tmp_dir/" . (($source_tree eq "") ? "staging_area" : $source_tree); my $dest_dir = "$tmp_dir/" . (($dest_tree eq "") ? "working_dir" : $dest_tree); safe_system("mkdir -p $source_dir"); safe_system("mkdir -p $dest_dir"); my $src_changed_files = nul_seperated_string_to_list(safe_cmd("git diff -z --diff-filter=DMTUXB --name-only $all_args")); my $dest_changed_files = nul_seperated_string_to_list(safe_cmd("git diff -z --diff-filter=ACMTUXB --name-only $all_args")); if ($source_tree eq "") { copy_files_staging_area($src_changed_files, $source_dir); } else { copy_files_named_tree($source_tree, $src_changed_files, $source_dir); } if (exists($opts->{"cached"}) || exists($opts->{"staged"})) { copy_files_staging_area($dest_changed_files, $dest_dir); } elsif ($dest_tree eq "") { link_files_working_dir($dest_changed_files, $dest_dir); } else { copy_files_named_tree($dest_tree, $dest_changed_files, $dest_dir); } safe_system("chmod", "-R", "a-w", "$tmp_dir/"); my $tool = get_config_or_default("treediff.tool", "meld"); my $cmd = get_config_or_default("treediff.$tool.cmd", $tool); my $path = get_config_or_default("treediff.$tool.path", ""); safe_system("$path$cmd", "$source_dir", "$dest_dir"); safe_system("chmod", "-R", "u+w", "$tmp_dir/"); safe_system("rm", "-Rf", $tmp_dir);