TO-231: gerrit topic integration

Checkout entire Gerrit topic at once

Change-Id: I9cff3b60ba9c0423709afe827c967ced76fb32cf
diff --git a/cmd/jiri/patch.go b/cmd/jiri/patch.go
index 8d61930..013b560 100644
--- a/cmd/jiri/patch.go
+++ b/cmd/jiri/patch.go
@@ -7,26 +7,34 @@
 import (
 	"fmt"
 	"net/url"
+	"os"
 	"strconv"
 	"strings"
 
 	"fuchsia.googlesource.com/jiri"
 	"fuchsia.googlesource.com/jiri/cmdline"
 	"fuchsia.googlesource.com/jiri/gerrit"
+	"fuchsia.googlesource.com/jiri/git"
 	"fuchsia.googlesource.com/jiri/gitutil"
 	"fuchsia.googlesource.com/jiri/project"
 )
 
 var (
-	rebaseFlag bool
+	patchRebaseFlag bool
+	patchTopicFlag  bool
+	patchBranchFlag string
+	patchDeleteFlag bool
+	patchHostFlag   string
+	patchForceFlag  bool
 )
 
 func init() {
-	cmdPatch.Flags.StringVar(&branchFlag, "branch", "", "Name of the branch the patch will be applied to")
-	cmdPatch.Flags.BoolVar(&deleteFlag, "delete", false, "Delete the existing branch if already exists")
-	cmdPatch.Flags.BoolVar(&forceFlag, "force", false, "Use force when deleting the existing branch")
-	cmdPatch.Flags.BoolVar(&rebaseFlag, "rebase", false, "Rebase the change after downloading")
-	cmdPatch.Flags.StringVar(&hostFlag, "host", "", `Gerrit host to use. Defaults to gerrit host specified in manifest.`)
+	cmdPatch.Flags.StringVar(&patchBranchFlag, "branch", "", "Name of the branch the patch will be applied to")
+	cmdPatch.Flags.BoolVar(&patchDeleteFlag, "delete", false, "Delete the existing branch if already exists")
+	cmdPatch.Flags.BoolVar(&patchForceFlag, "force", false, "Use force when deleting the existing branch")
+	cmdPatch.Flags.BoolVar(&patchRebaseFlag, "rebase", false, "Rebase the change after downloading")
+	cmdPatch.Flags.StringVar(&patchHostFlag, "host", "", `Gerrit host to use. Defaults to gerrit host specified in manifest.`)
+	cmdPatch.Flags.BoolVar(&patchTopicFlag, "topic", false, `Patch whole topic.`)
 }
 
 // cmdPatch represents the "jiri patch" command.
@@ -44,62 +52,82 @@
 -branch flag. The command will fail if the branch already exists. The -delete
 flag will delete the branch if already exists. Use the -force flag to force
 deleting the branch even if it contains unmerged changes).
+
+if -topic flag is true jiri will fetch whole topic and will try to apply to
+indivisual projects. Patch will assume topic is of form {USER}-{BRANCH} and
+will try to create branch name out of it. If this fails default branch name
+would be same as topic. Currently patch does not support the scenario when
+change "B" is created on top of "A" and both have same topic.
 `,
-	ArgsName: "<change>",
-	ArgsLong: "<change> is a change ID or a full reference.",
+	ArgsName: "<change or topic>",
+	ArgsLong: "<change or topic> is a change ID, full reference or topic when -topic is true.",
 }
 
 // patchProject checks out the given change.
-func patchProject(jirix *jiri.X, project project.Project, ref string) error {
-	var branch string
-	if branchFlag != "" {
-		branch = branchFlag
-	} else {
+func patchProject(jirix *jiri.X, project project.Project, ref, branch, remote string) (bool, error) {
+	if branch == "" {
 		cl, ps, err := gerrit.ParseRefString(ref)
 		if err != nil {
-			return err
+			return false, err
 		}
 		branch = fmt.Sprintf("change/%v/%v", cl, ps)
 	}
-
-	git := gitutil.New(jirix, gitutil.RootDirOpt(project.Path))
-	if git.BranchExists(branch) {
-		if deleteFlag {
-			if err := git.CheckoutBranch("origin/master"); err != nil {
-				return err
+	jirix.Logger.Infof("Patching project %s(%s) on branch %q\n", project.Name, project.Path, branch)
+	scm := gitutil.New(jirix, gitutil.RootDirOpt(project.Path))
+	g := git.NewGit(project.Path)
+	if scm.BranchExists(branch) {
+		if patchDeleteFlag {
+			if err := scm.CheckoutBranch("origin/master"); err != nil {
+				return false, err
 			}
-			if err := git.DeleteBranch(branch, gitutil.ForceOpt(forceFlag)); err != nil {
-				return err
+			if err := scm.DeleteBranch(branch, gitutil.ForceOpt(patchForceFlag)); err != nil {
+				jirix.Logger.Errorf("Cannot delete branch %q: %s", branch, err)
+				jirix.IncrementFailures()
+				return false, nil
 			}
 		} else {
-			return fmt.Errorf("branch %v already exists in project %q", branch, project.Name)
+			jirix.Logger.Errorf("Branch %q already exists in project %q", branch, project.Name)
+			jirix.IncrementFailures()
+			return false, nil
 		}
 	}
-	if err := git.FetchRefspec("origin", ref); err != nil {
-		return err
-	}
-	if err := git.CreateBranchWithUpstream(branch, "FETCH_HEAD"); err != nil {
-		return err
-	}
-	if err := git.CheckoutBranch(branch); err != nil {
-		return err
+	if err := scm.FetchRefspec("origin", ref); err != nil {
+		return false, err
 	}
 
-	return nil
+	if err := g.CreateBranchFromRef(branch, "FETCH_HEAD"); err != nil {
+		return false, err
+	}
+
+	if err := g.SetUpstream(branch, "origin/"+remote); err != nil {
+		return false, err
+	}
+
+	if err := scm.CheckoutBranch(branch); err != nil {
+		return false, err
+	}
+	jirix.Logger.Infof("Project patched\n")
+	return true, nil
 }
 
 // rebaseProject rebases the current branch on top of a given branch.
-func rebaseProject(jirix *jiri.X, project project.Project, change *gerrit.Change) error {
-	git := gitutil.New(jirix, gitutil.UserNameOpt(change.Owner.Name), gitutil.UserEmailOpt(change.Owner.Email), gitutil.RootDirOpt(project.Path))
-	if err := git.FetchRefspec("origin", change.Branch); err != nil {
-		return err
+func rebaseProject(jirix *jiri.X, project project.Project, change gerrit.Change) error {
+	jirix.Logger.Infof("Rebasing project %s(%s)\n", project.Name, project.Path)
+	scm := gitutil.New(jirix, gitutil.UserNameOpt(change.Owner.Name), gitutil.UserEmailOpt(change.Owner.Email), gitutil.RootDirOpt(project.Path))
+	if err := scm.FetchRefspec("origin", change.Branch); err != nil {
+		jirix.Logger.Errorf("Not able to fetch branch %q: %s", change.Branch, err)
+		jirix.IncrementFailures()
+		return nil
 	}
-	if err := git.Rebase("origin/" + change.Branch); err != nil {
-		if err := git.RebaseAbort(); err != nil {
+	if err := scm.Rebase("origin/" + change.Branch); err != nil {
+		if err := scm.RebaseAbort(); err != nil {
 			return err
 		}
-		return fmt.Errorf("Cannot rebase the change: %v", err)
+		jirix.Logger.Errorf("Cannot rebase the change: %s", err)
+		jirix.IncrementFailures()
+		return nil
 	}
+	jirix.Logger.Infof("Project rebased\n")
 	return nil
 }
 
@@ -109,16 +137,22 @@
 	}
 	arg := args[0]
 
-	cl, ps, err := gerrit.ParseRefString(arg)
-	if err != nil {
-		cl, err = strconv.Atoi(arg)
+	var cl int
+	var ps int
+	var err error
+	if !patchTopicFlag {
+		cl, ps, err = gerrit.ParseRefString(arg)
 		if err != nil {
-			return fmt.Errorf("invalid argument: %v", arg)
+			cl, err = strconv.Atoi(arg)
+			if err != nil {
+				return fmt.Errorf("invalid argument: %v", arg)
+			}
 		}
 	}
 
-	if p, err := currentProject(jirix); err == nil {
-		host := hostFlag
+	p, perr := currentProject(jirix)
+	if !patchTopicFlag && perr == nil {
+		host := patchHostFlag
 		if host == "" {
 			if p.GerritHost == "" {
 				return fmt.Errorf("no Gerrit host; use the '--host' flag, or add a 'gerrithost' attribute for project %q", p.Name)
@@ -135,23 +169,32 @@
 		if err != nil {
 			return err
 		}
+		branch := patchBranchFlag
+		ok := false
 		if ps != -1 {
-			if err := patchProject(jirix, p, arg); err != nil {
+			if ok, err = patchProject(jirix, p, arg, branch, change.Branch); err != nil {
 				return err
 			}
 		} else {
-			if err := patchProject(jirix, p, change.Reference()); err != nil {
+			if ok, err = patchProject(jirix, p, change.Reference(), branch, change.Branch); err != nil {
 				return err
 			}
 		}
-		if rebaseFlag {
-			if err := rebaseProject(jirix, p, change); err != nil {
+		if ok && patchRebaseFlag {
+			if err := rebaseProject(jirix, p, *change); err != nil {
 				return err
 			}
 		}
 	} else {
-		host := hostFlag
-		if host == "" {
+		host := patchHostFlag
+		if host == "" && patchTopicFlag {
+			if perr == nil {
+				host = p.GerritHost
+			}
+			if host == "" {
+				return fmt.Errorf("no Gerrit host; use the '--host' flag or run from inside a project with gerrit host")
+			}
+		} else if host == "" {
 			return fmt.Errorf("no Gerrit host; use the '--host' flag")
 		}
 		hostUrl, err := url.Parse(host)
@@ -160,36 +203,72 @@
 		}
 		g := jirix.Gerrit(hostUrl)
 
-		change, err := g.GetChange(cl)
-		if err != nil {
-			return err
-		}
-		var ref string
-		if ps != -1 {
-			ref = arg
+		var changes gerrit.CLList
+		branch := patchBranchFlag
+		if patchTopicFlag {
+			changes, err = g.ListOpenChangesByTopic(arg)
+			if err != nil {
+				return err
+			}
+			if len(changes) == 0 {
+				return fmt.Errorf("No changes found with topic %q", arg)
+			}
+			ps = -1
+			if branch == "" {
+				userPrefix := os.Getenv("USER") + "-"
+				if strings.HasPrefix(arg, userPrefix) {
+					branch = strings.Replace(arg, userPrefix, "", 1)
+				} else {
+					branch = arg
+				}
+			}
 		} else {
-			ref = change.Reference()
+			change, err := g.GetChange(cl)
+			if err != nil {
+				return err
+			}
+			changes = append(changes, *change)
 		}
-
-		projects, _, err := project.LoadManifest(jirix)
+		projects, err := project.LocalProjects(jirix, project.FastScan)
 		if err != nil {
 			return err
 		}
-
-		for _, p := range projects {
-			if strings.HasSuffix(p.Remote, change.Project) {
-				if err := patchProject(jirix, p, ref); err != nil {
+		for _, change := range changes {
+			var ref string
+			if ps != -1 {
+				ref = arg
+			} else {
+				ref = change.Reference()
+			}
+			projFound := false
+			for _, p := range projects {
+				if strings.HasSuffix(p.Remote, "/"+change.Project) {
+					projFound = true
+					if ok, err := patchProject(jirix, p, ref, branch, change.Branch); err != nil {
+						return err
+					} else if ok {
+						if patchRebaseFlag {
+							if err := rebaseProject(jirix, p, change); err != nil {
+								return err
+							}
+						}
+					}
+					fmt.Println()
+				}
+			}
+			if !projFound {
+				cl, _, err := gerrit.ParseRefString(ref)
+				if err != nil {
 					return err
 				}
-				if rebaseFlag {
-					if err := rebaseProject(jirix, p, change); err != nil {
-						return err
-					}
-				}
-				break
+				jirix.Logger.Errorf("Cannot find project to patch CL %s\n", g.GetChangeURL(cl))
+				jirix.IncrementFailures()
+				fmt.Println()
 			}
 		}
 	}
-
+	if jirix.Failures() != 0 {
+		return fmt.Errorf("Patch failed")
+	}
 	return nil
 }
diff --git a/gerrit/gerrit.go b/gerrit/gerrit.go
index 0b6a4b2..a909ae6 100644
--- a/gerrit/gerrit.go
+++ b/gerrit/gerrit.go
@@ -367,6 +367,10 @@
 	return parseQueryResults(res.Body)
 }
 
+func (g *Gerrit) ListOpenChangesByTopic(topic string) (CLList, error) {
+	return g.Query("topic:\"" + topic + "\" status:open")
+}
+
 // GetChange returns a Change object for the given changeId number.
 func (g *Gerrit) GetChange(changeNumber int) (*Change, error) {
 	clList, err := g.Query(fmt.Sprintf("%d", changeNumber))
@@ -384,6 +388,10 @@
 	return &clList[0], nil
 }
 
+func (g *Gerrit) GetChangeURL(changeNumber int) string {
+	return fmt.Sprintf("%s/c/%d", g.host, changeNumber)
+}
+
 // Submit submits the given changelist through Gerrit.
 func (g *Gerrit) Submit(changeID string) (e error) {
 	cred, err := hostCredentials(g.s, g.host)
diff --git a/git/git.go b/git/git.go
index f1c2b08..c7064bb 100644
--- a/git/git.go
+++ b/git/git.go
@@ -120,6 +120,44 @@
 	}
 }
 
+func (g *Git) SetUpstream(branch, upstream string) error {
+	repo, err := git2go.OpenRepository(g.rootDir)
+	if err != nil {
+		return err
+	}
+	defer repo.Free()
+	b, err := repo.LookupBranch(branch, git2go.BranchLocal)
+	if err != nil {
+		return err
+	}
+	return b.SetUpstream(upstream)
+}
+
+func (g *Git) CreateBranchFromRef(branch, ref string) error {
+	repo, err := git2go.OpenRepository(g.rootDir)
+	if err != nil {
+		return err
+	}
+	defer repo.Free()
+	obj, err := repo.RevparseSingle(ref)
+	if err != nil {
+		return err
+	}
+	defer obj.Free()
+	c, err := obj.Peel(git2go.ObjectCommit)
+	if err != nil {
+		return err
+	}
+	defer c.Free()
+	commit, err := c.AsCommit()
+	if err != nil {
+		return err
+	}
+	defer commit.Free()
+	_, err = repo.CreateBranch(branch, commit, false)
+	return err
+}
+
 func (g *Git) HasUntrackedFiles() (bool, error) {
 	repo, err := git2go.OpenRepository(g.rootDir)
 	if err != nil {