Subject: [PATCH] contrib/diffall: Add script to perform directory diff using external diff tool

From: Tim Henigan <[email-masked]>
Date: Tue, 13 Apr 2010 21:15:21 -0400

The existing 'git difftool' command allows the user to view git diffs
using an external diff tool.  However, if multiple files contain
differences, a separate instance of the diff tool is launched for
each one.

This script launches a single instance of the external diff tool
and performs a directory diff between the specified revisions.
The before/after files are copied to a tmp directory to do this.

The 'diff.tool' configuration option must be set for this script
to work.

The user interface matches the standard 'git diff' command.

Signed-off-by: Tim Henigan <[email-masked]>
---

This script is based on an example provided by Thomas Rast on the Git list [1].

I tested with:
  - msysgit and kdiff3 (the platform where I first needed this script)
  - cygwin git and kdiff3
  - git and meld

[1] http://thread.gmane.org/gmane.comp.version-control.git/124807

 contrib/diffall/git-diffall |  159 +++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 159 insertions(+), 0 deletions(-)
 create mode 100755 contrib/diffall/git-diffall

diff --git a/contrib/diffall/git-diffall b/contrib/diffall/git-diffall
new file mode 100755
index 0000000..c24fba9
--- /dev/null
+++ b/contrib/diffall/git-diffall
@@ -0,0 +1,159 @@
+#!/bin/sh -e
+# Copyright 2010, Tim Henigan <[email-masked]>
+#
+# Perform a directory diff between commits in the repository using
+# the external diff tool specified in the 'diff.tool' configuration
+# option.
+
+USAGE='<options> <commit>{0,2} -- <path>*
+
+--cached  Compare to the index rather than the working tree
+commit    SHA1 of a commit
+path      Limit the diff to the specified paths
+'
+
+. git-sh-setup
+
+if [ -z $(git config --get diff.tool) ]; then
+    echo "Error: The 'diff.tool' configuration option must be set."
+    usage
+fi
+
+start_dir=$(pwd)
+cd_to_toplevel      # needed to access tar utility
+
+# mktemp is not available on all platforms (missing from msysgit)
+# Use a hard-coded tmp dir if it is not available
+if [ -z $(which mktemp) ]; then
+    tmp=/tmp/git-diffall-tmp
+else
+    tmp="$(mktemp -d)"
+fi
+mkdir -p "$tmp" "$tmp"/a "$tmp"/b
+
+left=
+right=
+paths=
+path_sep=
+compare_staged=
+common_anscestor=
+
+while test $# != 0; do
+	case "$1" in
+    -h|--h|--he|--hel|--help)
+        usage
+        ;;
+    --cached)
+        compare_staged=1
+        ;;
+    --)
+        path_sep=1
+        ;;
+    -*)
+        echo Invalid option: "$1"
+        usage
+        ;;
+    *)
+        # could be commit, commit range or path limiter
+        case "$1" in
+        *...*)
+            left=${1%...*}
+            right=${1#*...}
+            common_anscestor=1
+            ;;
+        *..*)
+            left=${1%..*}
+            right=${1#*..}
+            ;;
+        *)
+            if [ -n "$path_sep" ]; then
+                if [ -z "$paths" ]; then
+                    paths=$1
+                else
+                    paths="$paths $1"
+                fi
+            elif [ -z "$left" ]; then
+                left=$1
+            elif [ -z "$right" ]; then
+                right=$1
+            else
+                if [ -z "$paths" ]; then
+                    paths=$1
+                else
+                    paths="$paths $1"
+                fi
+            fi
+            ;;
+        esac
+        ;;
+    esac
+    shift
+done
+
+# Determine the set of files which changed
+if [ -n "$left" ] && [ -n "$right" ]; then
+    if [ -n "$compare_staged" ]; then
+        usage
+    elif [ -n "$common_anscestor" ]; then
+        git diff --name-only "$left"..."$right" -- "$paths" > "$tmp"/filelist
+    else
+        git diff --name-only "$left" "$right" -- "$paths" > "$tmp"/filelist
+    fi
+elif [ -n "$left" ]; then
+    if [ -n "$compare_staged" ]; then
+        git diff --name-only --cached "$left" -- "$paths" > "$tmp"/filelist
+    else
+        git diff --name-only "$left" -- "$paths" > "$tmp"/filelist
+    fi
+else
+    if [ -n "$compare_staged" ]; then
+        git diff --name-only --cached -- "$paths" > "$tmp"/filelist
+    else
+        git diff --name-only -- "$paths" > "$tmp"/filelist
+    fi
+fi
+
+# Exit immediately if there are no diffs
+if [ ! -s "$tmp"/filelist ]; then
+    exit 0
+fi
+
+# Populate the tmp/b directory with the files to be compared
+if [ -n "$right" ]; then
+	while read name; do
+		mkdir -p "$tmp"/b/"$(dirname "$name")"
+		git show "$right":"$name" > "$tmp"/b/"$name"
+	done < "$tmp"/filelist
+elif [ -n "$compare_staged" ]; then
+	while read name; do
+		mkdir -p "$tmp"/b/"$(dirname "$name")"
+		git show :"$name" > "$tmp"/b/"$name"
+	done < "$tmp"/filelist
+else
+    tar -c -T "$tmp"/filelist | (cd "$tmp"/b && tar -x)
+fi
+
+# Populate the tmp/a directory with the files to be compared
+while read name; do
+	mkdir -p "$tmp"/a/"$(dirname "$name")"
+    if [ -n "$left" ]; then
+	    git show "$left":"$name" > "$tmp"/a/"$name"
+    else
+        if [ -n "$compare_staged" ]; then
+            git show HEAD:"$name" > "$tmp"/a/"$name"
+        else
+            git show :"$name" > "$tmp"/a/"$name"
+        fi
+    fi
+done < "$tmp"/filelist
+
+cd "$tmp"
+$(git config --get diff.tool) a b
+
+# On exit, remove the tmp directory
+cleanup () {
+    cd "$start_dir"
+	rm -rf "$tmp"
+}
+
+trap cleanup EXIT
-- 
1.7.0.3.291.g5e4f6.dirty