| // 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" |
| "os" |
| "path/filepath" |
| "strings" |
| |
| "fuchsia.googlesource.com/jiri" |
| "fuchsia.googlesource.com/jiri/cmdline" |
| "fuchsia.googlesource.com/jiri/gerrit" |
| "fuchsia.googlesource.com/jiri/gitutil" |
| "fuchsia.googlesource.com/jiri/project" |
| ) |
| |
| var ( |
| uploadCcsFlag string |
| uploadPresubmitFlag string |
| uploadReviewersFlag string |
| uploadTopicFlag string |
| uploadVerifyFlag bool |
| uploadRebaseFlag bool |
| uploadSetTopicFlag bool |
| uploadMultipartFlag bool |
| uploadBranchFlag string |
| uploadRemoteBranchFlag string |
| uploadLabelsFlag string |
| uploadGitOptions string |
| ) |
| |
| type uploadError string |
| |
| func (e uploadError) Error() string { |
| result := "sending code review failed\n\n" |
| result += string(e) |
| return result |
| } |
| |
| var cmdUpload = &cmdline.Command{ |
| Runner: jiri.RunnerFunc(runUpload), |
| Name: "upload", |
| Short: "Upload a changelist for review", |
| Long: `Command "upload" uploads commits of a local branch to Gerrit.`, |
| ArgsName: "<ref>", |
| ArgsLong: ` |
| <ref> is the valid git ref to upload. It is optional and HEAD is used by |
| default. This cannot be used with -multipart flag. |
| `, |
| } |
| |
| func init() { |
| cmdUpload.Flags.StringVar(&uploadCcsFlag, "cc", "", `Comma-separated list of emails or LDAPs to cc.`) |
| 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(&uploadLabelsFlag, "l", "", `Comma-separated list of review labels.`) |
| cmdUpload.Flags.StringVar(&uploadTopicFlag, "topic", "", `CL topic. Default is <username>-<branchname>. If this flag is set, upload will ignore -set-topic and will set a topic.`) |
| cmdUpload.Flags.BoolVar(&uploadSetTopicFlag, "set-topic", false, `Set topic. This flag would be ignored if -topic passed.`) |
| 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. Use -set-topic or -topic flag if you want to set a topic.`) |
| cmdUpload.Flags.StringVar(&uploadBranchFlag, "branch", "", `Used when multipart flag is true and this command is executed from root folder`) |
| cmdUpload.Flags.StringVar(&uploadRemoteBranchFlag, "remoteBranch", "", `Remote branch to upload change to. If this is not specified and branch is untracked, |
| change would be uploaded to branch in project manifest`) |
| cmdUpload.Flags.StringVar(&uploadGitOptions, "git-options", "", `Passthrough git options`) |
| } |
| |
| // runUpload is a wrapper that pushes the changes to gerrit for review. |
| func runUpload(jirix *jiri.X, args []string) error { |
| refToUpload := "HEAD" |
| if len(args) == 1 { |
| refToUpload = args[0] |
| } else if len(args) > 1 { |
| return jirix.UsageErrorf("wrong number of arguments") |
| } |
| if uploadMultipartFlag && refToUpload != "HEAD" { |
| return jirix.UsageErrorf("can only use HEAD as <ref> when using -multipart flag.") |
| } |
| 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 parent. |
| // Note that we can't just compare path prefixes because of soft links. |
| for dir != filepath.Dir(jirix.Root) && dir != string(filepath.Separator) { |
| if isLocal, err := project.IsLocalProject(jirix, dir); err != nil { |
| return fmt.Errorf("Error while checking for local project at path %q: %s", dir, err) |
| } else if !isLocal { |
| dir = filepath.Dir(dir) |
| continue |
| } |
| project, err := project.ProjectAtPath(jirix, dir) |
| if err != nil { |
| return fmt.Errorf("Error while getting project at path %q: %s", dir, err) |
| } |
| p = &project |
| break |
| } |
| |
| setTopic := uploadSetTopicFlag |
| |
| // Always set topic when either topic is passed. |
| if uploadTopicFlag != "" { |
| setTopic = true |
| } |
| |
| 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() { |
| if uploadMultipartFlag { |
| return fmt.Errorf("Current project is not on any branch. Multipart uploads require project to be on a branch.") |
| } |
| if uploadTopicFlag == "" && setTopic { |
| return fmt.Errorf("Current project is not on any branch. Either provide a topic or set flag \"-set-topic\" to false.") |
| } |
| } else { |
| currentBranch, err = scm.CurrentBranchName() |
| if err != nil { |
| return err |
| } |
| } |
| } |
| var projectsToProcess []project.Project |
| topic := "" |
| if setTopic { |
| if topic = uploadTopicFlag; topic == "" { |
| topic = fmt.Sprintf("%s-%s", os.Getenv("USER"), currentBranch) // use <username>-<branchname> as the default |
| } |
| } |
| localProjects, err := project.LocalProjects(jirix, project.FastScan) |
| if err != nil { |
| return err |
| } |
| if uploadMultipartFlag { |
| for _, project := range localProjects { |
| 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 { |
| projectsToProcess = append(projectsToProcess, *p) |
| } |
| 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 |
| remoteProjects, _, _, err := project.LoadManifestFile(jirix, jirix.JiriManifestFile(), localProjects, false /*localManifest*/) |
| if err != nil { |
| return err |
| } |
| 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 := gitutil.New(jirix, gitutil.RootDirOpt(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 := uploadRemoteBranchFlag |
| if remoteBranch == "" && currentBranch != "" { |
| remoteBranch, err = scm.RemoteBranchName() |
| if err != nil { |
| return err |
| } |
| } |
| if remoteBranch == "" { // Un-tracked branch |
| remoteBranch = "master" |
| if r, ok := remoteProjects[project.Key()]; ok { |
| remoteBranch = r.RemoteBranch |
| } else { |
| jirix.Logger.Warningf("Project %s(%s) not found in manifest, will upload change to %q", project.Name, relativePath, remoteBranch) |
| } |
| } |
| |
| opts := gerrit.CLOpts{ |
| Ccs: parseEmails(uploadCcsFlag), |
| GitOptions: uploadGitOptions, |
| Presubmit: gerrit.PresubmitTestType(uploadPresubmitFlag), |
| RemoteBranch: remoteBranch, |
| Remote: "origin", |
| Reviewers: parseEmails(uploadReviewersFlag), |
| Labels: parseLabels(uploadLabelsFlag), |
| Verify: uploadVerifyFlag, |
| Topic: topic, |
| RefToUpload: refToUpload, |
| } |
| |
| 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 |
| } |
| remoteBranch := "remotes/origin/" + gerritPushOption.CLOpts.RemoteBranch |
| if err = scm.Rebase(remoteBranch); 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, remoteBranch, err) |
| } |
| } |
| } |
| |
| for _, gerritPushOption := range gerritPushOptions { |
| fmt.Printf("Pushing project %s(%s)\n", gerritPushOption.Project.Name, gerritPushOption.relativePath) |
| if err := gerrit.Push(jirix, 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 uploadError(err.Error()) |
| } |
| } else { |
| return uploadError(err.Error()) |
| } |
| } |
| fmt.Println() |
| } |
| return nil |
| } |
| |
| // parseEmails input a list of comma separated tokens and outputs a |
| // list of email addresses. The tokens can either be email addresses |
| // or Google LDAPs in which case the suffix @google.com is appended to |
| // them to turn them into email addresses. |
| func parseEmails(value string) []string { |
| var emails []string |
| tokens := strings.Split(value, ",") |
| for _, token := range tokens { |
| if token == "" { |
| continue |
| } |
| if !strings.Contains(token, "@") { |
| token += "@google.com" |
| } |
| emails = append(emails, token) |
| } |
| return emails |
| } |
| |
| // parseLabels input a list of comma separated tokens and outputs a |
| // list of tokens without whitespaces |
| func parseLabels(value string) []string { |
| var ret []string |
| tokens := strings.Split(value, ",") |
| for _, token := range tokens { |
| token = strings.TrimSpace(token) |
| if token == "" { |
| continue |
| } |
| ret = append(ret, token) |
| } |
| return ret |
| } |