RevWalk: Add a setFirstParent that mimics C git's --first-parent

RevWalk does not currently provide a --first-parent equivalent and the
feature has been requested.

Add a field to the RevWalk class to specify whether walks should
traverse first parents only. Modify Generator implementations to support
the feature.

Change-Id: I4a9a0d5767f82141dcf6d08659d7cb77c585fae4
Signed-off-by: Dave Borowitz <dborowitz@google.com>
Signed-off-by: Alex Spradlin <alexaspradlin@google.com>
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FirstParentRevWalkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FirstParentRevWalkTest.java
new file mode 100644
index 0000000..474ff7a
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FirstParentRevWalkTest.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2019, Google LLC.
+ * 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 org.eclipse.jgit.revwalk;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.eclipse.jgit.revwalk.filter.MessageRevFilter;
+import org.eclipse.jgit.revwalk.filter.RevFilter;
+import org.junit.Test;
+
+public class FirstParentRevWalkTest extends RevWalkTestCase {
+	@Test
+	public void testStringOfPearls() throws Exception {
+		RevCommit a = commit();
+		RevCommit b = commit(a);
+		RevCommit c = commit(b);
+
+		rw.reset();
+		rw.setFirstParent(true);
+		markStart(c);
+		assertCommit(c, rw.next());
+		assertCommit(b, rw.next());
+		assertCommit(a, rw.next());
+		assertNull(rw.next());
+	}
+
+	@Test
+	public void testSideBranch() throws Exception {
+		RevCommit a = commit();
+		RevCommit b1 = commit(a);
+		RevCommit b2 = commit(a);
+		RevCommit c1 = commit(b1);
+		RevCommit c2 = commit(b2);
+		RevCommit d = commit(c1, c2);
+
+		rw.reset();
+		rw.setFirstParent(true);
+		markStart(d);
+		assertCommit(d, rw.next());
+		assertCommit(c1, rw.next());
+		assertCommit(b1, rw.next());
+		assertCommit(a, rw.next());
+		assertNull(rw.next());
+	}
+
+	@Test
+	public void testSecondParentAncestorOfFirstParent() throws Exception {
+		RevCommit a = commit();
+		RevCommit b = commit(a);
+		RevCommit c = commit(b, a);
+
+		rw.reset();
+		rw.setFirstParent(true);
+		markStart(c);
+		assertCommit(c, rw.next());
+		assertCommit(b, rw.next());
+		assertCommit(a, rw.next());
+		assertNull(rw.next());
+	}
+
+	@Test
+	public void testFirstParentMultipleOccurrences() throws Exception {
+		RevCommit a = commit();
+		RevCommit b = commit(a);
+		RevCommit c = commit(b);
+		RevCommit d = commit(b);
+
+		rw.reset();
+		rw.setFirstParent(true);
+		markStart(c);
+		markStart(d);
+		assertCommit(d, rw.next());
+		assertCommit(c, rw.next());
+		assertCommit(b, rw.next());
+		assertCommit(a, rw.next());
+		assertNull(rw.next());
+	}
+
+	@Test
+	public void testReachableAlongFirstAndLaterParents() throws Exception {
+		RevCommit a = commit();
+		RevCommit b1 = commit(a);
+		RevCommit b2 = commit(a);
+		RevCommit b3 = commit(a);
+		RevCommit c = commit(b1, b2);
+		RevCommit d = commit(b2, b3);
+
+		rw.reset();
+		rw.setFirstParent(true);
+		markStart(c);
+		markStart(d);
+		assertCommit(d, rw.next());
+		assertCommit(c, rw.next());
+		// b3 is only reachable from c's second parent.
+		// b2 is reachable from c's second parent but d's first parent.
+		assertCommit(b2, rw.next());
+		assertCommit(b1, rw.next());
+		assertCommit(a, rw.next());
+		assertNull(rw.next());
+	}
+
+	@Test
+	public void testStartCommitReachableOnlyFromLaterParents()
+			throws Exception {
+		RevCommit a = commit();
+		RevCommit b1 = commit(a);
+		RevCommit b2 = commit(a);
+		RevCommit c = commit(b1, b2);
+
+		rw.reset();
+		rw.setFirstParent(true);
+		markStart(c);
+		markStart(b2);
+		assertCommit(c, rw.next());
+		// b2 is only reachable from second parent, but is itself a start
+		// commit.
+		assertCommit(b2, rw.next());
+		assertCommit(b1, rw.next());
+		assertCommit(a, rw.next());
+		assertNull(rw.next());
+	}
+
+	@Test
+	public void testRevFilter() throws Exception {
+		RevCommit a = commit();
+		RevCommit b1 = commitBuilder().parent(a).message("commit b1").create();
+		RevCommit b2 = commitBuilder().parent(a).message("commit b2").create();
+		RevCommit c = commit(b1, b2);
+
+		rw.reset();
+		rw.setFirstParent(true);
+		rw.setRevFilter(MessageRevFilter.create("commit b"));
+		rw.markStart(c);
+		assertCommit(b1, rw.next());
+		assertNull(rw.next());
+	}
+
+	@Test
+	public void testTopoSort() throws Exception {
+		RevCommit a = commit();
+		RevCommit b1 = commit(a);
+		RevCommit b2 = commit(a);
+		RevCommit c = commit(b1, b2);
+
+		rw.reset();
+		rw.sort(RevSort.TOPO);
+		rw.setFirstParent(true);
+		markStart(c);
+		assertCommit(c, rw.next());
+		assertCommit(b1, rw.next());
+		assertCommit(a, rw.next());
+		assertNull(rw.next());
+	}
+
+	@Test
+	public void testCommitTimeSort() throws Exception {
+		RevCommit a = commit();
+		RevCommit b1 = commit(a);
+		RevCommit b2 = commit(a);
+		RevCommit c = commit(b1, b2);
+
+		rw.reset();
+		rw.sort(RevSort.COMMIT_TIME_DESC);
+		rw.setFirstParent(true);
+		markStart(c);
+		assertCommit(c, rw.next());
+		assertCommit(b1, rw.next());
+		assertCommit(a, rw.next());
+		assertNull(rw.next());
+	}
+
+	@Test
+	public void testReverseSort() throws Exception {
+		RevCommit a = commit();
+		RevCommit b1 = commit(a);
+		RevCommit b2 = commit(a);
+		RevCommit c = commit(b1, b2);
+
+		rw.reset();
+		rw.sort(RevSort.REVERSE);
+		rw.setFirstParent(true);
+		markStart(c);
+		assertCommit(a, rw.next());
+		assertCommit(b1, rw.next());
+		assertCommit(c, rw.next());
+		assertNull(rw.next());
+	}
+
+	@Test
+	public void testBoundarySort() throws Exception {
+		RevCommit a = commit();
+		RevCommit b = commit(a);
+		RevCommit c1 = commit(b);
+		RevCommit c2 = commit(b);
+		RevCommit d = commit(c1, c2);
+
+		rw.reset();
+		rw.sort(RevSort.BOUNDARY);
+		rw.setFirstParent(true);
+		markStart(d);
+		markUninteresting(a);
+		assertCommit(d, rw.next());
+		assertCommit(c1, rw.next());
+		assertCommit(b, rw.next());
+		assertCommit(a, rw.next());
+		assertNull(rw.next());
+	}
+
+	@Test
+	public void testFirstParentOfFirstParentMarkedUninteresting()
+			throws Exception {
+		RevCommit a = commit();
+		RevCommit b1 = commit(a);
+		RevCommit b2 = commit(a);
+		RevCommit c1 = commit(b1);
+		RevCommit c2 = commit(b2);
+		RevCommit d = commit(c1, c2);
+
+		rw.reset();
+		rw.setFirstParent(true);
+		markStart(d);
+		markUninteresting(b1);
+		assertCommit(d, rw.next());
+		assertCommit(c1, rw.next());
+		assertNull(rw.next());
+	}
+
+	@Test
+	public void testFirstParentMarkedUninteresting() throws Exception {
+		RevCommit a = commit();
+		RevCommit b1 = commit(a);
+		RevCommit b2 = commit(a);
+		RevCommit c = commit(b1, b2);
+
+		rw.reset();
+		rw.setFirstParent(true);
+		markStart(c);
+		markUninteresting(b1);
+		assertCommit(c, rw.next());
+		assertNull(rw.next());
+	}
+
+	@Test
+	public void testDepthWalk() throws Exception {
+		RevCommit a = commit();
+		RevCommit b1 = commit(a);
+		RevCommit b2 = commit(a);
+		RevCommit c = commit(b1, b2);
+
+		try (DepthWalk.RevWalk dw = new DepthWalk.RevWalk(db, 1)) {
+			dw.setFirstParent(true);
+			dw.markRoot(dw.parseCommit(c));
+			dw.markStart(dw.parseCommit(c));
+			assertEquals(c, dw.next());
+			assertEquals(b1, dw.next());
+			assertNull(dw.next());
+		}
+	}
+
+	@Test
+	public void testDoNotRewriteParents() throws Exception {
+		RevCommit a = commit();
+		RevCommit b1 = commit(a);
+		RevCommit b2 = commit(a);
+		RevCommit c = commit(b1, b2);
+
+		rw.reset();
+		rw.setFirstParent(true);
+		rw.setRewriteParents(false);
+		markStart(c);
+		assertCommit(c, rw.next());
+		assertCommit(b1, rw.next());
+		assertCommit(a, rw.next());
+		assertNull(rw.next());
+	}
+
+	@Test(expected = IllegalStateException.class)
+	public void testMarkStartBeforeSetFirstParent() throws Exception {
+		RevCommit a = commit();
+
+		rw.reset();
+		markStart(a);
+		rw.setFirstParent(true);
+	}
+
+	@Test(expected = IllegalStateException.class)
+	public void testMergeBaseWithFirstParentNotAllowed() throws Exception {
+		RevCommit a = commit();
+
+		rw.reset();
+		rw.setFirstParent(true);
+		rw.setRevFilter(RevFilter.MERGE_BASE);
+		markStart(a);
+		assertNull(rw.next());
+	}
+}
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index df42dc7..b610deb 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -77,6 +77,7 @@
 cannotEnterObjectsPath=Cannot enter {0}/objects: {1}
 cannotEnterPathFromParent=Cannot enter {0} from {1}: {2}
 cannotExecute=cannot execute: {0}
+cannotFindMergeBaseUsingFirstParent=Cannot find merge bases using a first-parent walk.
 cannotGet=Cannot get {0}
 cannotGetObjectsPath=Cannot get {0}/{1}: {2}
 cannotListObjectsPath=Cannot ls {0}/{1}: {2}
@@ -134,6 +135,7 @@
 commitMessageNotSpecified=commit message not specified
 commitOnRepoWithoutHEADCurrentlyNotSupported=Commit on repo without HEAD currently not supported
 commitAmendOnInitialNotPossible=Amending is not possible on initial commit.
+commitsHaveAlreadyBeenMarkedAsStart=Commits have already been marked as walk starts.
 compressingObjects=Compressing objects
 configSubsectionContainsNewline=config subsection name contains newline
 configSubsectionContainsNullByte=config subsection name contains byte 0x00
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index bdaef5a..572f963 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -138,6 +138,7 @@ public static JGitText get() {
 	/***/ public String cannotEnterObjectsPath;
 	/***/ public String cannotEnterPathFromParent;
 	/***/ public String cannotExecute;
+	/***/ public String cannotFindMergeBaseUsingFirstParent;
 	/***/ public String cannotGet;
 	/***/ public String cannotGetObjectsPath;
 	/***/ public String cannotListObjectsPath;
@@ -195,6 +196,7 @@ public static JGitText get() {
 	/***/ public String commitMessageNotSpecified;
 	/***/ public String commitOnRepoWithoutHEADCurrentlyNotSupported;
 	/***/ public String commitAmendOnInitialNotPossible;
+	/***/ public String commitsHaveAlreadyBeenMarkedAsStart;
 	/***/ public String compressingObjects;
 	/***/ public String configSubsectionContainsNewline;
 	/***/ public String configSubsectionContainsNullByte;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AbstractRevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AbstractRevQueue.java
index 247a3bd..7b5e6fa 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AbstractRevQueue.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/AbstractRevQueue.java
@@ -49,6 +49,10 @@ abstract class AbstractRevQueue extends Generator {
 	/** Current output flags set for this generator instance. */
 	int outputType;
 
+	AbstractRevQueue(boolean firstParent) {
+		super(firstParent);
+	}
+
 	/**
 	 * Add a commit to the queue.
 	 * <p>
@@ -96,10 +100,15 @@ public final void add(RevCommit c, RevFlag queueControl) {
 	 */
 	public final void addParents(RevCommit c, RevFlag queueControl) {
 		final RevCommit[] pList = c.parents;
-		if (pList == null)
+		if (pList == null) {
 			return;
-		for (RevCommit p : pList)
-			add(p, queueControl);
+		}
+		for (int i = 0; i < pList.length; i++) {
+			if (firstParent && i > 0) {
+				break;
+			}
+			add(pList[i], queueControl);
+		}
 	}
 
 	/**
@@ -138,6 +147,10 @@ protected static void describe(StringBuilder s, RevCommit c) {
 	}
 
 	private static class AlwaysEmptyQueue extends AbstractRevQueue {
+		private AlwaysEmptyQueue() {
+			super(false);
+		}
+
 		@Override
 		public void add(RevCommit c) {
 			throw new UnsupportedOperationException();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BlockRevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BlockRevQueue.java
index 79307b5..64e9e03 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BlockRevQueue.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BlockRevQueue.java
@@ -53,13 +53,19 @@ abstract class BlockRevQueue extends AbstractRevQueue {
 
 	/**
 	 * Create an empty revision queue.
+	 *
+	 * @param firstParent
+	 *            whether only first-parent links should be followed when
+	 *            walking
 	 */
-	protected BlockRevQueue() {
+	protected BlockRevQueue(boolean firstParent) {
+		super(firstParent);
 		free = new BlockFreeList();
 	}
 
 	BlockRevQueue(Generator s) throws MissingObjectException,
 			IncorrectObjectTypeException, IOException {
+		super(s.firstParent);
 		free = new BlockFreeList();
 		outputType = s.outputType();
 		s.shareFreeList(this);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BoundaryGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BoundaryGenerator.java
index 0fd6621..98c99e8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BoundaryGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/BoundaryGenerator.java
@@ -55,6 +55,7 @@ class BoundaryGenerator extends Generator {
 	Generator g;
 
 	BoundaryGenerator(RevWalk w, Generator s) {
+		super(s.firstParent);
 		g = new InitialGenerator(w, s);
 	}
 
@@ -86,8 +87,9 @@ private class InitialGenerator extends Generator {
 		private final Generator source;
 
 		InitialGenerator(RevWalk w, Generator s) {
+			super(s.firstParent);
 			walk = w;
-			held = new FIFORevQueue();
+			held = new FIFORevQueue(firstParent);
 			source = s;
 			source.shareFreeList(held);
 		}
@@ -107,13 +109,19 @@ RevCommit next() throws MissingObjectException,
 				IncorrectObjectTypeException, IOException {
 			RevCommit c = source.next();
 			if (c != null) {
-				for (RevCommit p : c.parents)
-					if ((p.flags & UNINTERESTING) != 0)
+				for (int i = 0; i < c.parents.length; i++) {
+					if (firstParent && i > 0) {
+						break;
+					}
+					RevCommit p = c.parents[i];
+					if ((p.flags & UNINTERESTING) != 0) {
 						held.add(p);
+					}
+				}
 				return c;
 			}
 
-			final FIFORevQueue boundary = new FIFORevQueue();
+			final FIFORevQueue boundary = new FIFORevQueue(firstParent);
 			boundary.shareFreeList(held);
 			for (;;) {
 				c = held.next();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java
index b86e876..d77b055 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DateRevQueue.java
@@ -69,15 +69,18 @@ public class DateRevQueue extends AbstractRevQueue {
 
 	private int last = -1;
 
-	/**
-	 * Create an empty date queue.
-	 */
+	/** Create an empty date queue. */
 	public DateRevQueue() {
-		super();
+		super(false);
+	}
+
+	DateRevQueue(boolean firstParent) {
+		super(firstParent);
 	}
 
 	DateRevQueue(Generator s) throws MissingObjectException,
 			IncorrectObjectTypeException, IOException {
+		super(s.firstParent);
 		for (;;) {
 			final RevCommit c = s.next();
 			if (c == null)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DelayRevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DelayRevQueue.java
index c397a01..9134e08 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DelayRevQueue.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DelayRevQueue.java
@@ -70,6 +70,7 @@ final class DelayRevQueue extends Generator {
 	private int size;
 
 	DelayRevQueue(Generator g) {
+		super(g.firstParent);
 		pending = g;
 		delay = new FIFORevQueue();
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthGenerator.java
index 5199a29..0b04d5d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/DepthGenerator.java
@@ -95,7 +95,8 @@ class DepthGenerator extends Generator {
 	 */
 	DepthGenerator(DepthWalk w, Generator s) throws MissingObjectException,
 			IncorrectObjectTypeException, IOException {
-		pending = new FIFORevQueue();
+		super(s.firstParent);
+		pending = new FIFORevQueue(firstParent);
 		walk = (RevWalk)w;
 
 		this.depth = w.getDepth();
@@ -196,7 +197,11 @@ RevCommit next() throws MissingObjectException,
 
 			int newDepth = c.depth + 1;
 
-			for (RevCommit p : c.parents) {
+			for (int i = 0; i < c.parents.length; i++) {
+				if (firstParent && i > 0) {
+					break;
+				}
+				RevCommit p = c.parents[i];
 				DepthWalk.Commit dp = (DepthWalk.Commit) p;
 
 				// If no depth has been assigned to this commit, assign
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/EndGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/EndGenerator.java
index 627e1c7..03916c8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/EndGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/EndGenerator.java
@@ -47,7 +47,7 @@ class EndGenerator extends Generator {
 	static final EndGenerator INSTANCE = new EndGenerator();
 
 	private EndGenerator() {
-		// We have nothing to initialize.
+		super(false);
 	}
 
 	@Override
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FIFORevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FIFORevQueue.java
index cdb084c..40ee55c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FIFORevQueue.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FIFORevQueue.java
@@ -56,11 +56,13 @@ public class FIFORevQueue extends BlockRevQueue {
 
 	private Block tail;
 
-	/**
-	 * Create an empty FIFO queue.
-	 */
+	/** Create an empty FIFO queue. */
 	public FIFORevQueue() {
-		super();
+		super(false);
+	}
+
+	FIFORevQueue(boolean firstParent) {
+		super(firstParent);
 	}
 
 	FIFORevQueue(Generator s) throws MissingObjectException,
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FixUninterestingGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FixUninterestingGenerator.java
index 4e6d7e6..289842a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FixUninterestingGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/FixUninterestingGenerator.java
@@ -62,6 +62,7 @@ final class FixUninterestingGenerator extends Generator {
 	private final Generator pending;
 
 	FixUninterestingGenerator(Generator g) {
+		super(g.firstParent);
 		pending = g;
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/Generator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/Generator.java
index b2c92ea..59c5cce 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/Generator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/Generator.java
@@ -75,6 +75,12 @@ abstract class Generator {
 	/** Output may have {@link RevWalk#UNINTERESTING} marked on it. */
 	static final int HAS_UNINTERESTING = 1 << 4;
 
+	protected final boolean firstParent;
+
+	protected Generator(boolean firstParent) {
+		this.firstParent = firstParent;
+	}
+
 	/**
 	 * Connect the supplied queue to this generator's own free list (if any).
 	 *
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/LIFORevQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/LIFORevQueue.java
index 846b8d9..5628d35 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/LIFORevQueue.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/LIFORevQueue.java
@@ -59,7 +59,7 @@ public class LIFORevQueue extends BlockRevQueue {
 	 * Create an empty LIFO queue.
 	 */
 	public LIFORevQueue() {
-		super();
+		super(false);
 	}
 
 	LIFORevQueue(Generator s) throws MissingObjectException,
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java
index 2fe9531..4ea57cb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/MergeBaseGenerator.java
@@ -85,8 +85,9 @@ class MergeBaseGenerator extends Generator {
 	private CarryStack stack;
 
 	MergeBaseGenerator(RevWalk w) {
+		super(w.isFirstParent());
 		walker = w;
-		pending = new DateRevQueue();
+		pending = new DateRevQueue(firstParent);
 	}
 
 	void init(AbstractRevQueue p) throws IOException {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PendingGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PendingGenerator.java
index e607b7d..52dd56d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PendingGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/PendingGenerator.java
@@ -109,6 +109,7 @@ class PendingGenerator extends Generator {
 
 	PendingGenerator(final RevWalk w, final DateRevQueue p,
 			final RevFilter f, final int out) {
+		super(w.isFirstParent());
 		walker = w;
 		pending = p;
 		filter = f;
@@ -140,7 +141,11 @@ RevCommit next() throws MissingObjectException,
 					produce = filter.include(walker, c);
 				}
 
-				for (RevCommit p : c.parents) {
+				for (int i = 0; i < c.parents.length; i++) {
+					RevCommit p = c.parents[i];
+					if (firstParent && i > 0) {
+						continue;
+					}
 					if ((p.flags & SEEN) != 0)
 						continue;
 					if ((p.flags & PARSED) == 0)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
index 80fc810..12e9f89 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
@@ -200,6 +200,8 @@ public class RevWalk implements Iterable<RevCommit>, AutoCloseable {
 
 	private boolean rewriteParents = true;
 
+	private boolean firstParent;
+
 	boolean shallowCommitsInitialized;
 
 	/**
@@ -232,7 +234,7 @@ private RevWalk(ObjectReader or, boolean closeReader) {
 		idBuffer = new MutableObjectId();
 		objects = new ObjectIdOwnerMap<>();
 		roots = new ArrayList<>();
-		queue = new DateRevQueue();
+		queue = new DateRevQueue(false);
 		pending = new StartGenerator(this);
 		sorting = EnumSet.of(RevSort.NONE);
 		filter = RevFilter.ALL;
@@ -661,6 +663,33 @@ public void setRetainBody(boolean retain) {
 	}
 
 	/**
+	 * @return whether only first-parent links should be followed when walking.
+	 * @since 5.4
+	 */
+	public boolean isFirstParent() {
+		return firstParent;
+	}
+
+	/**
+	 * Set whether or not only first parent links should be followed.
+	 * <p>
+	 * If set, second- and higher-parent links are not traversed at all.
+	 * <p>
+	 * This must be called prior to {@link #markStart(RevCommit)}.
+	 *
+	 * @param enable
+	 *            true to walk only first-parent links.
+	 * @since 5.4
+	 */
+	public void setFirstParent(boolean enable) {
+		assertNotStarted();
+		assertNoCommitsMarkedStart();
+		firstParent = enable;
+		queue = new DateRevQueue(firstParent);
+		pending = new StartGenerator(this);
+	}
+
+	/**
 	 * Locate a reference to a blob without loading it.
 	 * <p>
 	 * The blob may or may not exist in the repository. It is impossible to tell
@@ -1292,7 +1321,8 @@ public final void resetRetain(RevFlag... retainFlags) {
 	 * <p>
 	 * Unlike {@link #dispose()} previously acquired RevObject (and RevCommit)
 	 * instances are not invalidated. RevFlag instances are not invalidated, but
-	 * are removed from all RevObjects.
+	 * are removed from all RevObjects. The value of {@code firstParent} is
+	 * retained.
 	 *
 	 * @param retainFlags
 	 *            application flags that should <b>not</b> be cleared from
@@ -1328,7 +1358,7 @@ protected void reset(int retainFlags) {
 		}
 
 		roots.clear();
-		queue = new DateRevQueue();
+		queue = new DateRevQueue(firstParent);
 		pending = new StartGenerator(this);
 	}
 
@@ -1346,9 +1376,10 @@ public void dispose() {
 		delayFreeFlags = 0;
 		retainOnReset = 0;
 		carryFlags = UNINTERESTING;
+		firstParent = false;
 		objects.clear();
 		roots.clear();
-		queue = new DateRevQueue();
+		queue = new DateRevQueue(firstParent);
 		pending = new StartGenerator(this);
 		shallowCommitsInitialized = false;
 	}
@@ -1420,6 +1451,19 @@ protected void assertNotStarted() {
 		throw new IllegalStateException(JGitText.get().outputHasAlreadyBeenStarted);
 	}
 
+	/**
+	 * Throws an exception if any commits have been marked as start.
+	 * <p>
+	 * If {@link #markStart(RevCommit)} has already been called,
+	 * {@link #reset()} can be called to satisfy this condition.
+	 */
+	protected void assertNoCommitsMarkedStart() {
+		if (roots.isEmpty())
+			return;
+		throw new IllegalStateException(
+				JGitText.get().commitsHaveAlreadyBeenMarkedAsStart);
+	}
+
 	private boolean isNotStarted() {
 		return pending instanceof StartGenerator;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java
index 1c868ff..2e26641 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RewriteGenerator.java
@@ -77,6 +77,7 @@ class RewriteGenerator extends Generator {
 	private final Generator source;
 
 	RewriteGenerator(Generator s) {
+		super(s.firstParent);
 		source = s;
 	}
 
@@ -102,6 +103,10 @@ RevCommit next() throws MissingObjectException,
 		final int nParents = pList.length;
 		for (int i = 0; i < nParents; i++) {
 			final RevCommit oldp = pList[i];
+			if (firstParent && i > 0) {
+				c.parents = new RevCommit[] { rewrite(oldp) };
+				return c;
+			}
 			final RevCommit newp = rewrite(oldp);
 			if (oldp != newp) {
 				pList[i] = newp;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java
index eb129a2..b309d6f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/StartGenerator.java
@@ -67,6 +67,7 @@ class StartGenerator extends Generator {
 	private final RevWalk walker;
 
 	StartGenerator(RevWalk w) {
+		super(w.isFirstParent());
 		walker = w;
 	}
 
@@ -89,9 +90,14 @@ RevCommit next() throws MissingObjectException,
 			// Computing for merge bases is a special case and does not
 			// use the bulk of the generator pipeline.
 			//
-			if (tf != TreeFilter.ALL)
+			if (tf != TreeFilter.ALL) {
 				throw new IllegalStateException(MessageFormat.format(
 						JGitText.get().cannotCombineTreeFilterWithRevFilter, tf, rf));
+			}
+			if (w.isFirstParent()) {
+				throw new IllegalStateException(
+						JGitText.get().cannotFindMergeBaseUsingFirstParent);
+			}
 
 			final MergeBaseGenerator mbg = new MergeBaseGenerator(w);
 			walker.pending = mbg;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java
index 6450343..a2c9ef6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/TopoSortGenerator.java
@@ -71,15 +71,21 @@ class TopoSortGenerator extends Generator {
 	 */
 	TopoSortGenerator(Generator s) throws MissingObjectException,
 			IncorrectObjectTypeException, IOException {
-		pending = new FIFORevQueue();
+		super(s.firstParent);
+		pending = new FIFORevQueue(firstParent);
 		outputType = s.outputType() | SORT_TOPO;
 		s.shareFreeList(pending);
 		for (;;) {
 			final RevCommit c = s.next();
-			if (c == null)
+			if (c == null) {
 				break;
-			for (RevCommit p : c.parents)
-				p.inDegree++;
+			}
+			for (int i = 0; i < c.parents.length; i++) {
+				if (firstParent && i > 0) {
+					break;
+				}
+				c.parents[i].inDegree++;
+			}
 			pending.add(c);
 		}
 	}
@@ -113,7 +119,11 @@ RevCommit next() throws MissingObjectException,
 			// All of our children have already produced,
 			// so it is OK for us to produce now as well.
 			//
-			for (RevCommit p : c.parents) {
+			for (int i = 0; i < c.parents.length; i++) {
+				if (firstParent && i > 0) {
+					break;
+				}
+				RevCommit p = c.parents[i];
 				if (--p.inDegree == 0 && (p.flags & TOPO_DELAY) != 0) {
 					// This parent tried to come before us, but we are
 					// his last child. unpop the parent so it goes right