Project branch naming logic

Added logic for naming git branches when creating/renaming branches.

BUG=chromium:980346
TEST=new and existing unit tests

Change-Id: Ia773c399d8445e63c3fd0ebbae9d3187e3425430
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/infra/go/+/1695711
Reviewed-by: Evan Hernandez <evanhernandez@chromium.org>
Commit-Queue: Jack Neus <jackneus@google.com>
Tested-by: Jack Neus <jackneus@google.com>
diff --git a/cmd/branch_util/branch.go b/cmd/branch_util/branch.go
new file mode 100644
index 0000000..b9cedc5
--- /dev/null
+++ b/cmd/branch_util/branch.go
@@ -0,0 +1,45 @@
+// Copyright 2019 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+package main
+
+import (
+	"strings"
+
+	"go.chromium.org/chromiumos/infra/go/internal/git"
+	"go.chromium.org/chromiumos/infra/go/internal/repo"
+)
+
+// projectBranchName determines the git branch name for the project.
+func (c *createBranchRun) projectBranchName(branch string, project repo.Project, original string) string {
+	// If the project has only one checkout, then the base branch name is fine.
+	var checkouts []string
+	manifest := checkout.Manifest()
+	for _, proj := range manifest.Projects {
+		if proj.Name == project.Name {
+			checkouts = append(checkouts, proj.Name)
+		}
+	}
+
+	if len(checkouts) == 1 {
+		return branch
+	}
+
+	// Otherwise, the project name needs a suffix. We append its upstream or
+	// revision to distinguish it from other checkouts.
+	suffix := "-"
+	if project.Upstream != "" {
+		suffix += git.StripRefs(project.Upstream)
+	} else {
+		suffix += git.StripRefs(project.Revision)
+	}
+
+	// If the revision is itself a branch, we need to strip the old branch name
+	// from the suffix to keep naming consistent.
+	if original != "" {
+		if strings.HasPrefix(suffix, "-"+original+"-") {
+			suffix = strings.TrimPrefix(suffix, "-"+original)
+		}
+	}
+	return branch + suffix
+}
diff --git a/cmd/branch_util/branch_test.go b/cmd/branch_util/branch_test.go
new file mode 100644
index 0000000..88b3a0f
--- /dev/null
+++ b/cmd/branch_util/branch_test.go
@@ -0,0 +1,53 @@
+// Copyright 2019 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+package main
+
+import (
+	"github.com/golang/mock/gomock"
+	mock_checkout "go.chromium.org/chromiumos/infra/go/internal/checkout/mock"
+	"go.chromium.org/chromiumos/infra/go/internal/repo"
+	"gotest.tools/assert"
+	"testing"
+)
+
+var testManifest = repo.Manifest{
+	Projects: []repo.Project{
+		{Path: "foo1/", Name: "foo", Revision: "100", Upstream: "refs/heads/factory-100"},
+		{Path: "foo2/", Name: "foo", Revision: "101"},
+		{Path: "bar/", Name: "bar"},
+		{Path: "baz1/", Name: "baz", Upstream: "refs/heads/oldbranch-factory-100"},
+		{Path: "baz2/", Name: "baz", Upstream: "refs/heads/oldbranch-factory-101"},
+	},
+}
+
+func TestProjectBranchName(t *testing.T) {
+	ctl := gomock.NewController(t)
+	defer ctl.Finish()
+
+	m := mock_checkout.NewMockCheckout(ctl)
+	checkout = m
+	c := &createBranchRun{}
+	m.EXPECT().
+		Manifest().
+		Return(testManifest).
+		AnyTimes()
+	assert.Equal(t, c.projectBranchName("mybranch", testManifest.Projects[0], ""), "mybranch-factory-100")
+	assert.Equal(t, c.projectBranchName("mybranch", testManifest.Projects[1], ""), "mybranch-101")
+	assert.Equal(t, c.projectBranchName("mybranch", testManifest.Projects[2], ""), "mybranch")
+}
+
+func TestProjectBranchName_withOriginal(t *testing.T) {
+	ctl := gomock.NewController(t)
+	defer ctl.Finish()
+
+	m := mock_checkout.NewMockCheckout(ctl)
+	checkout = m
+	c := &createBranchRun{}
+	m.EXPECT().
+		Manifest().
+		Return(testManifest).
+		AnyTimes()
+	assert.Equal(t, c.projectBranchName("mybranch", testManifest.Projects[3], "oldbranch"), "mybranch-factory-100")
+	assert.Equal(t, c.projectBranchName("mybranch", testManifest.Projects[4], "oldbranch"), "mybranch-factory-101")
+}
diff --git a/cmd/branch_util/create.go b/cmd/branch_util/create.go
index 79e29d7..1cb8c5e 100644
--- a/cmd/branch_util/create.go
+++ b/cmd/branch_util/create.go
@@ -207,5 +207,7 @@
 			"would like to proceed.", vinfo.VersionString())
 	}
 
+	// TODO(@jackneus): double check name with user via boolean CLI prompt
+
 	return 0
 }
diff --git a/internal/repo/manifest.go b/internal/repo/manifest.go
index 829e7d2..d95007d 100644
--- a/internal/repo/manifest.go
+++ b/internal/repo/manifest.go
@@ -1,3 +1,6 @@
+// Copyright 2019 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
 package repo
 
 import (
@@ -22,14 +25,16 @@
 	Includes []Include `xml:"include"`
 	Projects []Project `xml:"project"`
 	Remotes  []Remote  `xml:"remote"`
-	Default  []Default `xml:"default"`
+	Default  Default   `xml:"default"`
 }
 
 // Project is an element of a manifest containing a Gerrit project to source path definition.
 type Project struct {
-	Path     string `xml:"path,attr"`
-	Name     string `xml:"name,attr"`
-	Revision string `xml:"revision,attr"`
+	Path       string `xml:"path,attr"`
+	Name       string `xml:"name,attr"`
+	Revision   string `xml:"revision,attr"`
+	Upstream   string `xml:"upstream,attr"`
+	RemoteName string `xml:"remote,attr"`
 }
 
 // Include is a manifest element that imports another manifest file.
@@ -46,8 +51,17 @@
 
 // Default is a manifest element that lists the default.
 type Default struct {
-	Remote   string `xml:"remote,attr"`
-	Revision string `xml:"revision,attr"`
+	RemoteName string `xml:"remote,attr"`
+	Revision   string `xml:"revision,attr"`
+}
+
+func (m *Manifest) getRemoteByName(name string) *Remote {
+	for _, remote := range m.Remotes {
+		if remote.Name == name {
+			return &remote
+		}
+	}
+	return &Remote{}
 }
 
 // LoadManifestFromFile loads the manifest at the given file path into
@@ -64,6 +78,22 @@
 	if err = xml.Unmarshal(data, manifest); err != nil {
 		return nil, errors.Annotate(err, "failed to unmarshal %s", file).Err()
 	}
+	for i, project := range manifest.Projects {
+		// Set default remote on projects without an explicit remote
+		if project.RemoteName == "" {
+			project.RemoteName = manifest.Default.RemoteName
+		}
+		// Set default revision on projects without an explicit revision
+		if project.Revision == "" {
+			remote := manifest.getRemoteByName(project.RemoteName)
+			if remote.Revision == "" {
+				project.Revision = manifest.Default.Revision
+			} else {
+				project.Revision = remote.Revision
+			}
+		}
+		manifest.Projects[i] = project
+	}
 	results[file] = manifest
 
 	// Recursively fetch manifests listed in "include" elements.
diff --git a/internal/repo/manifest_test.go b/internal/repo/manifest_test.go
index 829d575..7c87973 100644
--- a/internal/repo/manifest_test.go
+++ b/internal/repo/manifest_test.go
@@ -1,3 +1,6 @@
+// Copyright 2019 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
 package repo
 
 import (
@@ -85,7 +88,9 @@
 	expected_results := make(map[string]*Manifest)
 	expected_results["test_data/foo.xml"] = &Manifest{
 		Projects: []Project{
-			{"baz/", "baz", "123"},
+			{Path: "baz/", Name: "baz", Revision: "123", RemoteName: "chromium"},
+			{Path: "fiz/", Name: "fiz", Revision: "124", RemoteName: "chromeos"},
+			{Path: "buz/", Name: "buz", Revision: "125", RemoteName: "google"},
 		},
 		Includes: []Include{
 			{"bar.xml"},
@@ -93,7 +98,7 @@
 	}
 	expected_results["test_data/bar.xml"] = &Manifest{
 		Projects: []Project{
-			{"baz/", "baz", ""},
+			{Path: "baz/", Name: "baz"},
 		},
 	}
 
@@ -117,9 +122,9 @@
 func TestGetUniqueProject(t *testing.T) {
 	manifest := &Manifest{
 		Projects: []Project{
-			{"foo-a/", "foo", ""},
-			{"foo-b/", "foo", ""},
-			{"bar/", "bar", ""},
+			{Path: "foo-a/", Name: "foo"},
+			{Path: "foo-b/", Name: "foo"},
+			{Path: "bar/", Name: "bar"},
 		},
 	}
 
diff --git a/internal/repo/test_data/foo.xml b/internal/repo/test_data/foo.xml
index ecf06c6..0b600ba 100644
--- a/internal/repo/test_data/foo.xml
+++ b/internal/repo/test_data/foo.xml
@@ -3,5 +3,8 @@
   <include name="bar.xml" />
   <default remote="chromeos" revision="123"/>
   <remote fetch="https://chromium.org/remote" name="chromium"/>
-  <project name="baz" path="baz/" revision="123"/>       
+  <remote fetch="https://google.com/remote" name="google" revision="125"/>
+  <project name="baz" path="baz/" remote="chromium"/>
+  <project name="fiz" path="fiz/" revision="124" />
+  <project name="buz" path="buz/" remote="google" />
 </manifest>
\ No newline at end of file
diff --git a/internal/repo/test_data/unspecified_revisions.xml b/internal/repo/test_data/unspecified_revisions.xml
new file mode 100644
index 0000000..df5fa79
--- /dev/null
+++ b/internal/repo/test_data/unspecified_revisions.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest>
+  <include name="bar.xml" />
+  <default remote="chromeos" revision="123"/>
+  <remote fetch="https://chromium.org/remote" name="chromium"/>
+  <project name="baz" path="baz/"/>
+</manifest>
\ No newline at end of file
diff --git a/internal/repo_util/repo_util_test.go b/internal/repo_util/repo_util_test.go
index e8f89dd..1663146 100644
--- a/internal/repo_util/repo_util_test.go
+++ b/internal/repo_util/repo_util_test.go
@@ -175,9 +175,9 @@
 	}
 	expectedManifest := repo.Manifest{
 		Projects: []repo.Project{
-			repo.Project{Path: "src/foo", Name: "foo"},
-			repo.Project{Path: "src/bar", Name: "bar"},
-			repo.Project{Path: "src/baz", Name: "baz"},
+			{Path: "src/foo", Name: "foo"},
+			{Path: "src/bar", Name: "bar"},
+			{Path: "src/baz", Name: "baz"},
 		},
 	}