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 }