go.fuchsia.dev/jiri@v0.0.0-20240502161911-b66513b29486/cmd/jiri/patch.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 "net/url" 10 "os" 11 "strconv" 12 "strings" 13 "sync/atomic" 14 15 "go.fuchsia.dev/jiri" 16 "go.fuchsia.dev/jiri/cmdline" 17 "go.fuchsia.dev/jiri/gerrit" 18 "go.fuchsia.dev/jiri/gitutil" 19 "go.fuchsia.dev/jiri/project" 20 ) 21 22 var ( 23 patchRebaseFlag bool 24 patchRebaseRevision string 25 patchRebaseBranch string 26 patchTopicFlag bool 27 patchBranchFlag string 28 patchDeleteFlag bool 29 patchHostFlag string 30 patchForceFlag bool 31 cherryPickFlag bool 32 detachedHeadFlag bool 33 patchProjectFlag string 34 rebaseFailures uint32 35 ) 36 37 func init() { 38 cmdPatch.Flags.StringVar(&patchBranchFlag, "branch", "", "Name of the branch the patch will be applied to") 39 cmdPatch.Flags.BoolVar(&patchDeleteFlag, "delete", false, "Delete the existing branch if already exists") 40 cmdPatch.Flags.BoolVar(&patchForceFlag, "force", false, "Use force when deleting the existing branch") 41 cmdPatch.Flags.BoolVar(&patchRebaseFlag, "rebase", false, "Rebase the change after downloading") 42 cmdPatch.Flags.StringVar(&patchRebaseRevision, "rebase-revision", "", "Rebase the change to a specific revision after downloading") 43 cmdPatch.Flags.StringVar(&patchRebaseBranch, "rebase-branch", "", "The branch to rebase the change onto") 44 cmdPatch.Flags.StringVar(&patchHostFlag, "host", "", `Gerrit host to use. Defaults to gerrit host specified in manifest.`) 45 cmdPatch.Flags.StringVar(&patchProjectFlag, "project", "", `Project to apply patch to. This cannot be passed with topic flag.`) 46 cmdPatch.Flags.BoolVar(&patchTopicFlag, "topic", false, `Patch whole topic.`) 47 cmdPatch.Flags.BoolVar(&cherryPickFlag, "cherry-pick", false, `Cherry-pick patches instead of checking out.`) 48 cmdPatch.Flags.BoolVar(&detachedHeadFlag, "no-branch", false, `Don't create the branch for the patch.`) 49 } 50 51 // Use special address codes for errors that are addressable by the user. The 52 // recipes will use this to detect when the failure should be considered an 53 // infrastructure failure vs a failure that is addressable by the user. 54 const noSuchProjectErr = cmdline.ErrExitCode(23) 55 const rebaseFailedErr = cmdline.ErrExitCode(24) 56 57 // cmdPatch represents the "jiri patch" command. 58 var cmdPatch = &cmdline.Command{ 59 Runner: jiri.RunnerFunc(runPatch), 60 Name: "patch", 61 Short: "Patch in the existing change", 62 Long: ` 63 Command "patch" applies the existing changelist to the current project. The 64 change can be identified either using change ID, in which case the latest 65 patchset will be used, or the the full reference. By default patch will be 66 checked-out on a new branch. 67 68 A new branch will be created to apply the patch to. The default name of this 69 branch is "change/<changeset>/<patchset>", but this can be overridden using 70 the -branch flag. The command will fail if the branch already exists. The 71 -delete flag will delete the branch if already exists. Use the -force flag to 72 force deleting the branch even if it contains unmerged changes). 73 74 if -topic flag is true jiri will fetch whole topic and will try to apply to 75 individual projects. Patch will assume topic is of form {USER}-{BRANCH} and 76 will try to create branch name out of it. If this fails default branch name 77 will be same as topic. Currently patch does not support the scenario when 78 change "B" is created on top of "A" and both have same topic. 79 `, 80 ArgsName: "<change or topic>", 81 ArgsLong: "<change or topic> is a change ID, full reference or topic when -topic is true.", 82 } 83 84 // patchProject checks out the given change. 85 func patchProject(jirix *jiri.X, local project.Project, ref, branch, remote string) (bool, error) { 86 scm := gitutil.New(jirix, gitutil.RootDirOpt(local.Path)) 87 if !detachedHeadFlag { 88 if branch == "" { 89 cl, ps, err := gerrit.ParseRefString(ref) 90 if err != nil { 91 return false, err 92 } 93 branch = fmt.Sprintf("change/%v/%v", cl, ps) 94 } 95 jirix.Logger.Infof("Patching project %s(%s) on branch %q to ref %q\n", local.Name, local.Path, branch, ref) 96 branchExists, err := scm.BranchExists(branch) 97 if err != nil { 98 return false, err 99 } 100 if branchExists { 101 if patchDeleteFlag { 102 _, currentBranch, err := scm.GetBranches() 103 if err != nil { 104 return false, err 105 } 106 if currentBranch == branch { 107 if err := scm.CheckoutBranch("remotes/origin/"+remote, (local.GitSubmodules && jirix.EnableSubmodules), false, gitutil.DetachOpt(true)); err != nil { 108 return false, err 109 } 110 } 111 if err := scm.DeleteBranch(branch, gitutil.ForceOpt(patchForceFlag)); err != nil { 112 jirix.Logger.Errorf("Cannot delete branch %q: %s", branch, err) 113 jirix.IncrementFailures() 114 return false, nil 115 } 116 } else { 117 jirix.Logger.Errorf("Branch %q already exists in project %q", branch, local.Name) 118 jirix.IncrementFailures() 119 return false, nil 120 } 121 } 122 } else { 123 jirix.Logger.Infof("Patching project %s(%s) to ref %q\n", local.Name, local.Path, ref) 124 } 125 if err := scm.FetchRefspec("origin", ref, jirix.EnableSubmodules); err != nil { 126 return false, err 127 } 128 branchBase := "FETCH_HEAD" 129 lastRef := "" 130 if cherryPickFlag { 131 if state, err := project.GetProjectState(jirix, local, false); err != nil { 132 return false, err 133 } else { 134 lastRef = state.CurrentBranch.Name 135 if lastRef == "" { 136 lastRef = state.CurrentBranch.Revision 137 } 138 } 139 branchBase = "HEAD" 140 } 141 if !detachedHeadFlag { 142 if err := scm.CreateBranchFromRef(branch, branchBase); err != nil { 143 return false, err 144 } 145 if err := scm.SetUpstream(branch, "origin/"+remote); err != nil { 146 return false, fmt.Errorf("setting upstream to 'origin/%s': %s", remote, err) 147 } 148 branchBase = branch 149 } 150 151 // Perform rebases prior to checking out the new branch to avoid unnecesary 152 // file writes. 153 if patchRebaseFlag { 154 if patchRebaseRevision != "" { 155 if err := rebaseProjectWRevision(jirix, local, branchBase, patchRebaseRevision); err != nil { 156 return false, err 157 } 158 } else { 159 if err := rebaseProject(jirix, local, branchBase, remote); err != nil { 160 return false, err 161 } 162 } 163 164 // The cherry pick stanza below relies on the ref being present at 165 // FETCH_HEAD. This will not be true after a rebase, as the rebase 166 // functions perform fetches of their own. 167 if cherryPickFlag { 168 if err := scm.FetchRefspec("origin", ref, jirix.EnableSubmodules); err != nil { 169 return false, err 170 } 171 } 172 } 173 174 if err := scm.CheckoutBranch(branchBase, (local.GitSubmodules && jirix.EnableSubmodules), false); err != nil { 175 return false, err 176 } 177 if cherryPickFlag { 178 if err := scm.CherryPick("FETCH_HEAD"); err != nil { 179 jirix.Logger.Errorf("Error: %s\n", err) 180 jirix.IncrementFailures() 181 182 jirix.Logger.Infof("Aborting and checking out last ref: %s\n", lastRef) 183 184 // abort cherry-pick 185 if err := scm.CherryPickAbort(); err != nil { 186 jirix.Logger.Errorf("Cherry-pick abort failed. Error:%s\nPlease do it manually:'%s'\n\n", err, 187 jirix.Color.Yellow("git -C %q cherry-pick --abort && git -C %q checkout %s", local.Path, local.Path, lastRef)) 188 return false, nil 189 } 190 191 // checkout last ref 192 if err := scm.CheckoutBranch(lastRef, (local.GitSubmodules && jirix.EnableSubmodules), false); err != nil { 193 jirix.Logger.Errorf("Not able to checkout last ref. Error:%s\nPlease do it manually:'%s'\n\n", err, 194 jirix.Color.Yellow("git -C %q checkout %s", local.Path, lastRef)) 195 return false, nil 196 } 197 198 scm.DeleteBranch(branch, gitutil.ForceOpt(true)) 199 200 return false, nil 201 } 202 } 203 jirix.Logger.Infof("Project patched\n") 204 return true, nil 205 } 206 207 // rebaseProject rebases one branch of a project on top of a remote branch. 208 func rebaseProject(jirix *jiri.X, project project.Project, branch, remoteBranch string) error { 209 jirix.Logger.Infof("Rebasing branch %s in project %s(%s)\n", branch, project.Name, project.Path) 210 scm := gitutil.New(jirix, gitutil.RootDirOpt(project.Path)) 211 name, email, err := scm.UserInfoForCommit("HEAD") 212 if err != nil { 213 return fmt.Errorf("Rebase: cannot get user info for HEAD: %s", err) 214 } 215 // TODO: provide a way to set username and email 216 scm = gitutil.New(jirix, gitutil.UserNameOpt(name), gitutil.UserEmailOpt(email), gitutil.RootDirOpt(project.Path)) 217 if err := scm.FetchRefspec("origin", remoteBranch, jirix.EnableSubmodules); err != nil { 218 jirix.Logger.Errorf("Not able to fetch branch %q: %s", remoteBranch, err) 219 jirix.IncrementFailures() 220 return nil 221 } 222 if err := scm.RebaseBranch(branch, "remotes/origin/"+remoteBranch, gitutil.RebaseMerges(true)); err != nil { 223 if err2 := scm.RebaseAbort(); err2 != nil { 224 return err2 225 } 226 jirix.Logger.Errorf("Cannot rebase the change: %s", err) 227 jirix.IncrementFailures() 228 atomic.AddUint32(&rebaseFailures, 1) 229 return nil 230 } 231 jirix.Logger.Infof("Project rebased\n") 232 return nil 233 } 234 235 // rebaseProjectWRevision rebases one branch of a project on top of a revision. 236 func rebaseProjectWRevision(jirix *jiri.X, project project.Project, branch, revision string) error { 237 jirix.Logger.Infof("Rebasing branch %s in project %s(%s)\n", branch, project.Name, project.Path) 238 scm := gitutil.New(jirix, gitutil.RootDirOpt(project.Path)) 239 name, email, err := scm.UserInfoForCommit("HEAD") 240 if err != nil { 241 return fmt.Errorf("Rebase: cannot get user info for HEAD: %s", err) 242 } 243 scm = gitutil.New(jirix, gitutil.UserNameOpt(name), gitutil.UserEmailOpt(email), gitutil.RootDirOpt(project.Path)) 244 if err := scm.Fetch("origin", jirix.EnableSubmodules, gitutil.PruneOpt(true)); err != nil { 245 jirix.Logger.Errorf("Not able to fetch origin: %v", err) 246 jirix.IncrementFailures() 247 return nil 248 } 249 if err := scm.FetchRefspec("origin", revision, jirix.EnableSubmodules); err != nil { 250 jirix.Logger.Errorf("Not able to fetch revision %q: %s", revision, err) 251 jirix.IncrementFailures() 252 return nil 253 } 254 if err := scm.RebaseBranch(branch, revision, gitutil.RebaseMerges(true)); err != nil { 255 if err2 := scm.RebaseAbort(); err2 != nil { 256 return err2 257 } 258 jirix.Logger.Errorf("Cannot rebase the change: %s", err) 259 jirix.IncrementFailures() 260 atomic.AddUint32(&rebaseFailures, 1) 261 return nil 262 } 263 jirix.Logger.Infof("Project rebased\n") 264 return nil 265 } 266 267 func findProject(jirix *jiri.X, projectName string, projects project.Projects, host string, hostUrl *url.URL, ref string) *project.Project { 268 var projectToPatch *project.Project 269 var projectToPatchNoGerritHost *project.Project 270 for _, p := range projects { 271 if p.Name == projectName { 272 if host != "" && p.GerritHost != host { 273 if p.GerritHost == "" { 274 cp := p 275 projectToPatchNoGerritHost = &cp 276 //skip for now 277 continue 278 } else { 279 u, err := url.Parse(p.GerritHost) 280 if err != nil { 281 jirix.Logger.Warningf("invalid Gerrit host %q for project %s: %s", p.GerritHost, p.Name, err) 282 } 283 if u.Host != hostUrl.Host { 284 jirix.Logger.Debugf("skipping project %s(%s) for CL %s\n\n", p.Name, p.Path, ref) 285 continue 286 } 287 } 288 } 289 projectToPatch = &p 290 break 291 } 292 } 293 if projectToPatch == nil && projectToPatchNoGerritHost != nil { 294 // Try to patch the project with no gerrit host 295 projectToPatch = projectToPatchNoGerritHost 296 } 297 return projectToPatch 298 } 299 300 func runPatch(jirix *jiri.X, args []string) error { 301 if expected, got := 1, len(args); expected != got { 302 return jirix.UsageErrorf("unexpected number of arguments: expected %v, got %v", expected, got) 303 } 304 arg := args[0] 305 306 if patchProjectFlag != "" && patchTopicFlag { 307 return jirix.UsageErrorf("-topic and -project flags cannot be used together") 308 } 309 310 if patchRebaseRevision != "" && (!patchRebaseFlag || patchProjectFlag == "") { 311 return jirix.UsageErrorf("-rebase-revision should only be used with -rebase and -project flag") 312 } 313 314 var cl int 315 var ps int 316 var err error 317 changeRef := "" 318 remoteBranch := "" 319 if !patchTopicFlag { 320 cl, ps, err = gerrit.ParseRefString(arg) 321 if err != nil { 322 if patchProjectFlag != "" { 323 return fmt.Errorf("Please pass change ref with -project flag (refs/changes/<ps>/<cl>/<patch-set>)") 324 } 325 cl, err = strconv.Atoi(arg) 326 if err != nil { 327 return fmt.Errorf("invalid argument: %v", arg) 328 } 329 } else { 330 changeRef = arg 331 } 332 } 333 334 var p *project.Project 335 host := patchHostFlag 336 if patchProjectFlag != "" { 337 projects, err := project.LocalProjects(jirix, project.FastScan) 338 if err != nil { 339 return err 340 } 341 var hostUrl *url.URL 342 if host != "" { 343 hostUrl, err = url.Parse(host) 344 if err != nil { 345 return fmt.Errorf("invalid Gerrit host %q: %s", host, err) 346 } 347 } 348 p = findProject(jirix, patchProjectFlag, projects, host, hostUrl, changeRef) 349 if p == nil { 350 jirix.Logger.Errorf("Cannot find project for %q", patchProjectFlag) 351 return noSuchProjectErr 352 } 353 // TODO: TO-592 - remove this hardcode 354 if patchRebaseBranch == "" && p.RemoteBranch != "" { 355 remoteBranch = p.RemoteBranch 356 } else if patchRebaseBranch != "" { 357 remoteBranch = patchRebaseBranch 358 } else { 359 remoteBranch = "main" 360 } 361 } else if project, perr := currentProject(jirix); perr == nil { 362 p = &project 363 if host == "" { 364 if p.GerritHost == "" { 365 return fmt.Errorf("no Gerrit host; use the '--host' flag, or add a 'gerrithost' attribute for project %q", p.Name) 366 } 367 host = p.GerritHost 368 } 369 } 370 if !patchTopicFlag && p != nil { 371 if remoteBranch == "" || changeRef == "" { 372 hostUrl, err := url.Parse(host) 373 if err != nil { 374 return fmt.Errorf("invalid Gerrit host %q: %s", host, err) 375 } 376 g := gerrit.New(jirix, hostUrl) 377 378 change, err := g.GetChange(cl) 379 if err != nil { 380 return err 381 } 382 remoteBranch = change.Branch 383 changeRef = change.Reference() 384 } 385 branch := patchBranchFlag 386 if ps != -1 { 387 if _, err = patchProject(jirix, *p, arg, branch, remoteBranch); err != nil { 388 return err 389 } 390 } else { 391 if _, err = patchProject(jirix, *p, changeRef, branch, remoteBranch); err != nil { 392 return err 393 } 394 } 395 } else { 396 if host == "" { 397 return fmt.Errorf("no Gerrit host; use the '--host' flag or run this from inside a project") 398 } 399 hostUrl, err := url.Parse(host) 400 if err != nil { 401 return fmt.Errorf("invalid Gerrit host %q: %v", host, err) 402 } 403 g := gerrit.New(jirix, hostUrl) 404 405 var changes gerrit.CLList 406 branch := patchBranchFlag 407 if patchTopicFlag { 408 temp, err := g.ListOpenChangesByTopic(arg) 409 if err != nil { 410 return err 411 } 412 if len(temp) == 0 { 413 return fmt.Errorf("No changes found with topic %q", arg) 414 } 415 416 projectMap := make(map[string]map[string]gerrit.Change) 417 //Handle stacked changes 418 for _, change := range temp { 419 v, ok := projectMap[change.Project] 420 if !ok { 421 v = make(map[string]gerrit.Change) 422 projectMap[change.Project] = v 423 } 424 v[change.Change_id] = change 425 } 426 427 for p, topicChanges := range projectMap { 428 // only CL in the project 429 if len(topicChanges) == 1 { 430 for _, change := range topicChanges { 431 changes = append(changes, change) 432 break 433 } 434 continue 435 } 436 437 // stacked CLs, get the top one 438 if cherryPickFlag { 439 return fmt.Errorf("Multiple CLs for projects %q. We do not support this with cherry-pick flag", p) 440 } 441 var relatedChanges *gerrit.RelatedChanges 442 relatedChangesMap := make(map[string]struct{}) 443 444 // get related changes and build map. 445 // loop will only run once as we just need one change to build the map. 446 for _, change := range topicChanges { 447 relatedChanges, err = g.GetRelatedChanges(change.Number, change.Current_revision) 448 if err != nil { 449 return err 450 } 451 changeAdded := false 452 // get the top one and also build a map 453 for _, relatedChange := range relatedChanges.Changes { 454 if !changeAdded { 455 if c, ok := topicChanges[relatedChange.Change_id]; ok { 456 changes = append(changes, c) 457 changeAdded = true 458 } 459 } 460 relatedChangesMap[relatedChange.Change_id] = struct{}{} 461 } 462 break 463 } 464 // check if all the CLs contained in topic are in related CL list 465 for changeId, change := range topicChanges { 466 if _, ok := relatedChangesMap[changeId]; !ok { 467 var cn []string 468 for _, c := range topicChanges { 469 cn = append(cn, strconv.Itoa(c.Number)) 470 } 471 return fmt.Errorf("Not all of the changes (%s) for project %q and topic %q are related to each other", strings.Join(cn, ","), change.Project, arg) 472 } 473 } 474 } 475 ps = -1 476 if branch == "" { 477 userPrefix := os.Getenv("USER") + "-" 478 if strings.HasPrefix(arg, userPrefix) { 479 branch = strings.Replace(arg, userPrefix, "", 1) 480 } else { 481 branch = arg 482 } 483 } 484 } else { 485 change, err := g.GetChange(cl) 486 if err != nil { 487 return err 488 } 489 changes = append(changes, *change) 490 } 491 projects, err := project.LocalProjects(jirix, project.FastScan) 492 if err != nil { 493 return err 494 } 495 for _, change := range changes { 496 var ref string 497 if ps != -1 { 498 ref = arg 499 } else { 500 ref = change.Reference() 501 } 502 if projectToPatch := findProject(jirix, change.Project, projects, host, hostUrl, g.GetChangeURL(change.Number)); projectToPatch != nil { 503 if _, err := patchProject(jirix, *projectToPatch, ref, branch, change.Branch); err != nil { 504 return err 505 } 506 fmt.Println() 507 } else { 508 jirix.Logger.Errorf("Cannot find project to patch CL %s\n", g.GetChangeURL(change.Number)) 509 jirix.IncrementFailures() 510 fmt.Println() 511 } 512 } 513 } 514 // In the case where jiri is called programatically by a recipe, 515 // we want to make it clear to the recipe if all failures were rebase errors. 516 if rebaseFailures != 0 && rebaseFailures == jirix.Failures() { 517 return rebaseFailedErr 518 } else if jirix.Failures() != 0 { 519 return fmt.Errorf("Patch failed") 520 } 521 return nil 522 }