Move rename detection, path following into DiffFormatter

Applications just want a quick way to configure our diff
implementation, and then just want to use it without a lot of fuss.

Move all of the rename detection logic and path following logic
out of our pgm package and into DiffFormatter itself, making it
much easier for a GUI to take advantage of the features without
duplicating a lot of code.

Change-Id: I4b54e987bb6dc804fb270cbc495fe4cae26c7b0e
Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties
index dc738d3..e7dce1b 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties
@@ -168,6 +168,7 @@
 usage_logAllPretty=format:%H %ct %P' output=log --all '--pretty=format:%H %ct %P' output
 usage_moveRenameABranch=move/rename a branch
 usage_nameStatus=show only name and status of files
+usage_noRenames=disable rename detection
 usage_outputFile=Output file
 usage_path=path
 usage_performFsckStyleChecksOnReceive=perform fsck style checks on receive
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Diff.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Diff.java
index 2be5722..b6650a4 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Diff.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Diff.java
@@ -46,9 +46,7 @@
 package org.eclipse.jgit.pgm;
 
 import java.io.BufferedOutputStream;
-import java.io.IOException;
 import java.io.PrintWriter;
-import java.util.ArrayList;
 import java.util.List;
 
 import org.eclipse.jgit.diff.DiffEntry;
@@ -62,8 +60,6 @@
 import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler;
 import org.eclipse.jgit.treewalk.AbstractTreeIterator;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
@@ -74,12 +70,10 @@ class Diff extends TextBuiltin {
 			new BufferedOutputStream(System.out));
 
 	@Argument(index = 0, metaVar = "metaVar_treeish", required = true)
-	void tree_0(final AbstractTreeIterator c) {
-		trees.add(c);
-	}
+	private AbstractTreeIterator oldTree;
 
 	@Argument(index = 1, metaVar = "metaVar_treeish", required = true)
-	private final List<AbstractTreeIterator> trees = new ArrayList<AbstractTreeIterator>();
+	private AbstractTreeIterator newTree;
 
 	@Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = PathTreeFilterHandler.class)
 	private TreeFilter pathFilter = TreeFilter.ALL;
@@ -89,7 +83,12 @@ void tree_0(final AbstractTreeIterator c) {
 	boolean showPatch;
 
 	@Option(name = "-M", usage = "usage_detectRenames")
-	private boolean detectRenames;
+	private Boolean detectRenames;
+
+	@Option(name = "--no-renames", usage = "usage_noRenames")
+	void noRenames(@SuppressWarnings("unused") boolean on) {
+		detectRenames = Boolean.FALSE;
+	}
 
 	@Option(name = "-l", usage = "usage_renameLimit")
 	private Integer renameLimit;
@@ -136,16 +135,27 @@ void abbrev(@SuppressWarnings("unused") boolean on) {
 
 	@Override
 	protected void run() throws Exception {
-		List<DiffEntry> files = scan();
+		diffFmt.setRepository(db);
+		try {
+			diffFmt.setProgressMonitor(new TextProgressMonitor());
+			diffFmt.setPathFilter(pathFilter);
+			if (detectRenames != null)
+				diffFmt.setDetectRenames(detectRenames.booleanValue());
+			if (renameLimit != null && diffFmt.isDetectRenames()) {
+				RenameDetector rd = diffFmt.getRenameDetector();
+				rd.setRenameLimit(renameLimit.intValue());
+			}
 
-		if (showNameAndStatusOnly) {
-			nameStatus(out, files);
-			out.flush();
+			if (showNameAndStatusOnly) {
+				nameStatus(out, diffFmt.scan(oldTree, newTree));
+				out.flush();
 
-		} else {
-			diffFmt.setRepository(db);
-			diffFmt.format(files);
-			diffFmt.flush();
+			} else {
+				diffFmt.format(oldTree, newTree);
+				diffFmt.flush();
+			}
+		} finally {
+			diffFmt.release();
 		}
 	}
 
@@ -174,23 +184,4 @@ static void nameStatus(PrintWriter out, List<DiffEntry> files) {
 			}
 		}
 	}
-
-	private List<DiffEntry> scan() throws IOException {
-		final TreeWalk walk = new TreeWalk(db);
-		walk.reset();
-		walk.setRecursive(true);
-		for (final AbstractTreeIterator i : trees)
-			walk.addTree(i);
-		walk.setFilter(AndTreeFilter.create(TreeFilter.ANY_DIFF, pathFilter));
-
-		List<DiffEntry> files = DiffEntry.scan(walk);
-		if (detectRenames) {
-			RenameDetector rd = new RenameDetector(db);
-			if (renameLimit != null)
-				rd.setRenameLimit(renameLimit.intValue());
-			rd.addAll(files);
-			files = rd.compute(new TextProgressMonitor());
-		}
-		return files;
-	}
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java
index d0ae22a..2b29f73 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Log.java
@@ -51,32 +51,25 @@
 import java.text.MessageFormat;
 import java.text.SimpleDateFormat;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Iterator;
-import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.TimeZone;
 
-import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.diff.DiffFormatter;
 import org.eclipse.jgit.diff.RawTextIgnoreAllWhitespace;
 import org.eclipse.jgit.diff.RawTextIgnoreLeadingWhitespace;
 import org.eclipse.jgit.diff.RawTextIgnoreTrailingWhitespace;
 import org.eclipse.jgit.diff.RawTextIgnoreWhitespaceChange;
 import org.eclipse.jgit.diff.RenameDetector;
-import org.eclipse.jgit.diff.DiffEntry.ChangeType;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.revwalk.FollowFilter;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
-import org.eclipse.jgit.treewalk.filter.TreeFilter;
 import org.kohsuke.args4j.Option;
 
 @Command(common = true, usage = "usage_viewCommitHistory")
@@ -98,7 +91,12 @@ class Log extends RevWalkTextBuiltin {
 	boolean showPatch;
 
 	@Option(name = "-M", usage = "usage_detectRenames")
-	private boolean detectRenames;
+	private Boolean detectRenames;
+
+	@Option(name = "--no-renames", usage = "usage_noRenames")
+	void noRenames(@SuppressWarnings("unused") boolean on) {
+		detectRenames = Boolean.FALSE;
+	}
 
 	@Option(name = "-l", usage = "usage_renameLimit")
 	private Integer renameLimit;
@@ -156,6 +154,24 @@ protected RevWalk createWalk() {
 	}
 
 	@Override
+	protected void run() throws Exception {
+		diffFmt.setRepository(db);
+		try {
+			diffFmt.setPathFilter(pathFilter);
+			if (detectRenames != null)
+				diffFmt.setDetectRenames(detectRenames.booleanValue());
+			if (renameLimit != null && diffFmt.isDetectRenames()) {
+				RenameDetector rd = diffFmt.getRenameDetector();
+				rd.setRenameLimit(renameLimit.intValue());
+			}
+
+			super.run();
+		} finally {
+			diffFmt.release();
+		}
+	}
+
+	@Override
 	protected void show(final RevCommit c) throws Exception {
 		out.print(CLIText.get().commitLabel);
 		out.print(" ");
@@ -196,71 +212,16 @@ protected void show(final RevCommit c) throws Exception {
 	}
 
 	private void showDiff(RevCommit c) throws IOException {
-		final TreeWalk tw = new TreeWalk(db);
-		tw.setRecursive(true);
-		tw.reset();
-		tw.addTree(c.getParent(0).getTree());
-		tw.addTree(c.getTree());
-		tw.setFilter(AndTreeFilter.create(pathFilter, TreeFilter.ANY_DIFF));
+		final RevTree a = c.getParent(0).getTree();
+		final RevTree b = c.getTree();
 
-		List<DiffEntry> files = DiffEntry.scan(tw);
-		if (pathFilter instanceof FollowFilter && isAdd(files)) {
-			// The file we are following was added here, find where it
-			// came from so we can properly show the rename or copy,
-			// then continue digging backwards.
-			//
-			tw.reset();
-			tw.addTree(c.getParent(0).getTree());
-			tw.addTree(c.getTree());
-			tw.setFilter(TreeFilter.ANY_DIFF);
-			files = updateFollowFilter(detectRenames(DiffEntry.scan(tw)));
-
-		} else if (detectRenames)
-			files = detectRenames(files);
-
-		if (showNameAndStatusOnly) {
-			Diff.nameStatus(out, files);
-
-		} else {
-			diffFmt.setRepository(db);
-			diffFmt.format(files);
+		if (showNameAndStatusOnly)
+			Diff.nameStatus(out, diffFmt.scan(a, b));
+		else {
+			diffFmt.format(a, b);
 			diffFmt.flush();
 		}
 		out.println();
-	}
-
-	private List<DiffEntry> detectRenames(List<DiffEntry> files)
-			throws IOException {
-		RenameDetector rd = new RenameDetector(db);
-		if (renameLimit != null)
-			rd.setRenameLimit(renameLimit.intValue());
-		rd.addAll(files);
-		return rd.compute();
-	}
-
-	private boolean isAdd(List<DiffEntry> files) {
-		String oldPath = ((FollowFilter) pathFilter).getPath();
-		for (DiffEntry ent : files) {
-			if (ent.getChangeType() == ChangeType.ADD
-					&& ent.getNewPath().equals(oldPath))
-				return true;
-		}
-		return false;
-	}
-
-	private List<DiffEntry> updateFollowFilter(List<DiffEntry> files) {
-		String oldPath = ((FollowFilter) pathFilter).getPath();
-		for (DiffEntry ent : files) {
-			if (isRename(ent) && ent.getNewPath().equals(oldPath)) {
-				pathFilter = FollowFilter.create(ent.getOldPath());
-				return Collections.singletonList(ent);
-			}
-		}
-		return Collections.emptyList();
-	}
-
-	private static boolean isRename(DiffEntry ent) {
-		return ent.getChangeType() == ChangeType.RENAME
-				|| ent.getChangeType() == ChangeType.COPY;
+		out.flush();
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterTest.java
index d7a10e4..eefbefb 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterTest.java
@@ -77,10 +77,17 @@ public void setUp() throws Exception {
 		df.setAbbreviationLength(8);
 	}
 
+	@Override
+	public void tearDown() throws Exception {
+		if (df != null)
+			df.release();
+		super.tearDown();
+	}
+
 	public void testCreateFileHeader_Add() throws Exception {
 		ObjectId adId = blob("a\nd\n");
 		DiffEntry ent = DiffEntry.add("FOO", adId);
-		FileHeader fh = df.createFileHeader(ent);
+		FileHeader fh = df.toFileHeader(ent);
 
 		String diffHeader = "diff --git a/FOO b/FOO\n" //
 				+ "new file mode " + REGULAR_FILE + "\n"
@@ -115,7 +122,7 @@ public void testCreateFileHeader_Add() throws Exception {
 	public void testCreateFileHeader_Delete() throws Exception {
 		ObjectId adId = blob("a\nd\n");
 		DiffEntry ent = DiffEntry.delete("FOO", adId);
-		FileHeader fh = df.createFileHeader(ent);
+		FileHeader fh = df.toFileHeader(ent);
 
 		String diffHeader = "diff --git a/FOO b/FOO\n" //
 				+ "deleted file mode " + REGULAR_FILE + "\n"
@@ -158,7 +165,7 @@ public void testCreateFileHeader_Modify() throws Exception {
 
 		DiffEntry mod = DiffEntry.pair(ChangeType.MODIFY, ad, abcd, 0);
 
-		FileHeader fh = df.createFileHeader(mod);
+		FileHeader fh = df.toFileHeader(mod);
 
 		assertEquals(diffHeader, RawParseUtils.decode(fh.getBuffer()));
 		assertEquals(0, fh.getStartOffset());
@@ -193,7 +200,7 @@ public void testCreateFileHeader_Binary() throws Exception {
 
 		DiffEntry mod = DiffEntry.pair(ChangeType.MODIFY, ad, abcd, 0);
 
-		FileHeader fh = df.createFileHeader(mod);
+		FileHeader fh = df.toFileHeader(mod);
 
 		assertEquals(diffHeader, RawParseUtils.decode(fh.getBuffer()));
 		assertEquals(FileHeader.PatchType.BINARY, fh.getPatchType());
@@ -218,7 +225,7 @@ public void testCreateFileHeader_GitLink() throws Exception {
 
 		DiffEntry mod = DiffEntry.pair(ChangeType.MODIFY, ad, abcd, 0);
 
-		FileHeader fh = df.createFileHeader(mod);
+		FileHeader fh = df.toFileHeader(mod);
 
 		assertEquals(diffHeader, RawParseUtils.decode(fh.getBuffer()));
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffConfig.java
index 91b7467..4b86f55 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffConfig.java
@@ -55,12 +55,28 @@ public DiffConfig parse(final Config cfg) {
 		}
 	};
 
+	private final boolean noPrefix;
+
+	private final boolean renames;
+
 	private final int renameLimit;
 
 	private DiffConfig(final Config rc) {
+		noPrefix = rc.getBoolean("diff", "noprefix", false);
+		renames = rc.getBoolean("diff", "renames", false);
 		renameLimit = rc.getInt("diff", "renamelimit", 200);
 	}
 
+	/** @return true if the prefix "a/" and "b/" should be suppressed. */
+	public boolean isNoPrefix() {
+		return noPrefix;
+	}
+
+	/** @return true if rename detection is enabled by default. */
+	public boolean isRenameDetectionEnabled() {
+		return renames;
+	}
+
 	/** @return limit on number of paths to perform inexact rename detection. */
 	public int getRenameLimit() {
 		return renameLimit;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java
index cb145e4..3590ef5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java
@@ -45,6 +45,7 @@
 package org.eclipse.jgit.diff;
 
 import static org.eclipse.jgit.diff.DiffEntry.ChangeType.ADD;
+import static org.eclipse.jgit.diff.DiffEntry.ChangeType.COPY;
 import static org.eclipse.jgit.diff.DiffEntry.ChangeType.DELETE;
 import static org.eclipse.jgit.diff.DiffEntry.ChangeType.MODIFY;
 import static org.eclipse.jgit.diff.DiffEntry.ChangeType.RENAME;
@@ -56,6 +57,7 @@
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 
 import org.eclipse.jgit.JGitText;
@@ -65,16 +67,26 @@
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.patch.FileHeader;
 import org.eclipse.jgit.patch.HunkHeader;
 import org.eclipse.jgit.patch.FileHeader.PatchType;
+import org.eclipse.jgit.revwalk.FollowFilter;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.storage.pack.PackConfig;
+import org.eclipse.jgit.treewalk.AbstractTreeIterator;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
 import org.eclipse.jgit.util.QuotedString;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
 
@@ -96,6 +108,8 @@ public class DiffFormatter {
 
 	private Repository db;
 
+	private ObjectReader reader;
+
 	private int context = 3;
 
 	private int abbreviationLength = 7;
@@ -108,6 +122,12 @@ public class DiffFormatter {
 
 	private String newPrefix = "b/";
 
+	private TreeFilter pathFilter = TreeFilter.ALL;
+
+	private RenameDetector renameDetector;
+
+	private ProgressMonitor progressMonitor;
+
 	/**
 	 * Create a new formatter with a default level of context.
 	 *
@@ -128,11 +148,25 @@ protected OutputStream getOutputStream() {
 	/**
 	 * Set the repository the formatter can load object contents from.
 	 *
+	 * Once a repository has been set, the formatter must be released to ensure
+	 * the internal ObjectReader is able to release its resources.
+	 *
 	 * @param repository
 	 *            source repository holding referenced objects.
 	 */
 	public void setRepository(Repository repository) {
+		if (reader != null)
+			reader.release();
+
 		db = repository;
+		reader = db.newObjectReader();
+
+		DiffConfig dc = db.getConfig().get(DiffConfig.KEY);
+		if (dc.isNoPrefix()) {
+			setOldPrefix("");
+			setNewPrefix("");
+		}
+		setDetectRenames(dc.isRenameDetectionEnabled());
 	}
 
 	/**
@@ -220,6 +254,64 @@ public void setNewPrefix(String prefix) {
 		newPrefix = prefix;
 	}
 
+	/** @return true if rename detection is enabled. */
+	public boolean isDetectRenames() {
+		return renameDetector != null;
+	}
+
+	/**
+	 * Enable or disable rename detection.
+	 *
+	 * Before enabling rename detection the repository must be set with
+	 * {@link #setRepository(Repository)}. Once enabled the detector can be
+	 * configured away from its defaults by obtaining the instance directly from
+	 * {@link #getRenameDetector()} and invoking configuration.
+	 *
+	 * @param on
+	 *            if rename detection should be enabled.
+	 */
+	public void setDetectRenames(boolean on) {
+		if (on && renameDetector == null) {
+			assertHaveRepository();
+			renameDetector = new RenameDetector(db);
+		} else if (!on)
+			renameDetector = null;
+	}
+
+	/** @return the rename detector if rename detection is enabled. */
+	public RenameDetector getRenameDetector() {
+		return renameDetector;
+	}
+
+	/**
+	 * Set the progress monitor for long running rename detection.
+	 *
+	 * @param pm
+	 *            progress monitor to receive rename detection status through.
+	 */
+	public void setProgressMonitor(ProgressMonitor pm) {
+		progressMonitor = pm;
+	}
+
+	/**
+	 * Set the filter to produce only specific paths.
+	 *
+	 * If the filter is an instance of {@link FollowFilter}, the filter path
+	 * will be updated during successive scan or format invocations. The updated
+	 * path can be obtained from {@link #getPathFilter()}.
+	 *
+	 * @param filter
+	 *            the tree filter to apply.
+	 */
+	public void setPathFilter(TreeFilter filter) {
+		pathFilter = filter != null ? filter : TreeFilter.ALL;
+	}
+
+	/** @return the current path filter. */
+	public TreeFilter getPathFilter() {
+		return pathFilter;
+	}
+
 	/**
 	 * Flush the underlying output stream of this formatter.
 	 *
@@ -230,6 +322,208 @@ public void flush() throws IOException {
 		out.flush();
 	}
 
+	/** Release the internal ObjectReader state. */
+	public void release() {
+		if (reader != null)
+			reader.release();
+	}
+
+	/**
+	 * Determine the differences between two trees.
+	 *
+	 * No output is created, instead only the file paths that are different are
+	 * returned. Callers may choose to format these paths themselves, or convert
+	 * them into {@link FileHeader} instances with a complete edit list by
+	 * calling {@link #toFileHeader(DiffEntry)}.
+	 *
+	 * @param a
+	 *            the old (or previous) side.
+	 * @param b
+	 *            the new (or updated) side.
+	 * @return the paths that are different.
+	 * @throws IOException
+	 *             trees cannot be read or file contents cannot be read.
+	 */
+	public List<DiffEntry> scan(AnyObjectId a, AnyObjectId b)
+			throws IOException {
+		assertHaveRepository();
+
+		RevWalk rw = new RevWalk(reader);
+		return scan(rw.parseTree(a), rw.parseTree(b));
+	}
+
+	/**
+	 * Determine the differences between two trees.
+	 *
+	 * No output is created, instead only the file paths that are different are
+	 * returned. Callers may choose to format these paths themselves, or convert
+	 * them into {@link FileHeader} instances with a complete edit list by
+	 * calling {@link #toFileHeader(DiffEntry)}.
+	 *
+	 * @param a
+	 *            the old (or previous) side.
+	 * @param b
+	 *            the new (or updated) side.
+	 * @return the paths that are different.
+	 * @throws IOException
+	 *             trees cannot be read or file contents cannot be read.
+	 */
+	public List<DiffEntry> scan(RevTree a, RevTree b) throws IOException {
+		assertHaveRepository();
+
+		CanonicalTreeParser aParser = new CanonicalTreeParser();
+		CanonicalTreeParser bParser = new CanonicalTreeParser();
+
+		aParser.reset(reader, a);
+		bParser.reset(reader, b);
+
+		return scan(aParser, bParser);
+	}
+
+	/**
+	 * Determine the differences between two trees.
+	 *
+	 * No output is created, instead only the file paths that are different are
+	 * returned. Callers may choose to format these paths themselves, or convert
+	 * them into {@link FileHeader} instances with a complete edit list by
+	 * calling {@link #toFileHeader(DiffEntry)}.
+	 *
+	 * @param a
+	 *            the old (or previous) side.
+	 * @param b
+	 *            the new (or updated) side.
+	 * @return the paths that are different.
+	 * @throws IOException
+	 *             trees cannot be read or file contents cannot be read.
+	 */
+	public List<DiffEntry> scan(AbstractTreeIterator a, AbstractTreeIterator b)
+			throws IOException {
+		assertHaveRepository();
+
+		TreeWalk walk = new TreeWalk(reader);
+		walk.reset();
+		walk.addTree(a);
+		walk.addTree(b);
+		walk.setRecursive(true);
+
+		if (pathFilter == TreeFilter.ALL) {
+			walk.setFilter(TreeFilter.ANY_DIFF);
+		} else if (pathFilter instanceof FollowFilter) {
+			walk.setFilter(pathFilter);
+		} else {
+			walk.setFilter(AndTreeFilter
+					.create(pathFilter, TreeFilter.ANY_DIFF));
+		}
+
+		List<DiffEntry> files = DiffEntry.scan(walk);
+		if (pathFilter instanceof FollowFilter && isAdd(files)) {
+			// The file we are following was added here, find where it
+			// came from so we can properly show the rename or copy,
+			// then continue digging backwards.
+			//
+			a.reset();
+			b.reset();
+			walk.reset();
+			walk.addTree(a);
+			walk.addTree(b);
+			walk.setFilter(TreeFilter.ANY_DIFF);
+
+			if (renameDetector == null)
+				setDetectRenames(true);
+			files = updateFollowFilter(detectRenames(DiffEntry.scan(walk)));
+
+		} else if (renameDetector != null)
+			files = detectRenames(files);
+
+		return files;
+	}
+
+	private List<DiffEntry> detectRenames(List<DiffEntry> files)
+			throws IOException {
+		renameDetector.reset();
+		renameDetector.addAll(files);
+		return renameDetector.compute(reader, progressMonitor);
+	}
+
+	private boolean isAdd(List<DiffEntry> files) {
+		String oldPath = ((FollowFilter) pathFilter).getPath();
+		for (DiffEntry ent : files) {
+			if (ent.getChangeType() == ADD && ent.getNewPath().equals(oldPath))
+				return true;
+		}
+		return false;
+	}
+
+	private List<DiffEntry> updateFollowFilter(List<DiffEntry> files) {
+		String oldPath = ((FollowFilter) pathFilter).getPath();
+		for (DiffEntry ent : files) {
+			if (isRename(ent) && ent.getNewPath().equals(oldPath)) {
+				pathFilter = FollowFilter.create(ent.getOldPath());
+				return Collections.singletonList(ent);
+			}
+		}
+		return Collections.emptyList();
+	}
+
+	private static boolean isRename(DiffEntry ent) {
+		return ent.getChangeType() == RENAME || ent.getChangeType() == COPY;
+	}
+
+	/**
+	 * Format the differences between two trees.
+	 *
+	 * The patch is expressed as instructions to modify {@code a} to make it
+	 * {@code b}.
+	 *
+	 * @param a
+	 *            the old (or previous) side.
+	 * @param b
+	 *            the new (or updated) side.
+	 * @throws IOException
+	 *             trees cannot be read, file contents cannot be read, or the
+	 *             patch cannot be output.
+	 */
+	public void format(AnyObjectId a, AnyObjectId b) throws IOException {
+		format(scan(a, b));
+	}
+
+	/**
+	 * Format the differences between two trees.
+	 *
+	 * The patch is expressed as instructions to modify {@code a} to make it
+	 * {@code b}.
+	 *
+	 * @param a
+	 *            the old (or previous) side.
+	 * @param b
+	 *            the new (or updated) side.
+	 * @throws IOException
+	 *             trees cannot be read, file contents cannot be read, or the
+	 *             patch cannot be output.
+	 */
+	public void format(RevTree a, RevTree b) throws IOException {
+		format(scan(a, b));
+	}
+
+	/**
+	 * Format the differences between two trees.
+	 *
+	 * The patch is expressed as instructions to modify {@code a} to make it
+	 * {@code b}.
+	 *
+	 * @param a
+	 *            the old (or previous) side.
+	 * @param b
+	 *            the new (or updated) side.
+	 * @throws IOException
+	 *             trees cannot be read, file contents cannot be read, or the
+	 *             patch cannot be output.
+	 */
+	public void format(AbstractTreeIterator a, AbstractTreeIterator b)
+			throws IOException {
+		format(scan(a, b));
+	}
+
 	/**
 	 * Format a patch script from a list of difference entries.
 	 *
@@ -272,13 +566,10 @@ private void writeGitLinkDiffText(OutputStream o, DiffEntry ent)
 
 	private String format(AbbreviatedObjectId id) {
 		if (id.isComplete() && db != null) {
-			ObjectReader reader = db.newObjectReader();
 			try {
 				id = reader.abbreviate(id.toObjectId(), abbreviationLength);
 			} catch (IOException cannotAbbreviate) {
 				// Ignore this. We'll report the full identity.
-			} finally {
-				reader.release();
 			}
 		}
 		return id.name();
@@ -319,22 +610,22 @@ public void format(final FileHeader head, final RawText a, final RawText b)
 			end = head.getHunks().get(0).getStartOffset();
 		out.write(head.getBuffer(), start, end - start);
 		if (head.getPatchType() == PatchType.UNIFIED)
-			formatEdits(a, b, head.toEditList());
+			format(head.toEditList(), a, b);
 	}
 
 	/**
 	 * Formats a list of edits in unified diff format
 	 *
+	 * @param edits
+	 *            some differences which have been calculated between A and B
 	 * @param a
 	 *            the text A which was compared
 	 * @param b
 	 *            the text B which was compared
-	 * @param edits
-	 *            some differences which have been calculated between A and B
 	 * @throws IOException
 	 */
-	public void formatEdits(final RawText a, final RawText b,
-			final EditList edits) throws IOException {
+	public void format(final EditList edits, final RawText a, final RawText b)
+			throws IOException {
 		for (int curIdx = 0; curIdx < edits.size();) {
 			Edit curEdit = edits.get(curIdx);
 			final int endIdx = findCombinedEnd(edits, curIdx);
@@ -513,7 +804,7 @@ protected void writeLine(final char prefix, final RawText text,
 	 * @throws MissingObjectException
 	 *             one of the blobs referenced by the DiffEntry is missing.
 	 */
-	public FileHeader createFileHeader(DiffEntry ent) throws IOException,
+	public FileHeader toFileHeader(DiffEntry ent) throws IOException,
 			CorruptObjectException, MissingObjectException {
 		return createFormatResult(ent).header;
 	}
@@ -542,24 +833,14 @@ private FormatResult createFormatResult(DiffEntry ent) throws IOException,
 			type = PatchType.UNIFIED;
 
 		} else {
-			if (db == null)
-				throw new IllegalStateException(
-						JGitText.get().repositoryIsRequired);
+			assertHaveRepository();
 
-			ObjectReader reader = db.newObjectReader();
-			byte[] aRaw, bRaw;
-			try {
-				aRaw = open(reader, //
-						ent.getOldPath(), //
-						ent.getOldMode(), //
-						ent.getOldId());
-				bRaw = open(reader, //
-						ent.getNewPath(), //
-						ent.getNewMode(), //
-						ent.getNewId());
-			} finally {
-				reader.release();
-			}
+			byte[] aRaw = open(ent.getOldPath(), //
+					ent.getOldMode(), //
+					ent.getOldId());
+			byte[] bRaw = open(ent.getNewPath(), //
+					ent.getNewMode(), //
+					ent.getNewId());
 
 			if (aRaw == BINARY || bRaw == BINARY //
 					|| RawText.isBinary(aRaw) || RawText.isBinary(bRaw)) {
@@ -592,8 +873,13 @@ private FormatResult createFormatResult(DiffEntry ent) throws IOException,
 		return res;
 	}
 
-	private byte[] open(ObjectReader reader, String path, FileMode mode,
-			AbbreviatedObjectId id) throws IOException {
+	private void assertHaveRepository() {
+		if (db == null)
+			throw new IllegalStateException(JGitText.get().repositoryIsRequired);
+	}
+
+	private byte[] open(String path, FileMode mode, AbbreviatedObjectId id)
+			throws IOException {
 		if (mode == FileMode.MISSING)
 			return EMPTY;
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java
index 9c1310a..bd4a5e2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java
@@ -100,11 +100,11 @@ private int sortOf(ChangeType changeType) {
 		}
 	};
 
-	private List<DiffEntry> entries = new ArrayList<DiffEntry>();
+	private List<DiffEntry> entries;
 
-	private List<DiffEntry> deleted = new ArrayList<DiffEntry>();
+	private List<DiffEntry> deleted;
 
-	private List<DiffEntry> added = new ArrayList<DiffEntry>();
+	private List<DiffEntry> added;
 
 	private boolean done;
 
@@ -137,6 +137,8 @@ public RenameDetector(Repository repo) {
 
 		DiffConfig cfg = repo.getConfig().get(DiffConfig.KEY);
 		renameLimit = cfg.getRenameLimit();
+
+		reset();
 	}
 
 	/**
@@ -305,19 +307,39 @@ public List<DiffEntry> compute() throws IOException {
 	 */
 	public List<DiffEntry> compute(ProgressMonitor pm) throws IOException {
 		if (!done) {
+			ObjectReader reader = repo.newObjectReader();
+			try {
+				return compute(reader, pm);
+			} finally {
+				reader.release();
+			}
+		}
+		return Collections.unmodifiableList(entries);
+	}
+
+	/**
+	 * Detect renames in the current file set.
+	 *
+	 * @param reader
+	 *            reader to obtain objects from the repository with.
+	 * @param pm
+	 *            report progress during the detection phases.
+	 * @return an unmodifiable list of {@link DiffEntry}s representing all files
+	 *         that have been changed.
+	 * @throws IOException
+	 *             file contents cannot be read from the repository.
+	 */
+	public List<DiffEntry> compute(ObjectReader reader, ProgressMonitor pm)
+			throws IOException {
+		if (!done) {
 			done = true;
 
 			if (pm == null)
 				pm = NullProgressMonitor.INSTANCE;
-			ObjectReader reader = repo.newObjectReader();
-			try {
 				breakModifies(reader, pm);
 				findExactRenames(pm);
 				findContentRenames(reader, pm);
 				rejoinModifies(pm);
-			} finally {
-				reader.release();
-			}
 
 			entries.addAll(added);
 			added = null;
@@ -330,6 +352,14 @@ public List<DiffEntry> compute(ProgressMonitor pm) throws IOException {
 		return Collections.unmodifiableList(entries);
 	}
 
+	/** Reset this rename detector for another rename detection pass. */
+	public void reset() {
+		entries = new ArrayList<DiffEntry>();
+		deleted = new ArrayList<DiffEntry>();
+		added = new ArrayList<DiffEntry>();
+		done = false;
+	}
+
 	private void breakModifies(ObjectReader reader, ProgressMonitor pm)
 			throws IOException {
 		if (breakScore <= 0)