go.fuchsia.dev/jiri@v0.0.0-20240502161911-b66513b29486/cmd/jiri/upload.go (about)

     1  // Copyright 2016 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"go.fuchsia.dev/jiri"
    14  	"go.fuchsia.dev/jiri/cmdline"
    15  	"go.fuchsia.dev/jiri/gerrit"
    16  	"go.fuchsia.dev/jiri/gitutil"
    17  	"go.fuchsia.dev/jiri/project"
    18  )
    19  
    20  var (
    21  	uploadCcsFlag          string
    22  	uploadPresubmitFlag    string
    23  	uploadReviewersFlag    string
    24  	uploadTopicFlag        string
    25  	uploadVerifyFlag       bool
    26  	uploadRebaseFlag       bool
    27  	uploadSetTopicFlag     bool
    28  	uploadMultipartFlag    bool
    29  	uploadBranchFlag       string
    30  	uploadRemoteBranchFlag string
    31  	uploadLabelsFlag       string
    32  	uploadGitOptions       string
    33  )
    34  
    35  type uploadError string
    36  
    37  func (e uploadError) Error() string {
    38  	result := "sending code review failed\n\n"
    39  	result += string(e)
    40  	return result
    41  }
    42  
    43  var cmdUpload = &cmdline.Command{
    44  	Runner:   jiri.RunnerFunc(runUpload),
    45  	Name:     "upload",
    46  	Short:    "Upload a changelist for review",
    47  	Long:     `Command "upload" uploads commits of a local branch to Gerrit.`,
    48  	ArgsName: "<ref>",
    49  	ArgsLong: `
    50  <ref> is the valid git ref to upload. It is optional and HEAD is used by
    51  default. This cannot be used with -multipart flag.
    52  `,
    53  }
    54  
    55  func init() {
    56  	cmdUpload.Flags.StringVar(&uploadCcsFlag, "cc", "", `Comma-separated list of emails or LDAPs to cc.`)
    57  	cmdUpload.Flags.StringVar(&uploadPresubmitFlag, "presubmit", string(gerrit.PresubmitTestTypeAll),
    58  		fmt.Sprintf("The type of presubmit tests to run. Valid values: %s.", strings.Join(gerrit.PresubmitTestTypes(), ",")))
    59  	cmdUpload.Flags.StringVar(&uploadReviewersFlag, "r", "", `Comma-separated list of emails or LDAPs to request review.`)
    60  	cmdUpload.Flags.StringVar(&uploadLabelsFlag, "l", "", `Comma-separated list of review labels.`)
    61  	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.`)
    62  	cmdUpload.Flags.BoolVar(&uploadSetTopicFlag, "set-topic", false, `Set topic. This flag would be ignored if -topic passed.`)
    63  	cmdUpload.Flags.BoolVar(&uploadVerifyFlag, "verify", true, `Run pre-push git hooks.`)
    64  	cmdUpload.Flags.BoolVar(&uploadRebaseFlag, "rebase", false, `Run rebase before pushing.`)
    65  	cmdUpload.Flags.BoolVar(&uploadMultipartFlag, "multipart", false, `Send multipart CL.  Use -set-topic or -topic flag if you want to set a topic.`)
    66  	cmdUpload.Flags.StringVar(&uploadBranchFlag, "branch", "", `Used when multipart flag is true and this command is executed from root folder`)
    67  	cmdUpload.Flags.StringVar(&uploadRemoteBranchFlag, "remoteBranch", "", `Remote branch to upload change to. If this is not specified and branch is untracked,
    68  change would be uploaded to branch in project manifest`)
    69  	cmdUpload.Flags.StringVar(&uploadGitOptions, "git-options", "", `Passthrough git options`)
    70  }
    71  
    72  // runUpload is a wrapper that pushes the changes to gerrit for review.
    73  func runUpload(jirix *jiri.X, args []string) error {
    74  	refToUpload := "HEAD"
    75  	if len(args) == 1 {
    76  		refToUpload = args[0]
    77  	} else if len(args) > 1 {
    78  		return jirix.UsageErrorf("wrong number of arguments")
    79  	}
    80  	if uploadMultipartFlag && refToUpload != "HEAD" {
    81  		return jirix.UsageErrorf("can only use HEAD as <ref> when using -multipart flag.")
    82  	}
    83  	dir, err := os.Getwd()
    84  	if err != nil {
    85  		return fmt.Errorf("os.Getwd() failed: %s", err)
    86  	}
    87  	var p *project.Project
    88  	// Walk up the path until we find a project at that path, or hit the jirix.Root parent.
    89  	// Note that we can't just compare path prefixes because of soft links.
    90  	for dir != filepath.Dir(jirix.Root) && dir != string(filepath.Separator) {
    91  		if isLocal, err := project.IsLocalProject(jirix, dir); err != nil {
    92  			return fmt.Errorf("Error while checking for local project at path %q: %s", dir, err)
    93  		} else if !isLocal {
    94  			dir = filepath.Dir(dir)
    95  			continue
    96  		}
    97  		project, err := project.ProjectAtPath(jirix, dir)
    98  		if err != nil {
    99  			return fmt.Errorf("Error while getting project at path %q: %s", dir, err)
   100  		}
   101  		p = &project
   102  		break
   103  	}
   104  
   105  	setTopic := uploadSetTopicFlag
   106  
   107  	// Always set topic when either topic is passed.
   108  	if uploadTopicFlag != "" {
   109  		setTopic = true
   110  	}
   111  
   112  	currentBranch := ""
   113  	if p == nil {
   114  		if !uploadMultipartFlag {
   115  			return fmt.Errorf("directory %q is not contained in a project", dir)
   116  		} else if uploadBranchFlag == "" {
   117  			return fmt.Errorf("Please run with -branch flag")
   118  		} else {
   119  			currentBranch = uploadBranchFlag
   120  		}
   121  	} else {
   122  		scm := gitutil.New(jirix, gitutil.RootDirOpt(p.Path))
   123  		if !scm.IsOnBranch() {
   124  			if uploadMultipartFlag {
   125  				return fmt.Errorf("Current project is not on any branch. Multipart uploads require project to be on a branch.")
   126  			}
   127  			if uploadTopicFlag == "" && setTopic {
   128  				return fmt.Errorf("Current project is not on any branch. Either provide a topic or set flag \"-set-topic\" to false.")
   129  			}
   130  		} else {
   131  			currentBranch, err = scm.CurrentBranchName()
   132  			if err != nil {
   133  				return err
   134  			}
   135  		}
   136  	}
   137  	var projectsToProcess []project.Project
   138  	topic := ""
   139  	if setTopic {
   140  		if topic = uploadTopicFlag; topic == "" {
   141  			topic = fmt.Sprintf("%s-%s", os.Getenv("USER"), currentBranch) // use <username>-<branchname> as the default
   142  		}
   143  	}
   144  	localProjects, err := project.LocalProjects(jirix, project.FastScan)
   145  	if err != nil {
   146  		return err
   147  	}
   148  	if uploadMultipartFlag {
   149  		for _, project := range localProjects {
   150  			scm := gitutil.New(jirix, gitutil.RootDirOpt(project.Path))
   151  			if scm.IsOnBranch() {
   152  				branch, err := scm.CurrentBranchName()
   153  				if err != nil {
   154  					return err
   155  				}
   156  				if currentBranch == branch {
   157  					projectsToProcess = append(projectsToProcess, project)
   158  				}
   159  			}
   160  		}
   161  
   162  	} else {
   163  		projectsToProcess = append(projectsToProcess, *p)
   164  	}
   165  	if len(projectsToProcess) == 0 {
   166  		return fmt.Errorf("Did not find any project to push for branch %q", currentBranch)
   167  	}
   168  	type GerritPushOption struct {
   169  		Project      project.Project
   170  		CLOpts       gerrit.CLOpts
   171  		relativePath string
   172  	}
   173  	cwd, err := os.Getwd()
   174  	if err != nil {
   175  		return err
   176  	}
   177  	var gerritPushOptions []GerritPushOption
   178  	remoteProjects, _, _, err := project.LoadManifestFile(jirix, jirix.JiriManifestFile(), localProjects, false /*localManifest*/)
   179  	if err != nil {
   180  		return err
   181  	}
   182  	for _, project := range projectsToProcess {
   183  		scm := gitutil.New(jirix, gitutil.RootDirOpt(project.Path))
   184  		relativePath, err := filepath.Rel(cwd, project.Path)
   185  		if err != nil {
   186  			// Just use the full path if an error occurred.
   187  			relativePath = project.Path
   188  		}
   189  		if uploadRebaseFlag {
   190  			if changes, err := gitutil.New(jirix, gitutil.RootDirOpt(project.Path)).HasUncommittedChanges(); err != nil {
   191  				return err
   192  			} else if changes {
   193  				return fmt.Errorf("Project %s(%s) has uncommited changes, please commit them or stash them. Cannot rebase before pushing.", project.Name, relativePath)
   194  			}
   195  		}
   196  		remoteBranch := uploadRemoteBranchFlag
   197  		if remoteBranch == "" && currentBranch != "" {
   198  			remoteBranch, err = scm.RemoteBranchName()
   199  			if err != nil {
   200  				return err
   201  			}
   202  		}
   203  		if remoteBranch == "" { // Un-tracked branch
   204  			remoteBranch = "main"
   205  			if r, ok := remoteProjects[project.Key()]; ok {
   206  				remoteBranch = r.RemoteBranch
   207  			} else {
   208  				jirix.Logger.Warningf("Project %s(%s) not found in manifest, will upload change to %q", project.Name, relativePath, remoteBranch)
   209  			}
   210  		}
   211  
   212  		opts := gerrit.CLOpts{
   213  			Ccs:          parseEmails(uploadCcsFlag),
   214  			GitOptions:   uploadGitOptions,
   215  			Presubmit:    gerrit.PresubmitTestType(uploadPresubmitFlag),
   216  			RemoteBranch: remoteBranch,
   217  			Remote:       "origin",
   218  			Reviewers:    parseEmails(uploadReviewersFlag),
   219  			Labels:       parseLabels(uploadLabelsFlag),
   220  			Verify:       uploadVerifyFlag,
   221  			Topic:        topic,
   222  			RefToUpload:  refToUpload,
   223  		}
   224  
   225  		if opts.Presubmit == gerrit.PresubmitTestType("") {
   226  			opts.Presubmit = gerrit.PresubmitTestTypeAll
   227  		}
   228  		gerritPushOptions = append(gerritPushOptions, GerritPushOption{project, opts, relativePath})
   229  	}
   230  
   231  	// Rebase all projects before pushing
   232  	if uploadRebaseFlag {
   233  		for _, gerritPushOption := range gerritPushOptions {
   234  			scm := gitutil.New(jirix, gitutil.RootDirOpt(gerritPushOption.Project.Path))
   235  			if err := scm.Fetch("origin", jirix.EnableSubmodules); err != nil {
   236  				return err
   237  			}
   238  			remoteBranch := "remotes/origin/" + gerritPushOption.CLOpts.RemoteBranch
   239  			if err = scm.Rebase(remoteBranch); err != nil {
   240  				if err2 := scm.RebaseAbort(); err2 != nil {
   241  					return err2
   242  				}
   243  				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)
   244  			}
   245  		}
   246  	}
   247  
   248  	for _, gerritPushOption := range gerritPushOptions {
   249  		fmt.Printf("Pushing project %s(%s)\n", gerritPushOption.Project.Name, gerritPushOption.relativePath)
   250  		if err := gerrit.Push(jirix, gerritPushOption.Project.Path, gerritPushOption.CLOpts); err != nil {
   251  			if strings.Contains(err.Error(), "(no new changes)") {
   252  				if gitErr, ok := err.(gerrit.PushError); ok {
   253  					fmt.Printf("%s", gitErr.Output)
   254  					fmt.Printf("%s", gitErr.ErrorOutput)
   255  				} else {
   256  					return uploadError(err.Error())
   257  				}
   258  			} else {
   259  				return uploadError(err.Error())
   260  			}
   261  		}
   262  		fmt.Println()
   263  	}
   264  	return nil
   265  }
   266  
   267  // parseEmails input a list of comma separated tokens and outputs a
   268  // list of email addresses. The tokens can either be email addresses
   269  // or Google LDAPs in which case the suffix @google.com is appended to
   270  // them to turn them into email addresses.
   271  func parseEmails(value string) []string {
   272  	var emails []string
   273  	tokens := strings.Split(value, ",")
   274  	for _, token := range tokens {
   275  		if token == "" {
   276  			continue
   277  		}
   278  		if !strings.Contains(token, "@") {
   279  			token += "@google.com"
   280  		}
   281  		emails = append(emails, token)
   282  	}
   283  	return emails
   284  }
   285  
   286  // parseLabels input a list of comma separated tokens and outputs a
   287  // list of tokens without whitespaces
   288  func parseLabels(value string) []string {
   289  	var ret []string
   290  	tokens := strings.Split(value, ",")
   291  	for _, token := range tokens {
   292  		token = strings.TrimSpace(token)
   293  		if token == "" {
   294  			continue
   295  		}
   296  		ret = append(ret, token)
   297  	}
   298  	return ret
   299  }