Replace git diff-tree invocation

Gerrit currently runs git diff-tree to discover changed files.
That command might not be available in all environments, so I
replaced it with a Java based implementation.

The Java based implementation does not yet support whitespace
ignore, rename detection, or merge commits.

Change-Id: I0adfb7b68e7beba635176f1295c442ffdd397d4e
(cherry picked from commit 34f5af4c854da484db05f6570f5022761858b1be)
Change-Id: Ib76aaad601c858399a58219fdf179a55bdaacf85
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 5e6aa63..2c1d24c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -11,6 +11,52 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
+//
+// Some portions (e.g. outputDiff) below are:
+//
+// Copyright (C) 2009, Christian Halstrick <christian.halstrick@sap.com>
+// Copyright (C) 2009, Johannes E. Schindelin
+// Copyright (C) 2009, Johannes Schindelin <johannes.schindelin@gmx.de>
+// and other copyright owners as documented in the project's IP log.
+//
+// This program and the accompanying materials are made available
+// under the terms of the Eclipse Distribution License v1.0 which
+// accompanies this distribution, is reproduced below, and is
+// available at http://www.eclipse.org/org/documents/edl-v10.php
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or
+// without modification, are permitted provided that the following
+// conditions are met:
+//
+// - Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//
+// - Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following
+// disclaimer in the documentation and/or other materials provided
+// with the distribution.
+//
+// - Neither the name of the Eclipse Foundation, Inc. nor the
+// names of its contributors may be used to endorse or promote
+// products derived from this software without specific prior
+// written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
 
 package com.google.gerrit.server.patch;
 
@@ -31,8 +77,10 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
+import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.MyersDiff;
+import org.eclipse.jgit.diff.RawText;
 import org.eclipse.jgit.diff.ReplaceEdit;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Config;
@@ -48,9 +96,13 @@
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.QuotedString;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -129,64 +181,40 @@
 
     private PatchList readPatchList(final PatchListKey key,
         final Repository repo) throws IOException {
+      // TODO(jeffschu) correctly handle file renames
+      // TODO(jeffschu) correctly handle merge commits
+      // TODO(jeffschu) implement whitespace ignore
+
       final RevWalk rw = new RevWalk(repo);
       final RevCommit b = rw.parseCommit(key.getNewId());
       final AnyObjectId a = aFor(key, repo, b);
 
-      final List<String> args = new ArrayList<String>();
-      args.add("git");
-      args.add("--git-dir=.");
-      args.add("diff-tree");
-      args.add("-M");
-      switch (key.getWhitespace()) {
-        case IGNORE_NONE:
-          break;
-        case IGNORE_SPACE_AT_EOL:
-          args.add("--ignore-space-at-eol");
-          break;
-        case IGNORE_SPACE_CHANGE:
-          args.add("--ignore-space-change");
-          break;
-        case IGNORE_ALL_SPACE:
-          args.add("--ignore-all-space");
-          break;
-        default:
-          throw new IOException("Unsupported whitespace " + key.getWhitespace());
-      }
-      if (a == null /* want combined diff */) {
-        args.add("--cc");
-        args.add(b.name());
-      } else {
-        args.add("--unified=1");
-        args.add(a.name());
-        args.add(b.name());
+      if (a == null) {
+        return new PatchList(a, b, computeIntraline, new PatchListEntry[0]);
       }
 
-      final org.eclipse.jgit.patch.Patch p = new org.eclipse.jgit.patch.Patch();
-      final Process diffProcess = exec(repo, args);
-      try {
-        diffProcess.getOutputStream().close();
-        diffProcess.getErrorStream().close();
-
-        final InputStream in = diffProcess.getInputStream();
-        try {
-          p.parse(in);
-        } finally {
-          in.close();
-        }
-      } finally {
-        try {
-          final int rc = diffProcess.waitFor();
-          if (rc != 0) {
-            throw new IOException("git diff-tree exited abnormally: " + rc);
-          }
-        } catch (InterruptedException ie) {
-        }
-      }
-
-      RevTree aTree = a != null ? rw.parseTree(a) : null;
+      RevTree aTree = rw.parseTree(a);
       RevTree bTree = b.getTree();
 
+      final TreeWalk walk = new TreeWalk(repo);
+      walk.reset();
+      walk.setRecursive(true);
+      walk.addTree(aTree);
+      walk.addTree(bTree);
+      walk.setFilter(TreeFilter.ANY_DIFF);
+
+      ByteArrayOutputStream buf = new ByteArrayOutputStream();
+      PrintStream ps = new PrintStream(buf, true, "UTF-8");
+
+      while (walk.next()) {
+        outputDiff(ps, walk.getPathString(), walk.getObjectId(0), walk
+            .getFileMode(0), walk.getObjectId(1), walk.getFileMode(1), repo);
+      }
+
+      org.eclipse.jgit.patch.Patch p = new org.eclipse.jgit.patch.Patch();
+      ps.flush();
+      p.parse(new ByteArrayInputStream(buf.toByteArray()));
+
       final int cnt = p.getFiles().size();
       final PatchListEntry[] entries = new PatchListEntry[cnt];
       for (int i = 0; i < cnt; i++) {
@@ -195,6 +223,59 @@
       return new PatchList(a, b, computeIntraline, entries);
     }
 
+    private void outputDiff(PrintStream out, String path, ObjectId id1,
+        FileMode mode1, ObjectId id2, FileMode mode2, Repository repo)
+        throws IOException {
+      DiffFormatter fmt = new DiffFormatter();
+
+      String name1 = "a/" + path;
+      if (needsQuoting(name1)) {
+        name1 = QuotedString.GIT_PATH.quote(name1);
+      }
+      String name2 = "b/" + path;
+      if (needsQuoting(name2)) {
+        name2 = QuotedString.GIT_PATH.quote(name2);
+      }
+
+      out.print("diff --git " + name1 + " " + name2 + "\n");
+
+      boolean isNew = FileMode.MISSING.equals(mode1);
+      boolean isDelete = FileMode.MISSING.equals(mode2);
+
+      if (isNew) {
+        out.print("new file mode " + mode2 + "\n");
+      } else if (isDelete) {
+        out.print("deleted file mode " + mode1 + "\n");
+      } else if (!mode1.equals(mode2)) {
+        out.print("old mode " + mode1 + "\n");
+        out.print("new mode " + mode2 + "\n");
+      }
+      out.print("index " + id1.abbreviate(repo, 7).name() + ".."
+          + id2.abbreviate(repo, 7).name()
+          + (mode1.equals(mode2) ? " " + mode1 : "") + "\n");
+      out.print("--- " + (isNew ? "/dev/null" : name1) + "\n");
+      out.print("+++ " + (isDelete ? "/dev/null" : name2) + "\n");
+      RawText a = getRawText(id1, repo);
+      RawText b = getRawText(id2, repo);
+      MyersDiff diff = new MyersDiff(a, b);
+      fmt.formatEdits(out, a, b, diff.getEdits());
+    }
+
+    private static boolean needsQuoting(String path) {
+      // We should quote the path if the quoted form of the path
+      // differs by more than simply having a leading and trailing
+      // double quote added.
+      //
+      return !QuotedString.GIT_PATH.quote(path).equals('"' + path + '"');
+    }
+
+    private RawText getRawText(ObjectId id, Repository repo) throws IOException {
+      if (id.equals(ObjectId.zeroId())) {
+        return new RawText(new byte[] {});
+      }
+      return new RawText(repo.openBlob(id).getCachedBytes());
+    }
+
     private PatchListEntry newEntry(Repository repo, RevTree aTree,
         RevTree bTree, FileHeader fileHeader) throws IOException {
       final FileMode oldMode = fileHeader.getOldMode();
@@ -521,12 +602,6 @@
       }
     }
 
-    private static Process exec(final Repository repo, final List<String> args)
-        throws IOException {
-      final String[] argv = args.toArray(new String[args.size()]);
-      return Runtime.getRuntime().exec(argv, null, repo.getDirectory());
-    }
-
     private static ObjectId emptyTree(final Repository repo) throws IOException {
       return new ObjectWriter(repo).writeCanonicalTree(new byte[0]);
     }