[autoroll] Use "git log --first-parent" without "--ancestry-path"

This allows the roller to self-recover when the lastRollRev is not
reachable from the configured branch head but gives us the same set of
roll candidates otherwise.

Bug: skia:9669
Change-Id: I0ba9c1058aa6f655eb0cbae0911638b0584c5c6e
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/256021
Reviewed-by: Ravi Mistry <rmistry@google.com>
Commit-Queue: Eric Boren <borenet@google.com>
diff --git a/autoroll/go/autoroll-google3/google3.go b/autoroll/go/autoroll-google3/google3.go
index 9524302..5f8b450 100644
--- a/autoroll/go/autoroll-google3/google3.go
+++ b/autoroll/go/autoroll-google3/google3.go
@@ -117,7 +117,7 @@
 		if err != nil {
 			return err
 		}
-		revs, err := a.childRepo.LogLinear(ctx, lastSuccessRev, headRev.Hash)
+		revs, err := a.childRepo.LogFirstParent(ctx, lastSuccessRev, headRev.Hash)
 		if err != nil {
 			return err
 		}
diff --git a/autoroll/go/repo_manager/no_checkout_deps_repo_manager.go b/autoroll/go/repo_manager/no_checkout_deps_repo_manager.go
index dd37215..b61eb65 100644
--- a/autoroll/go/repo_manager/no_checkout_deps_repo_manager.go
+++ b/autoroll/go/repo_manager/no_checkout_deps_repo_manager.go
@@ -318,7 +318,7 @@
 
 	// Find the not-yet-rolled child repo commits.
 	// Only consider commits on the "main" branch as roll candidates.
-	notRolled, err := rm.childRepo.LogLinear(ctx, lastRollRev.Id, tipRev.Id)
+	notRolled, err := rm.childRepo.LogFirstParent(ctx, lastRollRev.Id, tipRev.Id)
 	if err != nil {
 		return nil, nil, nil, err
 	}
diff --git a/autoroll/go/repo_manager/repo_manager.go b/autoroll/go/repo_manager/repo_manager.go
index 89579ee..f96cb6a 100644
--- a/autoroll/go/repo_manager/repo_manager.go
+++ b/autoroll/go/repo_manager/repo_manager.go
@@ -224,7 +224,7 @@
 	if tipRev.Id == lastRollRev.Id {
 		return []*revision.Revision{}, nil
 	}
-	commits, err := r.childRepo.RevList(ctx, "--ancestry-path", "--first-parent", git.LogFromTo(lastRollRev.Id, tipRev.Id))
+	commits, err := r.childRepo.RevList(ctx, "--first-parent", git.LogFromTo(lastRollRev.Id, tipRev.Id))
 	if err != nil {
 		return nil, err
 	}
diff --git a/go/gitiles/gitiles.go b/go/gitiles/gitiles.go
index 76eeb53..ab70236 100644
--- a/go/gitiles/gitiles.go
+++ b/go/gitiles/gitiles.go
@@ -356,6 +356,37 @@
 	return rv, nil
 }
 
+// LogFirstParent is equivalent to "git log --first-parent A..B", ie. it
+// only returns commits which are reachable from A by following the first parent
+// (the "main" branch) but not from B.
+func (r *Repo) LogFirstParent(ctx context.Context, from, to string, opts ...LogOption) ([]*vcsinfo.LongCommit, error) {
+	// Retrieve the normal "git log".
+	commits, err := r.Log(ctx, git.LogFromTo(from, to), opts...)
+	if err != nil {
+		return nil, err
+	}
+	if len(commits) == 0 {
+		return commits, nil
+	}
+
+	// Now filter to only those commits which are on the first-parent path.
+	commitsMap := make(map[string]*vcsinfo.LongCommit, len(commits))
+	for _, commit := range commits {
+		commitsMap[commit.Hash] = commit
+	}
+	rv := make([]*vcsinfo.LongCommit, 0, len(commits))
+	c := commitsMap[to]
+	for c != nil {
+		rv = append(rv, c)
+		if len(c.Parents) > 0 {
+			c = commitsMap[c.Parents[0]]
+		} else {
+			c = nil
+		}
+	}
+	return rv, nil
+}
+
 // LogLinear is equivalent to "git log --first-parent --ancestry-path from..to",
 // ie. it only returns commits which are on the direct path from A to B, and
 // only on the "main" branch. This is as opposed to "git log from..to" which
@@ -402,9 +433,11 @@
 		}
 		// If the first parent of this commit is on the direct line,
 		// then this commit is as well.
-		if search(commit.Parents[0]) {
-			isDescendant[hash] = true
-			return true
+		if len(commit.Parents) > 0 {
+			if search(commit.Parents[0]) {
+				isDescendant[hash] = true
+				return true
+			}
 		}
 		return false
 	}
diff --git a/go/gitiles/gitiles_test.go b/go/gitiles/gitiles_test.go
index 1b71400..e8f76f6 100644
--- a/go/gitiles/gitiles_test.go
+++ b/go/gitiles/gitiles_test.go
@@ -158,6 +158,17 @@
 	}
 
 	// Verify that we get the expected list of commits from both Gitiles
+	// (Repo.LogFirstParent) and Git ("git log --date-order --first-parent").
+	checkFirstParent := func(from, to string, expect []string) {
+		checkGit(from, to, expect, "--first-parent")
+		mockLog(from, to, expect)
+		log, err := r.LogFirstParent(ctx, from, to)
+		require.NoError(t, err)
+		deepequal.AssertDeepEqual(t, hashes(log), expect)
+		require.True(t, urlMock.Empty())
+	}
+
+	// Verify that we get the expected list of commits from both Gitiles
 	// (Repo.LogLinear) and Git
 	// ("git log --date-order --first-parent --ancestry-path).
 	checkLinear := func(from, to string, expect []string) {
@@ -171,18 +182,25 @@
 
 	// Test cases.
 	checkBasic(c0, c8, []string{c8, c7, c6, c5, c4, c3, c2, c1})
+	checkFirstParent(c0, c8, []string{c8, c7, c5, c4, c1})
 	checkLinear(c0, c8, []string{c8, c7, c5, c4, c1})
 	checkBasic(c0, c1, []string{c1})
+	checkFirstParent(c0, c1, []string{c1})
 	checkLinear(c0, c1, []string{c1})
 	checkBasic(c2, c4, []string{c4})
+	checkFirstParent(c2, c4, []string{c4})
 	checkLinear(c2, c4, []string{})
 	checkBasic(c1, c2, []string{c2})
+	checkFirstParent(c1, c2, []string{c2})
 	checkLinear(c1, c2, []string{c2})
 	checkBasic(c1, c4, []string{c4})
+	checkFirstParent(c1, c4, []string{c4})
 	checkLinear(c1, c4, []string{c4})
 	checkBasic(c5, c7, []string{c7, c6, c3, c2})
+	checkFirstParent(c5, c7, []string{c7})
 	checkLinear(c5, c7, []string{c7})
 	checkBasic(c2, c7, []string{c7, c6, c5, c4, c3})
+	checkFirstParent(c2, c7, []string{c7, c5, c4})
 	checkLinear(c2, c7, []string{})
 }