git-codereview: parse branch commits correctly during b.Pending

Change-Id: I71b087ec51f6101022e950f783e09e42f7e4e57d
Reviewed-on: https://go-review.googlesource.com/2787
Reviewed-by: Andrew Gerrand <adg@golang.org>
diff --git a/git-codereview/branch.go b/git-codereview/branch.go
index cdec9c3..2a69dcd 100644
--- a/git-codereview/branch.go
+++ b/git-codereview/branch.go
@@ -28,6 +28,7 @@
 	Hash      string // commit hash
 	ShortHash string // abbreviated commit hash
 	Parent    string // parent hash
+	Merge     string // for merges, hash of commit being merged into Parent
 	Message   string // commit message
 	Subject   string // first line of commit message
 	ChangeID  string // Change-Id in commit message ("" if missing)
@@ -119,8 +120,9 @@
 	}
 
 	// Note: --topo-order means child first, then parent.
+	origin := b.OriginBranch()
 	const numField = 5
-	all := getOutput("git", "log", "--topo-order", "--format=format:%H%x00%h%x00%P%x00%B%x00%s%x00", b.OriginBranch()+".."+b.Name, "--")
+	all := getOutput("git", "log", "--topo-order", "--format=format:%H%x00%h%x00%P%x00%B%x00%s%x00", origin+".."+b.Name, "--")
 	fields := strings.Split(all, "\x00")
 	if len(fields) < numField {
 		return // nothing pending
@@ -128,6 +130,7 @@
 	for i, field := range fields {
 		fields[i] = strings.TrimLeft(field, "\r\n")
 	}
+	foundMergeBranchpoint := false
 	for i := 0; i+numField <= len(fields); i += numField {
 		c := &Commit{
 			Hash:      fields[i],
@@ -136,6 +139,25 @@
 			Message:   fields[i+3],
 			Subject:   fields[i+4],
 		}
+		if j := strings.Index(c.Parent, " "); j >= 0 {
+			c.Parent, c.Merge = c.Parent[:j], c.Parent[j+1:]
+			// Found merge point.
+			// Merges break the invariant that the last shared commit (the branchpoint)
+			// is the parent of the final commit in the log output.
+			// If c.Parent is on the origin branch, then since we are reading the log
+			// in (reverse) topological order, we know that c.Parent is the actual branchpoint,
+			// even if we later see additional commits on a different branch leading down to
+			// a lower location on the same origin branch.
+			// Check c.Merge (the second parent) too, so we don't depend on the parent order.
+			if strings.Contains(getOutput("git", "branch", "-a", "--contains", c.Parent), " "+origin+"\n") {
+				foundMergeBranchpoint = true
+				b.branchpoint = c.Parent
+			}
+			if strings.Contains(getOutput("git", "branch", "-a", "--contains", c.Merge), " "+origin+"\n") {
+				foundMergeBranchpoint = true
+				b.branchpoint = c.Merge
+			}
+		}
 		for _, line := range strings.Split(c.Message, "\n") {
 			// Note: Keep going even if we find one, so that
 			// we take the last Change-Id line, just in case
@@ -148,7 +170,9 @@
 		}
 
 		b.pending = append(b.pending, c)
-		b.branchpoint = c.Parent
+		if !foundMergeBranchpoint {
+			b.branchpoint = c.Parent
+		}
 	}
 	b.commitsAhead = len(b.pending)
 	b.commitsBehind = len(getOutput("git", "log", "--format=format:x", b.Name+".."+b.OriginBranch(), "--"))
diff --git a/git-codereview/branch_test.go b/git-codereview/branch_test.go
index fdfb044..9a3ab5c 100644
--- a/git-codereview/branch_test.go
+++ b/git-codereview/branch_test.go
@@ -128,3 +128,31 @@
 		gt.work(t)
 	}
 }
+
+func TestBranchpointMerge(t *testing.T) {
+	gt := newGitTest(t)
+	defer gt.done()
+
+	// commit more work on master
+	write(t, gt.server+"/file", "more work")
+	trun(t, gt.server, "git", "commit", "-m", "work", "file")
+
+	// update client
+	trun(t, gt.client, "git", "checkout", "master")
+	trun(t, gt.client, "git", "pull")
+
+	hash := strings.TrimSpace(trun(t, gt.client, "git", "rev-parse", "HEAD"))
+
+	// merge dev.branch
+	testMain(t, "change", "work")
+	trun(t, gt.client, "git", "merge", "-m", "merge", "origin/dev.branch")
+
+	// check branchpoint is old head (despite this commit having two parents)
+	bp := CurrentBranch().Branchpoint()
+	if bp != hash {
+		t.Logf("branches:\n%s", trun(t, gt.client, "git", "branch", "-a", "-v"))
+		t.Logf("log:\n%s", trun(t, gt.client, "git", "log", "--graph", "--decorate"))
+		t.Logf("log origin/master..HEAD:\n%s", trun(t, gt.client, "git", "log", "origin/master..HEAD"))
+		t.Fatalf("branchpoint=%q, want %q", bp, hash)
+	}
+}