More work on create branch command

Added logic to check that a branch from a particular version
has not already been created. Also a bunch of small miscellaneous stuff.

TEST=run_tests.sh
BUG=chromium:980346

Change-Id: Ie948c2c5633eafe5a9ab619a2f1a56017dcd977d
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/infra/go/+/1690639
Reviewed-by: Sean Abraham <seanabraham@chromium.org>
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/checkout.go b/cmd/branch_util/checkout.go
index e051e60..b050d39 100644
--- a/cmd/branch_util/checkout.go
+++ b/cmd/branch_util/checkout.go
@@ -7,13 +7,18 @@
 	"fmt"
 	"log"
 	"os"
+	"path/filepath"
+	"regexp"
 
+	"go.chromium.org/chromiumos/infra/go/internal/git"
+	"go.chromium.org/chromiumos/infra/go/internal/repo"
 	"go.chromium.org/chromiumos/infra/go/internal/repo_util"
 )
 
 type CrosCheckout struct {
 	initialized bool
 	root        string
+	Manifest    repo.Manifest
 }
 
 func (c *CrosCheckout) Initialize(root, manifestUrl string) error {
@@ -46,6 +51,39 @@
 	}
 	log.Printf("Syncing checkout %s to manifest %s.", c.root, path)
 	repository := &repo_util.Repository{Root: c.root}
-	err := repository.SyncToFile(path, RepoToolPath)
+	if !skipSync {
+		if err := repository.SyncToFile(path, RepoToolPath); err != nil {
+			return err
+		}
+	}
+	var err error
+	c.Manifest, err = repository.Manifest(RepoToolPath)
 	return err
 }
+
+func (c *CrosCheckout) ReadVersion() repo.VersionInfo {
+	vinfo, err := repo.GetVersionInfoFromRepo(c.root)
+	if err != nil {
+		return repo.VersionInfo{}
+	}
+	return vinfo
+}
+
+// AbsolutePath joins the path components with the repo root.
+func (c *CrosCheckout) AbsolutePath(args ...string) string {
+	args = append([]string{c.root}, args...)
+	return filepath.Join(args...)
+}
+
+// AbsoluteProjectPath joins the path components with the project's root.
+func (c *CrosCheckout) AbsoluteProjectPath(project repo.Project, args ...string) string {
+	args = append([]string{project.Path}, args...)
+	return c.AbsolutePath(args...)
+}
+
+// BranchExists determines if any branch exists in the specified project
+// that matches the specified pattern.
+func (c *CrosCheckout) BranchExists(project repo.Project, pattern *regexp.Regexp) (bool, error) {
+	matches, err := git.MatchBranchName(c.AbsoluteProjectPath(project), pattern)
+	return len(matches) != 0, err
+}
diff --git a/cmd/branch_util/common.go b/cmd/branch_util/common.go
index ac9b793..3a2c34d 100644
--- a/cmd/branch_util/common.go
+++ b/cmd/branch_util/common.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 main
 
 import (
@@ -5,6 +8,7 @@
 	"io/ioutil"
 
 	"github.com/maruel/subcommands"
+	"go.chromium.org/luci/common/errors"
 )
 
 type branchCommand interface {
@@ -54,7 +58,7 @@
 	env subcommands.Env) int {
 	ok, errMsg := c.validate(args)
 	if !ok {
-		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), errMsg)
+		fmt.Fprintf(a.GetErr(), errMsg+"\n")
 		return 1
 	}
 
@@ -64,7 +68,7 @@
 		root, err = ioutil.TempDir("", "cros-branch-")
 		// TODO(jackneus): Delete tmp dir at end.
 		if err != nil {
-			fmt.Fprintf(a.GetErr(), "Error. Tmp root could not be created: %s", err)
+			fmt.Fprintf(a.GetErr(), errors.Annotate(err, "tmp root could not be created").Err().Error()+"\n")
 			return 1
 		}
 	}
diff --git a/cmd/branch_util/create.go b/cmd/branch_util/create.go
index b2df0ec..8089c06 100644
--- a/cmd/branch_util/create.go
+++ b/cmd/branch_util/create.go
@@ -1,9 +1,20 @@
+// 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 (
 	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
 
 	"github.com/maruel/subcommands"
+	"go.chromium.org/luci/common/errors"
+)
+
+var (
+	skipSync bool
 )
 
 var cmdCreateBranch = &subcommands.Command{
@@ -49,6 +60,13 @@
 				"names to determine which versions have already been branched. "+
 				"Version validation is not possible when the naming convention "+
 				"is broken. Use this at your own risk.")
+		// Dev flags
+		c.Flags.BoolVar(&skipSync, "skip_sync",
+			false,
+			"Used for development purposes. Assumes that you have a properly synced "+
+				"repo at the specified root and does not call `repo sync`. Do not use this "+
+				"unless you know what you're doing! The tool will likely break if you misuse "+
+				"this flag.")
 		return c
 	},
 }
@@ -64,6 +82,7 @@
 	firmware   bool
 	stabilize  bool
 	custom     string
+	skipSync   bool
 }
 
 func (c *createBranchRun) getBranchType() (string, bool) {
@@ -98,11 +117,11 @@
 
 func (c *createBranchRun) validate(args []string) (bool, string) {
 	if c.file == "" {
-		return false, "Must set --file."
+		return false, "must set --file."
 	}
 	_, ok := c.getBranchType()
 	if !ok {
-		return false, "Must select exactly one branch type " +
+		return false, "must select exactly one branch type " +
 			"(--release, --factory, --firmware, --stabilize, --custom)."
 	}
 	if c.descriptor != "" && c.custom != "" {
@@ -111,6 +130,9 @@
 	if c.version != "" && c.version[len(c.version)-1] != '0' {
 		return false, "cannot branch version from nonzero patch number."
 	}
+	if c.skipSync && c.Root == "" {
+		return false, "cannot use --skip_sync without --root."
+	}
 	return true, ""
 }
 
@@ -139,5 +161,42 @@
 		return 1
 	}
 
+	// Validate the version.
+	// Double check that the checkout has a zero patch number. Otherwise,
+	// we cannot branch from it.
+	vinfo := checkout.ReadVersion()
+	if vinfo.PatchNumber != 0 {
+		fmt.Fprintf(a.GetErr(), "Cannot branch version with nonzero patch number (version %s).",
+			vinfo.VersionString())
+		return 1
+	}
+
+	// Check that we did not already branch from this version.
+	// manifest-internal serves as the sentinel project.
+	manifestInternal, err := checkout.Manifest.GetUniqueProject("chromeos/manifest-internal")
+	if err != nil {
+		fmt.Fprintf(a.GetErr(),
+			errors.Annotate(err, "Could not get chromeos/manifest-internal project.").Err().Error())
+		return 1
+	}
+	var nonzeroVersionComponents []string
+	for _, component := range vinfo.VersionComponents() {
+		if component == 0 {
+			continue
+		}
+		nonzeroVersionComponents = append(nonzeroVersionComponents, strconv.Itoa(component))
+	}
+	majorVersion := strings.Join(nonzeroVersionComponents, `.`)
+	pattern := regexp.MustCompile(fmt.Sprintf(`.*-%s.B$`, majorVersion))
+	branchExists, err := checkout.BranchExists(manifestInternal, pattern)
+	if err != nil {
+		fmt.Fprintf(a.GetErr(), err.Error())
+		return 1
+	}
+	if branchExists && !c.Force {
+		fmt.Fprintf(a.GetErr(), "Already branched %s. Please rerun with --force if you "+
+			"would like to proceed.", vinfo.VersionString())
+	}
+
 	return 0
 }
diff --git a/cmd/branch_util/delete.go b/cmd/branch_util/delete.go
index 6b1e3b8..06461ea 100644
--- a/cmd/branch_util/delete.go
+++ b/cmd/branch_util/delete.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 main
 
 import (
@@ -23,7 +26,7 @@
 
 func (c *deleteBranchRun) validate(args []string) (bool, string) {
 	if len(args) < 1 {
-		return false, "Missing required argument(s)."
+		return false, "missing required argument(s)."
 	} else {
 		c.branch_name = args[0]
 	}
diff --git a/cmd/branch_util/rename.go b/cmd/branch_util/rename.go
index f5ec538..ab6bb9e 100644
--- a/cmd/branch_util/rename.go
+++ b/cmd/branch_util/rename.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 main
 
 import (
@@ -25,7 +28,7 @@
 
 func (c *renameBranchRun) validate(args []string) (bool, string) {
 	if len(args) < 2 {
-		return false, "Missing required argument(s)."
+		return false, "missing required argument(s)."
 	} else {
 		c.old = args[0]
 		c.new = args[1]
diff --git a/internal/repo/manifest_version.go b/internal/repo/manifest_version.go
index 353a72a..5e143a5 100644
--- a/internal/repo/manifest_version.go
+++ b/internal/repo/manifest_version.go
@@ -10,17 +10,15 @@
 	"path/filepath"
 	"regexp"
 	"strconv"
-	"strings"
 )
 
-type IncrType string
+type VersionComponent string
 
 const (
-	Unspecified  IncrType = ""
-	ChromeBranch IncrType = "chrome_branch"
-	Build        IncrType = "build"
-	Branch       IncrType = "branch"
-	Patch        IncrType = "patch"
+	ChromeBranch VersionComponent = "CHROME_BRANCH"
+	Build        VersionComponent = "CHROMEOS_BUILD"
+	Branch       VersionComponent = "CHROMEOS_BRANCH"
+	Patch        VersionComponent = "CHROMEOS_PATCH"
 )
 
 // This is a var and not a const for testing purposes.
@@ -30,26 +28,25 @@
 )
 
 const (
-	keyValueRegex string = `%s=(\d+)\s*$`
+	keyValueRegex string = `%s=(\d+)\b`
 )
 
-var (
-	chromeBranchRegex   *regexp.Regexp = regexp.MustCompile(fmt.Sprintf(keyValueRegex, "CHROME_BRANCH"))
-	chromeosBuildRegex  *regexp.Regexp = regexp.MustCompile(fmt.Sprintf(keyValueRegex, "CHROMEOS_BUILD"))
-	chromeosBranchRegex *regexp.Regexp = regexp.MustCompile(fmt.Sprintf(keyValueRegex, "CHROMEOS_BRANCH"))
-	chromeosPatchRegex  *regexp.Regexp = regexp.MustCompile(fmt.Sprintf(keyValueRegex, "CHROMEOS_PATCH"))
-)
-
-type VersionInfo struct {
-	BuildNumber       string
-	BranchBuildNumber string
-	PatchNumber       string
-	ChromeBranch      string
-	VersionFile       string
-	incrType          IncrType
+var chromeosVersionMapping = map[VersionComponent](*regexp.Regexp){
+	ChromeBranch: regexp.MustCompile(fmt.Sprintf(keyValueRegex, ChromeBranch)),
+	Build:        regexp.MustCompile(fmt.Sprintf(keyValueRegex, Build)),
+	Branch:       regexp.MustCompile(fmt.Sprintf(keyValueRegex, Branch)),
+	Patch:        regexp.MustCompile(fmt.Sprintf(keyValueRegex, Patch)),
 }
 
-func GetVersionInfoFromRepo(sourceRepo string, incrType IncrType) (VersionInfo, error) {
+type VersionInfo struct {
+	ChromeBranch      int
+	BuildNumber       int
+	BranchBuildNumber int
+	PatchNumber       int
+	VersionFile       string
+}
+
+func GetVersionInfoFromRepo(sourceRepo string) (VersionInfo, error) {
 	var v VersionInfo
 	v.VersionFile = filepath.Join(sourceRepo, versionFilePath)
 
@@ -58,36 +55,29 @@
 		return VersionInfo{}, fmt.Errorf("could not read version file %s", v.VersionFile)
 	}
 
-	for _, line := range strings.Split(string(fileData), "\n") {
-		if strings.TrimSpace(line) == "" {
+	for field, pattern := range chromeosVersionMapping {
+		if match := findValue(pattern, string(fileData)); match != "" {
+			num, err := strconv.Atoi(match)
+			if err != nil {
+				log.Fatal(fmt.Sprintf("%s value %s could not be converted to integer.", field, match))
+			}
+			switch field {
+			case ChromeBranch:
+				v.ChromeBranch = num
+			case Build:
+				v.BuildNumber = num
+			case Branch:
+				v.BranchBuildNumber = num
+			case Patch:
+				v.PatchNumber = num
+			default:
+				// This should never happen.
+				log.Fatal("Invalid version component.")
+			}
 			continue
 		}
+	}
 
-		if match := findValue(chromeBranchRegex, line); match != "" {
-			v.ChromeBranch = match
-			log.Printf("Set the Chrome branch number to: %s", v.ChromeBranch)
-			continue
-		}
-		if match := findValue(chromeosBuildRegex, line); match != "" {
-			v.BuildNumber = match
-			log.Printf("Set the Chrome branch number to: %s", v.BuildNumber)
-			continue
-		}
-		if match := findValue(chromeosBranchRegex, line); match != "" {
-			v.BranchBuildNumber = match
-			log.Printf("Set the Chrome branch number to: %s", v.BranchBuildNumber)
-			continue
-		}
-		if match := findValue(chromeosPatchRegex, line); match != "" {
-			v.PatchNumber = match
-			log.Printf("Set the Chrome branch number to: %s", v.PatchNumber)
-			continue
-		}
-	}
-	if incrType == Unspecified {
-		incrType = Build
-	}
-	v.incrType = incrType
 	return v, nil
 }
 
@@ -100,21 +90,21 @@
 	return string(match[1])
 }
 
-func (v *VersionInfo) IncrementVersion() string {
-	if v.incrType == ChromeBranch {
-		v.ChromeBranch = incrString(v.ChromeBranch)
+func (v *VersionInfo) IncrementVersion(incrType VersionComponent) string {
+	if incrType == ChromeBranch {
+		v.ChromeBranch += 1
 	}
 
 	// Increment build_number for ChromeBranch incrType to avoid
 	// crbug.com/213075.
-	if v.incrType == ChromeBranch || v.incrType == Build {
-		v.BuildNumber = incrString(v.BuildNumber)
-		v.BranchBuildNumber = "0"
-		v.PatchNumber = "0"
-	} else if v.incrType == Branch && v.PatchNumber == "0" {
-		v.BranchBuildNumber = incrString(v.BranchBuildNumber)
+	if incrType == ChromeBranch || incrType == Build {
+		v.BuildNumber += 1
+		v.BranchBuildNumber = 0
+		v.PatchNumber = 0
+	} else if incrType == Branch && v.PatchNumber == 0 {
+		v.BranchBuildNumber += 1
 	} else {
-		v.PatchNumber = incrString(v.PatchNumber)
+		v.PatchNumber += 1
 	}
 
 	return v.VersionString()
@@ -129,5 +119,9 @@
 }
 
 func (v *VersionInfo) VersionString() string {
-	return fmt.Sprintf("%s.%s.%s", v.BuildNumber, v.BranchBuildNumber, v.PatchNumber)
+	return fmt.Sprintf("%d.%d.%d", v.BuildNumber, v.BranchBuildNumber, v.PatchNumber)
+}
+
+func (v *VersionInfo) VersionComponents() []int {
+	return []int{v.BuildNumber, v.BranchBuildNumber, v.PatchNumber}
 }
diff --git a/internal/repo/manifest_version_test.go b/internal/repo/manifest_version_test.go
index af46da6..0adcaaf 100644
--- a/internal/repo/manifest_version_test.go
+++ b/internal/repo/manifest_version_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 (
@@ -5,7 +8,7 @@
 	"testing"
 )
 
-func assertVersionEqual(t *testing.T, v VersionInfo, expected []string) {
+func assertVersionEqual(t *testing.T, v VersionInfo, expected []int) {
 	assert.Equal(t, v.ChromeBranch, expected[0])
 	assert.Equal(t, v.BuildNumber, expected[1])
 	assert.Equal(t, v.BranchBuildNumber, expected[2])
@@ -14,57 +17,67 @@
 
 func TestGetVersionInfoFromRepo_success(t *testing.T) {
 	versionFilePath = "chromeos_version.sh"
-	versionInfo, err := GetVersionInfoFromRepo("test_data", Build)
+	versionInfo, err := GetVersionInfoFromRepo("test_data")
 	assert.NilError(t, err)
-	assertVersionEqual(t, versionInfo, []string{"77", "12302", "1", "0"})
+	assertVersionEqual(t, versionInfo, []int{77, 12302, 1, 0})
 }
 
 func TestIncrementVersion_ChromeBranch(t *testing.T) {
 	versionFilePath = "chromeos_version.sh"
-	versionInfo, err := GetVersionInfoFromRepo("test_data", ChromeBranch)
-	versionInfo.IncrementVersion()
+	versionInfo, err := GetVersionInfoFromRepo("test_data")
+	versionInfo.IncrementVersion(ChromeBranch)
 	assert.NilError(t, err)
-	assertVersionEqual(t, versionInfo, []string{"78", "12303", "0", "0"})
+	assertVersionEqual(t, versionInfo, []int{78, 12303, 0, 0})
 }
 
 func TestIncrementVersion_Build(t *testing.T) {
 	versionFilePath = "chromeos_version.sh"
-	versionInfo, err := GetVersionInfoFromRepo("test_data", Build)
-	versionInfo.IncrementVersion()
+	versionInfo, err := GetVersionInfoFromRepo("test_data")
+	versionInfo.IncrementVersion(Build)
 	assert.NilError(t, err)
-	assertVersionEqual(t, versionInfo, []string{"77", "12303", "0", "0"})
+	assertVersionEqual(t, versionInfo, []int{77, 12303, 0, 0})
 }
 
 func TestIncrementVersion_Branch(t *testing.T) {
 	versionFilePath = "chromeos_version.sh"
-	versionInfo, err := GetVersionInfoFromRepo("test_data", Branch)
-	versionInfo.IncrementVersion()
+	versionInfo, err := GetVersionInfoFromRepo("test_data")
+	versionInfo.IncrementVersion(Branch)
 	assert.NilError(t, err)
-	assertVersionEqual(t, versionInfo, []string{"77", "12302", "2", "0"})
+	assertVersionEqual(t, versionInfo, []int{77, 12302, 2, 0})
 }
 
 func TestIncrementVersion_Branch_nonzero(t *testing.T) {
 	versionFilePath = "chromeos_version.sh"
-	versionInfo, err := GetVersionInfoFromRepo("test_data", Branch)
-	versionInfo.PatchNumber = "1"
-	versionInfo.IncrementVersion()
+	versionInfo, err := GetVersionInfoFromRepo("test_data")
+	versionInfo.PatchNumber = 1
+	versionInfo.IncrementVersion(Branch)
 	assert.NilError(t, err)
-	assertVersionEqual(t, versionInfo, []string{"77", "12302", "1", "2"})
+	assertVersionEqual(t, versionInfo, []int{77, 12302, 1, 2})
 }
 
 func TestIncrementVersion_Patch(t *testing.T) {
 	versionFilePath = "chromeos_version.sh"
-	versionInfo, err := GetVersionInfoFromRepo("test_data", Patch)
-	versionInfo.IncrementVersion()
+	versionInfo, err := GetVersionInfoFromRepo("test_data")
+	versionInfo.IncrementVersion(Patch)
 	assert.NilError(t, err)
-	assertVersionEqual(t, versionInfo, []string{"77", "12302", "1", "1"})
+	assertVersionEqual(t, versionInfo, []int{77, 12302, 1, 1})
 }
 
 func TestVersionString(t *testing.T) {
 	versionFilePath = "chromeos_version.sh"
 	var v VersionInfo
-	v.BuildNumber = "123"
-	v.BranchBuildNumber = "1"
-	v.PatchNumber = "0"
+	v.BuildNumber = 123
+	v.BranchBuildNumber = 1
+	v.PatchNumber = 0
 	assert.Equal(t, v.VersionString(), "123.1.0")
 }
+
+func TestVersionComponents(t *testing.T) {
+	versionFilePath = "chromeos_version.sh"
+	var v VersionInfo
+	v.BuildNumber = 123
+	v.BranchBuildNumber = 1
+	v.PatchNumber = 0
+	components := []int{123, 1, 0}
+	assert.DeepEqual(t, v.VersionComponents(), components)
+}