blob: 642643a91b13ac4a3263ab77c9738f8162d84e7b [file] [log] [blame]
// Copyright 2016 The Fuchsia 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"
"net/url"
"os"
"path/filepath"
"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 (
uploadCcsFlag string
uploadHostFlag string
uploadPresubmitFlag string
uploadReviewersFlag string
uploadTopicFlag string
uploadVerifyFlag bool
uploadRebaseFlag bool
uploadSetTopicFlag bool
uploadMultipartFlag bool
uploadBranchFlag string
)
var cmdUpload = &cmdline.Command{
Runner: jiri.RunnerFunc(runUpload),
Name: "upload",
Short: "Upload a changelist for review",
Long: `Command "upload" uploads all commits of a local branch to Gerrit.`,
}
func init() {
cmdUpload.Flags.StringVar(&uploadCcsFlag, "cc", "", `Comma-separated list of emails or LDAPs to cc.`)
cmdUpload.Flags.StringVar(&uploadHostFlag, "host", "", `Gerrit host to use. Defaults to gerrit host specified in manifest.`)
cmdUpload.Flags.StringVar(&uploadPresubmitFlag, "presubmit", string(gerrit.PresubmitTestTypeAll),
fmt.Sprintf("The type of presubmit tests to run. Valid values: %s.", strings.Join(gerrit.PresubmitTestTypes(), ",")))
cmdUpload.Flags.StringVar(&uploadReviewersFlag, "r", "", `Comma-separated list of emails or LDAPs to request review.`)
cmdUpload.Flags.StringVar(&uploadTopicFlag, "topic", "", `CL topic. Default is <username>-<branchname>.`)
cmdUpload.Flags.BoolVar(&uploadSetTopicFlag, "set-topic", true, `Set topic.`)
cmdUpload.Flags.BoolVar(&uploadVerifyFlag, "verify", true, `Run pre-push git hooks.`)
cmdUpload.Flags.BoolVar(&uploadRebaseFlag, "rebase", false, `Run rebase before pushing.`)
cmdUpload.Flags.BoolVar(&uploadMultipartFlag, "multipart", false, `Send multipart CL.`)
cmdUpload.Flags.StringVar(&uploadBranchFlag, "branch", "", `Used when multipart flag is true and this command is executed from root folder`)
}
// runUpload is a wrapper that pushes the changes to gerrit for review.
func runUpload(jirix *jiri.X, _ []string) error {
dir, err := os.Getwd()
if err != nil {
return fmt.Errorf("os.Getwd() failed: %s", err)
}
var p *project.Project
// Walk up the path until we find a project at that path, or hit the jirix.Root.
// Note that we can't just compare path prefixes because of soft links.
for dir != jirix.Root && dir != string(filepath.Separator) {
project, err := project.ProjectAtPath(jirix, dir)
if err != nil {
dir = filepath.Dir(dir)
continue
}
p = &project
break
}
currentBranch := ""
if p == nil {
if !uploadMultipartFlag {
return fmt.Errorf("directory %q is not contained in a project", dir)
} else if uploadBranchFlag == "" {
return fmt.Errorf("Please run with -branch flag")
} else {
currentBranch = uploadBranchFlag
}
} else {
scm := gitutil.New(jirix, gitutil.RootDirOpt(p.Path))
if !scm.IsOnBranch() {
return fmt.Errorf("Current project is not on any branch.")
}
currentBranch, err = scm.CurrentBranchName()
if err != nil {
return err
}
}
var projectsToProcess []project.Project
topic := ""
if uploadSetTopicFlag {
if topic = uploadTopicFlag; topic == "" {
topic = fmt.Sprintf("%s-%s", os.Getenv("USER"), currentBranch) // use <username>-<branchname> as the default
}
}
if uploadMultipartFlag {
projects, err := project.LocalProjects(jirix, project.FastScan)
if err != nil {
return err
}
for _, project := range projects {
scm := gitutil.New(jirix, gitutil.RootDirOpt(project.Path))
if scm.IsOnBranch() {
branch, err := scm.CurrentBranchName()
if err != nil {
return err
}
if currentBranch == branch {
projectsToProcess = append(projectsToProcess, project)
}
}
}
} else {
if project, err := currentProject(jirix); err != nil {
return err
} else {
projectsToProcess = append(projectsToProcess, project)
}
}
if len(projectsToProcess) == 0 {
return fmt.Errorf("Did not find any project to push for branch %q", currentBranch)
}
type GerritPushOption struct {
Project project.Project
CLOpts gerrit.CLOpts
relativePath string
}
cwd, err := os.Getwd()
if err != nil {
return err
}
var gerritPushOptions []GerritPushOption
for _, project := range projectsToProcess {
scm := gitutil.New(jirix, gitutil.RootDirOpt(project.Path))
relativePath, err := filepath.Rel(cwd, project.Path)
if err != nil {
// Just use the full path if an error occurred.
relativePath = project.Path
}
if uploadRebaseFlag {
if changes, err := git.NewGit(project.Path).HasUncommittedChanges(); err != nil {
return err
} else if changes {
return fmt.Errorf("Project %s(%s) has uncommited changes, please commit them or stash them. Cannot rebase before pushing.", project.Name, relativePath)
}
}
remoteBranch, err := scm.RemoteBranchName()
if err != nil {
return err
}
if remoteBranch == "" {
return fmt.Errorf("For project %s(%s), branch %q is un-tracked or tracks a local un-tracked branch.", project.Name, relativePath, currentBranch)
}
host := uploadHostFlag
if host == "" {
if project.GerritHost == "" {
return fmt.Errorf("No gerrit host found. Please use the '--host' flag, or add a 'gerrithost' attribute for project %s(%s).", project.Name, relativePath)
}
host = project.GerritHost
}
hostUrl, err := url.Parse(host)
if err != nil {
return fmt.Errorf("invalid Gerrit host for project %s(%s) %q: %s", project.Name, relativePath, host, err)
}
projectRemoteUrl, err := url.Parse(project.Remote)
if err != nil {
return fmt.Errorf("invalid project remote for project %s(%s): %s", project.Name, relativePath, project.Remote, err)
}
gerritRemote := *hostUrl
gerritRemote.Path = projectRemoteUrl.Path
opts := gerrit.CLOpts{
Ccs: parseEmails(uploadCcsFlag),
Host: hostUrl,
Presubmit: gerrit.PresubmitTestType(uploadPresubmitFlag),
RemoteBranch: remoteBranch,
Remote: gerritRemote.String(),
Reviewers: parseEmails(uploadReviewersFlag),
Verify: uploadVerifyFlag,
Topic: topic,
Branch: currentBranch,
}
if opts.Presubmit == gerrit.PresubmitTestType("") {
opts.Presubmit = gerrit.PresubmitTestTypeAll
}
gerritPushOptions = append(gerritPushOptions, GerritPushOption{project, opts, relativePath})
}
// Rebase all projects before pushing
if uploadRebaseFlag {
for _, gerritPushOption := range gerritPushOptions {
scm := gitutil.New(jirix, gitutil.RootDirOpt(gerritPushOption.Project.Path))
if err := scm.Fetch("origin"); err != nil {
return err
}
trackingBranch, err := scm.TrackingBranchName()
if err != nil {
return err
}
if err = scm.Rebase(trackingBranch); err != nil {
if err2 := scm.RebaseAbort(); err2 != nil {
return err2
}
return fmt.Errorf("For project %s(%s), not able to rebase the branch to %s, please rebase manually: %s", gerritPushOption.Project.Name, gerritPushOption.relativePath, trackingBranch, err)
}
}
}
for _, gerritPushOption := range gerritPushOptions {
fmt.Printf("Pushing project %s(%s)\n", gerritPushOption.Project.Name, gerritPushOption.relativePath)
if err := gerrit.Push(jirix.NewSeq().Dir(gerritPushOption.Project.Path), gerritPushOption.CLOpts); err != nil {
if strings.Contains(err.Error(), "(no new changes)") {
if gitErr, ok := err.(gerrit.PushError); ok {
fmt.Printf("%s", gitErr.Output)
fmt.Printf("%s", gitErr.ErrorOutput)
} else {
return gerritError(err.Error())
}
} else {
return gerritError(err.Error())
}
}
fmt.Println()
}
return nil
}