github.com/vanadium-archive/go.jiri@v0.0.0-20160715023856-abfb8b131290/cmd/jiri/cl.go (about) 1 // Copyright 2015 The Vanadium 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 "io" 10 "io/ioutil" 11 "net/url" 12 "os" 13 "path/filepath" 14 "regexp" 15 "sort" 16 "strings" 17 18 "v.io/jiri" 19 "v.io/jiri/collect" 20 "v.io/jiri/gerrit" 21 "v.io/jiri/gitutil" 22 "v.io/jiri/profiles/profilescmdline" 23 "v.io/jiri/project" 24 "v.io/jiri/runutil" 25 "v.io/x/lib/cmdline" 26 ) 27 28 const ( 29 commitMessageFileName = ".gerrit_commit_message" 30 dependencyPathFileName = ".dependency_path" 31 multiPartMetaDataFileName = "multipart_index" 32 ) 33 34 var ( 35 autosubmitFlag bool 36 ccsFlag string 37 draftFlag bool 38 editFlag bool 39 forceFlag bool 40 hostFlag string 41 messageFlag string 42 commitMessageBodyFlag string 43 presubmitFlag string 44 remoteBranchFlag string 45 reviewersFlag string 46 setTopicFlag bool 47 topicFlag string 48 uncommittedFlag bool 49 verifyFlag bool 50 currentProjectFlag bool 51 cleanupMultiPartFlag bool 52 ) 53 54 // Special labels stored in the commit message. 55 var ( 56 // Auto submit label. 57 autosubmitLabelRE *regexp.Regexp = regexp.MustCompile("AutoSubmit") 58 59 // Change-Ids start with 'I' and are followed by 40 characters of hex. 60 changeIDRE *regexp.Regexp = regexp.MustCompile("Change-Id: (I[0123456789abcdefABCDEF]{40})") 61 62 // MultiPart messages are of the form: MultiPart: <n>/<m> 63 multiPartRE *regexp.Regexp = regexp.MustCompile(`(?m)^MultiPart: \d+/\d+$`) 64 65 // Presubmit test label. 66 // PresubmitTest: <type> 67 presubmitTestLabelRE *regexp.Regexp = regexp.MustCompile(`PresubmitTest:\s*(.*)`) 68 69 noChangesRE *regexp.Regexp = regexp.MustCompile(`! \[remote rejected\] HEAD -> refs/(for|drafts)/\S+ \(no new changes\)`) 70 ) 71 72 // init carries out the package initialization. 73 func init() { 74 cmdCLMail = newCmdCLMail() 75 cmdCL = newCmdCL() 76 cmdCLCleanup.Flags.BoolVar(&forceFlag, "f", false, `Ignore unmerged changes.`) 77 cmdCLCleanup.Flags.StringVar(&remoteBranchFlag, "remote-branch", "master", `Name of the remote branch the CL pertains to, without the leading "origin/".`) 78 cmdCLMail.Flags.BoolVar(&autosubmitFlag, "autosubmit", false, `Automatically submit the changelist when feasible.`) 79 cmdCLMail.Flags.StringVar(&ccsFlag, "cc", "", `Comma-seperated list of emails or LDAPs to cc.`) 80 cmdCLMail.Flags.BoolVar(&draftFlag, "d", false, `Send a draft changelist.`) 81 cmdCLMail.Flags.BoolVar(&editFlag, "edit", true, `Open an editor to edit the CL description.`) 82 cmdCLMail.Flags.StringVar(&hostFlag, "host", "", `Gerrit host to use. Defaults to gerrit host specified in manifest.`) 83 cmdCLMail.Flags.StringVar(&messageFlag, "m", "", `CL description.`) 84 cmdCLMail.Flags.StringVar(&commitMessageBodyFlag, "commit-message-body-file", "", `file containing the body of the CL description, that is, text without a ChangeID, MultiPart etc.`) 85 cmdCLMail.Flags.StringVar(&presubmitFlag, "presubmit", string(gerrit.PresubmitTestTypeAll), 86 fmt.Sprintf("The type of presubmit tests to run. Valid values: %s.", strings.Join(gerrit.PresubmitTestTypes(), ","))) 87 cmdCLMail.Flags.StringVar(&remoteBranchFlag, "remote-branch", "master", `Name of the remote branch the CL pertains to, without the leading "origin/".`) 88 cmdCLMail.Flags.StringVar(&reviewersFlag, "r", "", `Comma-seperated list of emails or LDAPs to request review.`) 89 cmdCLMail.Flags.BoolVar(&setTopicFlag, "set-topic", true, `Set Gerrit CL topic.`) 90 cmdCLMail.Flags.StringVar(&topicFlag, "topic", "", `CL topic, defaults to <username>-<branchname>.`) 91 cmdCLMail.Flags.BoolVar(&uncommittedFlag, "check-uncommitted", true, `Check that no uncommitted changes exist.`) 92 cmdCLMail.Flags.BoolVar(&verifyFlag, "verify", true, `Run pre-push git hooks.`) 93 cmdCLMail.Flags.BoolVar(¤tProjectFlag, "current-project-only", false, `Run mail in the current project only.`) 94 cmdCLMail.Flags.BoolVar(&cleanupMultiPartFlag, "clean-multipart-metadata", false, `Cleanup the metadata associated with multipart CLs pertaining the MultiPart: x/y message without mailing any CLs.`) 95 cmdCLSync.Flags.StringVar(&remoteBranchFlag, "remote-branch", "master", `Name of the remote branch the CL pertains to, without the leading "origin/".`) 96 } 97 98 func getCommitMessageFileName(jirix *jiri.X, branch string) (string, error) { 99 topLevel, err := gitutil.New(jirix.NewSeq()).TopLevel() 100 if err != nil { 101 return "", err 102 } 103 return filepath.Join(topLevel, jiri.ProjectMetaDir, branch, commitMessageFileName), nil 104 } 105 106 func getDependencyPathFileName(jirix *jiri.X, branch string) (string, error) { 107 topLevel, err := gitutil.New(jirix.NewSeq()).TopLevel() 108 if err != nil { 109 return "", err 110 } 111 return filepath.Join(topLevel, jiri.ProjectMetaDir, branch, dependencyPathFileName), nil 112 } 113 114 func getDependentCLs(jirix *jiri.X, branch string) ([]string, error) { 115 file, err := getDependencyPathFileName(jirix, branch) 116 if err != nil { 117 return nil, err 118 } 119 data, err := jirix.NewSeq().ReadFile(file) 120 var branches []string 121 if err != nil { 122 if !runutil.IsNotExist(err) { 123 return nil, err 124 } 125 if branch != remoteBranchFlag { 126 branches = []string{remoteBranchFlag} 127 } 128 } else { 129 branches = strings.Split(strings.TrimSpace(string(data)), "\n") 130 } 131 return branches, nil 132 } 133 134 // cmdCL represents the "jiri cl" command. 135 var cmdCL *cmdline.Command 136 137 // Use a factory to avoid an initialization loop between between the 138 // Runner function and the ParsedFlags field in the Command. 139 func newCmdCL() *cmdline.Command { 140 return &cmdline.Command{ 141 Name: "cl", 142 Short: "Manage changelists for multiple projects", 143 Long: "Manage changelists for multiple projects.", 144 Children: []*cmdline.Command{cmdCLCleanup, cmdCLMail, cmdCLNew, cmdCLSync}, 145 } 146 } 147 148 // cmdCLCleanup represents the "jiri cl cleanup" command. 149 // 150 // TODO(jsimsa): Replace this with a "submit" command that talks to 151 // Gerrit to submit the CL and then (optionally) removes it locally. 152 var cmdCLCleanup = &cmdline.Command{ 153 Runner: jiri.RunnerFunc(runCLCleanup), 154 Name: "cleanup", 155 Short: "Clean up changelists that have been merged", 156 Long: ` 157 Command "cleanup" checks that the given branches have been merged into 158 the corresponding remote branch. If a branch differs from the 159 corresponding remote branch, the command reports the difference and 160 stops. Otherwise, it deletes the given branches. 161 `, 162 ArgsName: "<branches>", 163 ArgsLong: "<branches> is a list of branches to cleanup.", 164 } 165 166 func cleanupCL(jirix *jiri.X, branches []string) (e error) { 167 git := gitutil.New(jirix.NewSeq()) 168 originalBranch, err := git.CurrentBranchName() 169 if err != nil { 170 return err 171 } 172 stashed, err := git.Stash() 173 if err != nil { 174 return err 175 } 176 if stashed { 177 defer collect.Error(func() error { return git.StashPop() }, &e) 178 } 179 if err := git.CheckoutBranch(remoteBranchFlag); err != nil { 180 return err 181 } 182 checkoutOriginalBranch := true 183 defer collect.Error(func() error { 184 if checkoutOriginalBranch { 185 return git.CheckoutBranch(originalBranch) 186 } 187 return nil 188 }, &e) 189 if err := git.FetchRefspec("origin", remoteBranchFlag); err != nil { 190 return err 191 } 192 s := jirix.NewSeq() 193 for _, branch := range branches { 194 cleanupFn := func() error { return cleanupBranch(jirix, branch) } 195 if err := s.Call(cleanupFn, "Cleaning up branch: %s", branch).Done(); err != nil { 196 return err 197 } 198 if branch == originalBranch { 199 checkoutOriginalBranch = false 200 } 201 } 202 return nil 203 } 204 205 func cleanupBranch(jirix *jiri.X, branch string) error { 206 git := gitutil.New(jirix.NewSeq()) 207 if err := git.CheckoutBranch(branch); err != nil { 208 return err 209 } 210 if !forceFlag { 211 trackingBranch := "origin/" + remoteBranchFlag 212 if err := git.Merge(trackingBranch); err != nil { 213 return err 214 } 215 files, err := git.ModifiedFiles(trackingBranch, branch) 216 if err != nil { 217 return err 218 } 219 if len(files) != 0 { 220 return fmt.Errorf("unmerged changes in\n%s", strings.Join(files, "\n")) 221 } 222 } 223 if err := git.CheckoutBranch(remoteBranchFlag); err != nil { 224 return err 225 } 226 if err := git.DeleteBranch(branch, gitutil.ForceOpt(true)); err != nil { 227 return err 228 } 229 reviewBranch := branch + "-REVIEW" 230 if git.BranchExists(reviewBranch) { 231 if err := git.DeleteBranch(reviewBranch, gitutil.ForceOpt(true)); err != nil { 232 return err 233 } 234 } 235 // Delete branch metadata. 236 topLevel, err := git.TopLevel() 237 if err != nil { 238 return err 239 } 240 s := jirix.NewSeq() 241 // Remove the branch from all dependency paths. 242 metadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir) 243 fileInfos, err := s.RemoveAll(filepath.Join(metadataDir, branch)). 244 ReadDir(metadataDir) 245 if err != nil { 246 return err 247 } 248 for _, fileInfo := range fileInfos { 249 if !fileInfo.IsDir() { 250 continue 251 } 252 file, err := getDependencyPathFileName(jirix, fileInfo.Name()) 253 if err != nil { 254 return err 255 } 256 data, err := s.ReadFile(file) 257 if err != nil { 258 if !runutil.IsNotExist(err) { 259 return err 260 } 261 continue 262 } 263 branches := strings.Split(string(data), "\n") 264 for i, tmpBranch := range branches { 265 if branch == tmpBranch { 266 data := []byte(strings.Join(append(branches[:i], branches[i+1:]...), "\n")) 267 if err := s.WriteFile(file, data, os.FileMode(0644)).Done(); err != nil { 268 return err 269 } 270 break 271 } 272 } 273 } 274 return nil 275 } 276 277 func runCLCleanup(jirix *jiri.X, args []string) error { 278 if len(args) == 0 { 279 return jirix.UsageErrorf("cleanup requires at least one argument") 280 } 281 return cleanupCL(jirix, args) 282 } 283 284 // cmdCLMail represents the "jiri cl mail" command. 285 var cmdCLMail *cmdline.Command 286 287 // Use a factory to avoid an initialization loop between between the 288 // Runner function and the ParsedFlags field in the Command. 289 func newCmdCLMail() *cmdline.Command { 290 return &cmdline.Command{ 291 Runner: jiri.RunnerFunc(runCLMail), 292 Name: "mail", 293 Short: "Mail a changelist for review", 294 Long: ` 295 Command "mail" squashes all commits of a local branch into a single 296 "changelist" and mails this changelist to Gerrit as a single 297 commit. First time the command is invoked, it generates a Change-Id 298 for the changelist, which is appended to the commit 299 message. Consecutive invocations of the command use the same Change-Id 300 by default, informing Gerrit that the incomming commit is an update of 301 an existing changelist. 302 `, 303 } 304 } 305 306 type changeConflictError struct { 307 localBranch string 308 message string 309 remoteBranch string 310 } 311 312 func (e changeConflictError) Error() string { 313 result := "changelist conflicts with the remote " + e.remoteBranch + " branch\n\n" 314 result += "To resolve this problem, run 'git pull origin " + e.remoteBranch + ":" + e.localBranch + "',\n" 315 result += "resolve the conflicts identified below, and then try again.\n" 316 result += e.message 317 return result 318 } 319 320 type emptyChangeError struct{} 321 322 func (_ emptyChangeError) Error() string { 323 return "current branch has no commits" 324 } 325 326 type gerritError string 327 328 func (e gerritError) Error() string { 329 result := "sending code review failed\n\n" 330 result += string(e) 331 return result 332 } 333 334 type noChangeIDError struct{} 335 336 func (_ noChangeIDError) Error() string { 337 result := "changelist is missing a Change-ID" 338 return result 339 } 340 341 type uncommittedChangesError []string 342 343 func (e uncommittedChangesError) Error() string { 344 result := "uncommitted local changes in files:\n" 345 result += " " + strings.Join(e, "\n ") 346 return result 347 } 348 349 var defaultMessageHeader = ` 350 # Describe your changelist, specifying what package(s) your change 351 # pertains to, followed by a short summary and, in case of non-trivial 352 # changelists, provide a detailed description. 353 # 354 # For example: 355 # 356 # rpc/stream/proxy: add publish address 357 # 358 # The listen address is not always the same as the address that external 359 # users need to connect to. This CL adds a new argument to proxy.New() 360 # to specify the published address that clients should connect to. 361 362 # FYI, you are about to submit the following local commits for review: 363 # 364 ` 365 366 // currentProject returns the Project containing the current working directory. 367 // The current working directory must be inside JIRI_ROOT. 368 func currentProject(jirix *jiri.X) (project.Project, error) { 369 dir, err := os.Getwd() 370 if err != nil { 371 return project.Project{}, fmt.Errorf("os.Getwd() failed: %v", err) 372 } 373 374 // Walk up the path until we find a project at that path, or hit the jirix.Root. 375 // Note that we can't just compare path prefixes because of soft links. 376 for dir != jirix.Root && dir != string(filepath.Separator) { 377 p, err := project.ProjectAtPath(jirix, dir) 378 if err != nil { 379 dir = filepath.Dir(dir) 380 continue 381 } 382 return p, nil 383 } 384 return project.Project{}, fmt.Errorf("directory %q is not contained in a project", dir) 385 } 386 387 type multiPart struct { 388 clean, current bool 389 currentKey project.ProjectKey 390 currentBranch string 391 states map[project.ProjectKey]*project.ProjectState 392 keys project.ProjectKeys 393 } 394 395 // initForMultiPart determines the actions to be taken 396 // based on command line flags and project state. 397 func initForMultiPart(jirix *jiri.X) (*multiPart, error) { 398 mp := &multiPart{} 399 mp.clean = cleanupMultiPartFlag 400 if currentProjectFlag { 401 mp.current = true 402 return mp, nil 403 } 404 if mp.clean { 405 states, keys, err := projectStates(jirix, true) 406 if err != nil { 407 return nil, err 408 } 409 mp.states = states 410 mp.keys = keys 411 return mp, nil 412 } 413 states, keys, err := projectStates(jirix, false) 414 if err != nil { 415 return nil, err 416 } 417 if len(states) == 0 { 418 return nil, fmt.Errorf("Failed to find any projects") 419 } 420 current, err := currentProject(jirix) 421 if err != nil { 422 return nil, err 423 } 424 mp.currentKey = current.Key() 425 mp.currentBranch = states[mp.currentKey].CurrentBranch 426 if len(keys) == 1 { 427 filename := filepath.Join(states[keys[0]].Project.Path, jiri.ProjectMetaDir, mp.currentBranch, multiPartMetaDataFileName) 428 os.Remove(filename) 429 if mp.currentKey == states[keys[0]].Project.Key() { 430 mp.current = true 431 return mp, nil 432 } 433 } 434 mp.states = states 435 mp.keys = keys 436 return mp, nil 437 } 438 439 // projectStates returns a map with all projects that are on the same 440 // current branch as the current project, as well as a slice of their 441 // project keys sorted lexicographically. Unless "allowdirty" is true, 442 // an error is returned if any matching project has uncommitted changes. 443 // The keys are returned, sorted, to avoid the caller having to recreate 444 // the them by iterating over the map. 445 func projectStates(jirix *jiri.X, allowdirty bool) (map[project.ProjectKey]*project.ProjectState, project.ProjectKeys, error) { 446 git := gitutil.New(jirix.NewSeq()) 447 branch, err := git.CurrentBranchName() 448 if err != nil { 449 return nil, nil, err 450 } 451 states, err := project.GetProjectStates(jirix, false) 452 if err != nil { 453 return nil, nil, err 454 } 455 uncommitted := []string{} 456 var keys project.ProjectKeys 457 for _, s := range states { 458 if s.CurrentBranch == branch { 459 key := s.Project.Key() 460 fullState, err := project.GetProjectState(jirix, key, true) 461 if err != nil { 462 return nil, nil, err 463 } 464 if !allowdirty && fullState.HasUncommitted { 465 uncommitted = append(uncommitted, string(key)) 466 } else { 467 keys = append(keys, key) 468 } 469 } 470 } 471 if len(uncommitted) > 0 { 472 return nil, nil, fmt.Errorf("the following projects have uncommitted changes: %s", strings.Join(uncommitted, ", ")) 473 } 474 members := map[project.ProjectKey]*project.ProjectState{} 475 for _, key := range keys { 476 members[key] = states[key] 477 } 478 if len(members) == 0 { 479 return nil, nil, nil 480 } 481 sort.Sort(keys) 482 return members, keys, nil 483 } 484 485 func (mp *multiPart) writeMultiPartMetadata(jirix *jiri.X) error { 486 total := len(mp.states) 487 index := 1 488 s := jirix.NewSeq() 489 for _, key := range mp.keys { 490 state := mp.states[key] 491 dir := filepath.Join(state.Project.Path, jiri.ProjectMetaDir, mp.currentBranch) 492 filename := filepath.Join(dir, multiPartMetaDataFileName) 493 if total < 2 { 494 os.Remove(filename) 495 continue 496 } 497 msg := fmt.Sprintf("MultiPart: %d/%d\n", index, total) 498 if err := s.MkdirAll(dir, os.FileMode(0755)). 499 WriteFile(filename, []byte(msg), os.FileMode(0644)). 500 Done(); err != nil { 501 return err 502 } 503 index++ 504 } 505 return nil 506 } 507 508 func (mp *multiPart) cleanMultiPartMetadata(jirix *jiri.X) error { 509 s := jirix.NewSeq() 510 for _, state := range mp.states { 511 filename := filepath.Join(state.Project.Path, jiri.ProjectMetaDir, mp.currentBranch, multiPartMetaDataFileName) 512 ok, err := s.IsFile(filename) 513 if err != nil { 514 return err 515 } 516 if ok { 517 if err := s.Remove(filename).Done(); err != nil { 518 return err 519 } 520 } 521 } 522 return nil 523 } 524 525 func (mp *multiPart) commandline(excludeKey project.ProjectKey, flags []string) []string { 526 keyflag := "--projects=" 527 for _, k := range mp.keys { 528 if k == excludeKey { 529 continue 530 } 531 keyflag += string(k) + "," 532 } 533 keyflag = strings.TrimSuffix(keyflag, ",") 534 clargs := []string{ 535 "runp", 536 "--interactive", 537 keyflag, 538 } 539 clargs = append(clargs, "jiri", "cl", "mail", "--current-project-only=true") 540 return append(clargs, flags...) 541 } 542 543 // clMailMultiFlags extracts flags from the invocation of cl mail 544 // that should be passed on to the sub invocations of cl mail when 545 // operating across multiple repos. 546 // These are: 547 // -autosubmit, -cc, -d, -edit, -host, -m, -presubmit, remote-branch, -r, 548 // -set-topic, -topic, -check-uncommitted and -verify, 549 func clMailMultiFlags() []string { 550 flags := []string{} 551 stringFlag := func(name, value string) { 552 if profilescmdline.IsFlagSet(cmdCLMail.ParsedFlags, name) { 553 flags = append(flags, fmt.Sprintf("--%s=%s", name, value)) 554 } 555 } 556 boolFlag := func(name string, value bool) { 557 if profilescmdline.IsFlagSet(cmdCLMail.ParsedFlags, name) { 558 flags = append(flags, fmt.Sprintf("--%s=%t", name, value)) 559 } 560 } 561 562 // --edit is handled differently to other flags, if it is not 563 // specifically set, the default is to run the editor once 564 // and then reuse that message for the other parts of a multipart 565 // CL - that is, set -edit=false for the other repos. If edit 566 // is specifically set then that setting is used for all repos. 567 // So using --edit=true allows for a different CL message in 568 // each repo of a multipart CL. 569 if profilescmdline.IsFlagSet(cmdCLMail.ParsedFlags, "edit") { 570 // if --edit is set on the command line, use that value 571 // for all subcommands 572 flags = append(flags, fmt.Sprintf("--edit=%t", editFlag)) 573 } else { 574 // if --edit is not set on the command line, use --edit=false 575 // for subcommands. 576 flags = append(flags, "--edit=false") 577 } 578 579 boolFlag("autosubmit", autosubmitFlag) 580 stringFlag("cc", ccsFlag) 581 boolFlag("d", draftFlag) 582 stringFlag("host", hostFlag) 583 stringFlag("m", messageFlag) 584 stringFlag("presubmit", presubmitFlag) 585 stringFlag("remote-branch", remoteBranchFlag) 586 stringFlag("r", reviewersFlag) 587 boolFlag("set-topic", setTopicFlag) 588 boolFlag("check-uncommitted", uncommittedFlag) 589 boolFlag("verify", verifyFlag) 590 return flags 591 } 592 593 // runCLMail is a wrapper that sets up and runs a review instance across 594 // multiple projects. 595 func runCLMail(jirix *jiri.X, _ []string) error { 596 mp, err := initForMultiPart(jirix) 597 if err != nil { 598 return err 599 } 600 if mp.clean { 601 if err := mp.cleanMultiPartMetadata(jirix); err != nil { 602 return err 603 } 604 return nil 605 } 606 if mp.current { 607 return runCLMailCurrent(jirix, []string{}) 608 } 609 // multipart mode 610 if err := mp.writeMultiPartMetadata(jirix); err != nil { 611 mp.cleanMultiPartMetadata(jirix) 612 return err 613 } 614 if err := runCLMailCurrent(jirix, []string{}); err != nil { 615 return err 616 } 617 git := gitutil.New(jirix.NewSeq()) 618 branch, err := git.CurrentBranchName() 619 if err != nil { 620 return err 621 } 622 initialMessage, err := strippedGerritCommitMessage(jirix, branch) 623 if err != nil { 624 return err 625 } 626 s := jirix.NewSeq() 627 tmp, err := s.TempFile("", branch+"-") 628 if err != nil { 629 return err 630 } 631 defer func() { 632 tmp.Close() 633 os.Remove(tmp.Name()) 634 }() 635 if _, err := io.WriteString(tmp, initialMessage); err != nil { 636 return err 637 } 638 // Use Capture to make sure that all output from the subcommands is 639 // sent to stdout/stderr. 640 flags := clMailMultiFlags() 641 flags = append(flags, "--commit-message-body-file="+tmp.Name()) 642 return s.Capture(jirix.Stdout(), jirix.Stderr()).Last("jiri", mp.commandline(mp.currentKey, flags)...) 643 } 644 645 func runCLMailCurrent(jirix *jiri.X, _ []string) error { 646 // Check that working dir exist on remote branch. Otherwise checking out 647 // remote branch will break the users working dir. 648 git := gitutil.New(jirix.NewSeq()) 649 wd, err := os.Getwd() 650 if err != nil { 651 return err 652 } 653 topLevel, err := git.TopLevel() 654 if err != nil { 655 return err 656 } 657 relWd, err := filepath.Rel(topLevel, wd) 658 if err != nil { 659 return err 660 } 661 if !git.DirExistsOnBranch(relWd, remoteBranchFlag) { 662 return fmt.Errorf("directory %q does not exist on branch %q.\nPlease run 'jiri cl mail' from root directory of this repo.", relWd, remoteBranchFlag) 663 } 664 665 // Sanity checks for the <presubmitFlag> flag. 666 if !checkPresubmitFlag() { 667 return jirix.UsageErrorf("invalid value for the -presubmit flag. Valid values: %s.", 668 strings.Join(gerrit.PresubmitTestTypes(), ",")) 669 } 670 671 p, err := currentProject(jirix) 672 if err != nil { 673 return err 674 } 675 676 host := hostFlag 677 if host == "" { 678 if p.GerritHost == "" { 679 return fmt.Errorf("No gerrit host found. Please use the '--host' flag, or add a 'gerrithost' attribute for project %q.", p.Name) 680 } 681 host = p.GerritHost 682 } 683 hostUrl, err := url.Parse(host) 684 if err != nil { 685 return fmt.Errorf("invalid Gerrit host %q: %v", host, err) 686 } 687 projectRemoteUrl, err := url.Parse(p.Remote) 688 if err != nil { 689 return fmt.Errorf("invalid project remote: %v", p.Remote, err) 690 } 691 gerritRemote := *hostUrl 692 gerritRemote.Path = projectRemoteUrl.Path 693 694 // Create and run the review. 695 review, err := newReview(jirix, p, gerrit.CLOpts{ 696 Autosubmit: autosubmitFlag, 697 Ccs: parseEmails(ccsFlag), 698 Draft: draftFlag, 699 Edit: editFlag, 700 Remote: gerritRemote.String(), 701 Host: hostUrl, 702 Presubmit: gerrit.PresubmitTestType(presubmitFlag), 703 RemoteBranch: remoteBranchFlag, 704 Reviewers: parseEmails(reviewersFlag), 705 Verify: verifyFlag, 706 }) 707 if err != nil { 708 return err 709 } 710 if confirmed, err := review.confirmFlagChanges(); err != nil { 711 return err 712 } else if !confirmed { 713 return nil 714 } 715 err = review.run() 716 // Ignore the error that is returned when there are no differences 717 // between the local and gerrit branches. 718 if err != nil && noChangesRE.MatchString(err.Error()) { 719 return nil 720 } 721 return err 722 } 723 724 // parseEmails input a list of comma separated tokens and outputs a 725 // list of email addresses. The tokens can either be email addresses 726 // or Google LDAPs in which case the suffix @google.com is appended to 727 // them to turn them into email addresses. 728 func parseEmails(value string) []string { 729 var emails []string 730 tokens := strings.Split(value, ",") 731 for _, token := range tokens { 732 if token == "" { 733 continue 734 } 735 if !strings.Contains(token, "@") { 736 token += "@google.com" 737 } 738 emails = append(emails, token) 739 } 740 return emails 741 } 742 743 // checkDependents makes sure that all CLs in the sequence of 744 // dependent CLs leading to (but not including) the current branch 745 // have been exported to Gerrit. 746 func checkDependents(jirix *jiri.X) (e error) { 747 originalBranch, err := gitutil.New(jirix.NewSeq()).CurrentBranchName() 748 if err != nil { 749 return err 750 } 751 branches, err := getDependentCLs(jirix, originalBranch) 752 if err != nil { 753 return err 754 } 755 for i := 1; i < len(branches); i++ { 756 file, err := getCommitMessageFileName(jirix, branches[i]) 757 if err != nil { 758 return err 759 } 760 if _, err := jirix.NewSeq().Stat(file); err != nil { 761 if !runutil.IsNotExist(err) { 762 return err 763 } 764 return fmt.Errorf(`Failed to export the branch %q to Gerrit because its ancestor %q has not been exported to Gerrit yet. 765 The following steps are needed before the operation can be retried: 766 $ git checkout %v 767 $ jiri cl mail 768 $ git checkout %v 769 # retry the original command 770 `, originalBranch, branches[i], branches[i], originalBranch) 771 } 772 } 773 774 return nil 775 } 776 777 type review struct { 778 jirix *jiri.X 779 featureBranch string 780 reviewBranch string 781 project project.Project 782 gerrit.CLOpts 783 } 784 785 func newReview(jirix *jiri.X, project project.Project, opts gerrit.CLOpts) (*review, error) { 786 // Sync all CLs in the sequence of dependent CLs ending in the 787 // current branch. 788 if err := syncCL(jirix); err != nil { 789 return nil, err 790 } 791 792 // Make sure that all CLs in the above sequence (possibly except for 793 // the current branch) have been exported to Gerrit. This is needed 794 // to make sure we have commit messages for all but the last CL. 795 // 796 // NOTE: The alternative here is to prompt the user for multiple 797 // commit messages, which seems less user friendly. 798 if err := checkDependents(jirix); err != nil { 799 return nil, err 800 } 801 802 branch, err := gitutil.New(jirix.NewSeq()).CurrentBranchName() 803 if err != nil { 804 return nil, err 805 } 806 opts.Branch = branch 807 if opts.Topic == "" { 808 opts.Topic = fmt.Sprintf("%s-%s", os.Getenv("USER"), branch) // use <username>-<branchname> as the default 809 } 810 if opts.Presubmit == gerrit.PresubmitTestType("") { 811 opts.Presubmit = gerrit.PresubmitTestTypeAll // use gerrit.PresubmitTestTypeAll as the default 812 } 813 if opts.RemoteBranch == "" { 814 opts.RemoteBranch = "master" // use master as the default 815 } 816 return &review{ 817 jirix: jirix, 818 project: project, 819 featureBranch: branch, 820 reviewBranch: branch + "-REVIEW", 821 CLOpts: opts, 822 }, nil 823 } 824 825 func checkPresubmitFlag() bool { 826 for _, t := range gerrit.PresubmitTestTypes() { 827 if presubmitFlag == t { 828 return true 829 } 830 } 831 return false 832 } 833 834 // confirmFlagChanges asks users to confirm if any of the 835 // presubmit and autosubmit flags changes. 836 func (review *review) confirmFlagChanges() (bool, error) { 837 file, err := getCommitMessageFileName(review.jirix, review.CLOpts.Branch) 838 if err != nil { 839 return false, err 840 } 841 bytes, err := review.jirix.NewSeq().ReadFile(file) 842 if err != nil { 843 if runutil.IsNotExist(err) { 844 return true, nil 845 } 846 return false, err 847 } 848 content := string(bytes) 849 changes := []string{} 850 851 // Check presubmit label change. 852 prevPresubmitType := string(gerrit.PresubmitTestTypeAll) 853 matches := presubmitTestLabelRE.FindStringSubmatch(content) 854 if matches != nil { 855 prevPresubmitType = matches[1] 856 } 857 if presubmitFlag != prevPresubmitType { 858 changes = append(changes, fmt.Sprintf("- presubmit=%s to presubmit=%s", prevPresubmitType, presubmitFlag)) 859 } 860 861 // Check autosubmit label change. 862 prevAutosubmit := autosubmitLabelRE.MatchString(content) 863 if autosubmitFlag != prevAutosubmit { 864 changes = append(changes, fmt.Sprintf("- autosubmit=%v to autosubmit=%v", prevAutosubmit, autosubmitFlag)) 865 866 } 867 868 if len(changes) > 0 { 869 fmt.Printf("Changes:\n%s\n", strings.Join(changes, "\n")) 870 fmt.Print("Are you sure you want to make the above changes? y/N:") 871 var response string 872 if _, err := fmt.Scanf("%s\n", &response); err != nil || response != "y" { 873 return false, nil 874 } 875 } 876 return true, nil 877 } 878 879 // cleanup cleans up after the review. 880 func (review *review) cleanup(stashed bool) error { 881 git := gitutil.New(review.jirix.NewSeq()) 882 if err := git.CheckoutBranch(review.CLOpts.Branch); err != nil { 883 return err 884 } 885 if git.BranchExists(review.reviewBranch) { 886 if err := git.DeleteBranch(review.reviewBranch, gitutil.ForceOpt(true)); err != nil { 887 return err 888 } 889 } 890 if stashed { 891 if err := git.StashPop(); err != nil { 892 return err 893 } 894 } 895 return nil 896 } 897 898 // createReviewBranch creates a clean review branch from the remote 899 // branch this CL pertains to and then iterates over the sequence of 900 // dependent CLs leading to the current branch, creating one commit 901 // per CL by squashing all commits of each individual CL. The commit 902 // message for all but that last CL is derived from their 903 // <commitMessageFileName>, while the <message> argument is used as 904 // the commit message for the last commit. 905 func (review *review) createReviewBranch(message string) (e error) { 906 git := gitutil.New(review.jirix.NewSeq()) 907 // Create the review branch. 908 if err := git.FetchRefspec("origin", review.CLOpts.RemoteBranch); err != nil { 909 return err 910 } 911 if git.BranchExists(review.reviewBranch) { 912 if err := git.DeleteBranch(review.reviewBranch, gitutil.ForceOpt(true)); err != nil { 913 return err 914 } 915 } 916 upstream := "origin/" + review.CLOpts.RemoteBranch 917 if err := git.CreateBranchWithUpstream(review.reviewBranch, upstream); err != nil { 918 return err 919 } 920 if err := git.CheckoutBranch(review.reviewBranch); err != nil { 921 return err 922 } 923 // Register a cleanup handler in case of subsequent errors. 924 cleanup := true 925 defer collect.Error(func() error { 926 if !cleanup { 927 return git.CheckoutBranch(review.CLOpts.Branch) 928 } 929 git.CheckoutBranch(review.CLOpts.Branch, gitutil.ForceOpt(true)) 930 git.DeleteBranch(review.reviewBranch, gitutil.ForceOpt(true)) 931 return nil 932 }, &e) 933 934 // Report an error if the CL is empty. 935 hasDiff, err := git.BranchesDiffer(review.CLOpts.Branch, review.reviewBranch) 936 if err != nil { 937 return err 938 } 939 if !hasDiff { 940 return emptyChangeError(struct{}{}) 941 } 942 943 // If <message> is empty, replace it with the default message. 944 if len(message) == 0 { 945 var err error 946 message, err = review.defaultCommitMessage() 947 if err != nil { 948 return err 949 } 950 } 951 952 // Iterate over all dependent CLs leading to (and including) the 953 // current branch, creating one commit in the review branch per CL 954 // by squashing all commits of each individual CL. 955 branches, err := getDependentCLs(review.jirix, review.CLOpts.Branch) 956 if err != nil { 957 return err 958 } 959 branches = append(branches, review.CLOpts.Branch) 960 if err := review.squashBranches(branches, message); err != nil { 961 return err 962 } 963 964 cleanup = false 965 return nil 966 } 967 968 // squashBranches iterates over the given list of branches, creating 969 // one commit per branch in the current branch by squashing all 970 // commits of each individual branch. 971 // 972 // TODO(jsimsa): Consider using "git rebase --onto" to avoid having to 973 // deal with merge conflicts. 974 func (review *review) squashBranches(branches []string, message string) (e error) { 975 git := gitutil.New(review.jirix.NewSeq()) 976 for i := 1; i < len(branches); i++ { 977 // We want to merge the <branches[i]> branch on top of the review 978 // branch, forcing all conflicts to be reviewed in favor of the 979 // <branches[i]> branch. Unfortunately, git merge does not offer a 980 // strategy that would do that for us. The solution implemented 981 // here is based on: 982 // 983 // http://stackoverflow.com/questions/173919/is-there-a-theirs-version-of-git-merge-s-ours 984 if err := git.Merge(branches[i], gitutil.SquashOpt(true), gitutil.StrategyOpt("ours")); err != nil { 985 return changeConflictError{ 986 localBranch: branches[i], 987 remoteBranch: review.CLOpts.RemoteBranch, 988 message: err.Error(), 989 } 990 } 991 // Fetch the timestamp of the last commit of <branches[i]> and use 992 // it to create the squashed commit. This is needed to make sure 993 // that the commit hash of the squashed commit stays the same as 994 // long as the squashed sequence of commits does not change. If 995 // this was not the case, consecutive invocations of "jiri cl mail" 996 // could fail if some, but not all, of the dependent CLs submitted 997 // to Gerrit have changed. 998 output, err := git.Log(branches[i], branches[i]+"^", "%ad%n%cd") 999 if err != nil { 1000 return err 1001 } 1002 if len(output) < 1 || len(output[0]) < 2 { 1003 return fmt.Errorf("unexpected output length: %v", output) 1004 } 1005 authorDate := gitutil.AuthorDateOpt(output[0][0]) 1006 committer := gitutil.CommitterDateOpt(output[0][1]) 1007 git = gitutil.New(review.jirix.NewSeq(), authorDate, committer) 1008 if i < len(branches)-1 { 1009 file, err := getCommitMessageFileName(review.jirix, branches[i]) 1010 if err != nil { 1011 return err 1012 } 1013 message, err := review.jirix.NewSeq().ReadFile(file) 1014 if err != nil { 1015 return err 1016 } 1017 if err := git.CommitWithMessage(string(message)); err != nil { 1018 return err 1019 } 1020 } else { 1021 committer := git.NewCommitter(review.CLOpts.Edit) 1022 if err := committer.Commit(message); err != nil { 1023 return err 1024 } 1025 } 1026 tmpBranch := review.reviewBranch + "-" + branches[i] + "-TMP" 1027 if err := git.CreateBranch(tmpBranch); err != nil { 1028 return err 1029 } 1030 defer collect.Error(func() error { 1031 return git.DeleteBranch(tmpBranch, gitutil.ForceOpt(true)) 1032 }, &e) 1033 if err := git.Reset(branches[i]); err != nil { 1034 return err 1035 } 1036 if err := git.Reset(tmpBranch, gitutil.ModeOpt("soft")); err != nil { 1037 return err 1038 } 1039 if err := git.CommitAmend(); err != nil { 1040 return err 1041 } 1042 } 1043 return nil 1044 } 1045 1046 func (review *review) readMultiPart() string { 1047 s := review.jirix.NewSeq() 1048 filename := filepath.Join(review.project.Path, jiri.ProjectMetaDir, review.featureBranch, multiPartMetaDataFileName) 1049 mpart, err := s.ReadFile(filename) 1050 if err != nil { 1051 return "" 1052 } 1053 return strings.TrimSpace(string(mpart)) 1054 } 1055 1056 // strippedGerritCommitMessage returns the commit message stripped of variable 1057 // meta-data such as multipart messages, or change IDs: 1058 func strippedGerritCommitMessage(jirix *jiri.X, branch string) (string, error) { 1059 filename, err := getCommitMessageFileName(jirix, branch) 1060 if err != nil { 1061 return "", err 1062 } 1063 msg, err := jirix.NewSeq().ReadFile(filename) 1064 if err != nil { 1065 return "", err 1066 } 1067 // Strip "MultiPart ..." from the commit messages. 1068 strippedMessages := multiPartRE.ReplaceAllLiteralString(string(msg), "") 1069 // Strip "Change-Id: ..." from the commit messages. 1070 strippedMessages = changeIDRE.ReplaceAllLiteralString(strippedMessages, "") 1071 return strippedMessages, nil 1072 } 1073 1074 // defaultCommitMessage creates the default commit message from the 1075 // list of commits on the branch. 1076 func (review *review) defaultCommitMessage() (string, error) { 1077 commitMessages := "" 1078 var err error 1079 if commitMessageBodyFlag != "" { 1080 msg, tmpErr := ioutil.ReadFile(commitMessageBodyFlag) 1081 commitMessages = string(msg) 1082 err = tmpErr 1083 } else { 1084 commitMessages, err = gitutil.New(review.jirix.NewSeq()).CommitMessages(review.CLOpts.Branch, review.reviewBranch) 1085 } 1086 if err != nil { 1087 return "", err 1088 } 1089 // Strip "MultiPart ..." from the commit messages. 1090 strippedMessages := multiPartRE.ReplaceAllLiteralString(commitMessages, "") 1091 // Strip "Change-Id: ..." from the commit messages. 1092 strippedMessages = changeIDRE.ReplaceAllLiteralString(strippedMessages, "") 1093 // Add comment markers (#) to every line. 1094 commentedMessages := "# " + strings.Replace(strippedMessages, "\n", "\n# ", -1) 1095 message := defaultMessageHeader + commentedMessages 1096 if multipart := review.readMultiPart(); multipart != "" { 1097 message = message + "\n" + multipart + "\n" 1098 } 1099 return message, nil 1100 } 1101 1102 // ensureChangeID makes sure that the last commit contains a Change-Id, and 1103 // returns an error if it does not. 1104 func (review *review) ensureChangeID() error { 1105 latestCommitMessage, err := gitutil.New(review.jirix.NewSeq()).LatestCommitMessage() 1106 if err != nil { 1107 return err 1108 } 1109 changeID := changeIDRE.FindString(latestCommitMessage) 1110 if changeID == "" { 1111 return noChangeIDError(struct{}{}) 1112 } 1113 return nil 1114 } 1115 1116 // processLabelsAndCommitFile adds/removes labels for the given commit 1117 // message and merges in the contents of the initial-message-file. 1118 func (review *review) processLabelsAndCommitFile(message string) string { 1119 // Find the Change-ID and MultiPart lines. 1120 changeIDLine := changeIDRE.FindString(message) 1121 multiPartLine := multiPartRE.FindString(message) 1122 1123 if commitMessageBodyFlag != "" { 1124 if msg, err := ioutil.ReadFile(commitMessageBodyFlag); err == nil { 1125 message = string(msg) 1126 } 1127 } 1128 1129 // Strip existing labels and change-ID. 1130 message = autosubmitLabelRE.ReplaceAllLiteralString(message, "") 1131 message = presubmitTestLabelRE.ReplaceAllLiteralString(message, "") 1132 message = changeIDRE.ReplaceAllLiteralString(message, "") 1133 message = multiPartRE.ReplaceAllLiteralString(message, "") 1134 1135 // Insert labels and change-ID back. 1136 if review.CLOpts.Autosubmit { 1137 message += fmt.Sprintf("AutoSubmit\n") 1138 } 1139 if review.CLOpts.Presubmit != gerrit.PresubmitTestTypeAll { 1140 message += fmt.Sprintf("PresubmitTest: %s\n", review.CLOpts.Presubmit) 1141 } 1142 if multiPartLine != "" && !strings.HasSuffix(message, "\n") { 1143 message += "\n" 1144 } else { 1145 if multipart := review.readMultiPart(); multipart != "" { 1146 if !strings.HasSuffix(message, "\n") { 1147 message += "\n" 1148 } 1149 multiPartLine = multipart 1150 } 1151 } 1152 message += multiPartLine 1153 if changeIDLine != "" && !strings.HasSuffix(message, "\n") { 1154 message += "\n" 1155 } 1156 message += changeIDLine 1157 return message 1158 } 1159 1160 // run implements checks that the review passes all local checks 1161 // and then mails it to Gerrit. 1162 func (review *review) run() (e error) { 1163 git := gitutil.New(review.jirix.NewSeq()) 1164 if uncommittedFlag { 1165 changes, err := git.FilesWithUncommittedChanges() 1166 if err != nil { 1167 return err 1168 } 1169 if len(changes) != 0 { 1170 return uncommittedChangesError(changes) 1171 } 1172 } 1173 if review.CLOpts.Branch == remoteBranchFlag { 1174 return fmt.Errorf("cannot do a review from the %q branch.", remoteBranchFlag) 1175 } 1176 stashed, err := git.Stash() 1177 if err != nil { 1178 return err 1179 } 1180 defer collect.Error(func() error { return review.cleanup(stashed) }, &e) 1181 wd, err := os.Getwd() 1182 if err != nil { 1183 return fmt.Errorf("Getwd() failed: %v", err) 1184 } 1185 topLevel, err := git.TopLevel() 1186 if err != nil { 1187 return err 1188 } 1189 s := review.jirix.NewSeq() 1190 if err := s.Chdir(topLevel).Done(); err != nil { 1191 return err 1192 } 1193 defer collect.Error(func() error { return review.jirix.NewSeq().Chdir(wd).Done() }, &e) 1194 1195 file, err := getCommitMessageFileName(review.jirix, review.CLOpts.Branch) 1196 if err != nil { 1197 return err 1198 } 1199 1200 message := messageFlag 1201 if message == "" { 1202 // Message was not passed in flag. Attempt to read it from file. 1203 data, err := s.ReadFile(file) 1204 if err != nil { 1205 if !runutil.IsNotExist(err) { 1206 return err 1207 } 1208 } else { 1209 message = string(data) 1210 } 1211 } 1212 1213 // Add/remove labels to/from the commit message before asking users 1214 // to edit it. We do this only when this is not the initial commit 1215 // where the message is empty. 1216 // 1217 // For the initial commit, the labels will be processed after the 1218 // message is edited by users, which happens in the 1219 // updateReviewMessage method. 1220 if message != "" { 1221 message = review.processLabelsAndCommitFile(message) 1222 } 1223 if err := review.createReviewBranch(message); err != nil { 1224 return err 1225 } 1226 if err := review.updateReviewMessage(file); err != nil { 1227 return err 1228 } 1229 if err := review.send(); err != nil { 1230 return err 1231 } 1232 if setTopicFlag { 1233 if err := review.setTopic(); err != nil { 1234 return err 1235 } 1236 } 1237 return nil 1238 } 1239 1240 // send mails the current branch out for review. 1241 func (review *review) send() error { 1242 if err := review.ensureChangeID(); err != nil { 1243 return err 1244 } 1245 if err := gerrit.Push(review.jirix.NewSeq(), review.CLOpts); err != nil { 1246 return gerritError(err.Error()) 1247 } 1248 return nil 1249 } 1250 1251 // getChangeID reads the commit message and extracts the change-Id 1252 func (review *review) getChangeID() (string, error) { 1253 file, err := getCommitMessageFileName(review.jirix, review.CLOpts.Branch) 1254 if err != nil { 1255 return "", err 1256 } 1257 bytes, err := review.jirix.NewSeq().ReadFile(file) 1258 if err != nil { 1259 return "", err 1260 } 1261 changeID := changeIDRE.FindSubmatch(bytes) 1262 if changeID == nil || len(changeID) < 2 { 1263 return "", fmt.Errorf("could not find Change-Id in:\n%s", bytes) 1264 } 1265 return string(changeID[1]), nil 1266 } 1267 1268 // setTopic sets the topic for the CL corresponding to the branch the 1269 // review was created for. 1270 func (review *review) setTopic() error { 1271 changeID, err := review.getChangeID() 1272 if err != nil { 1273 return err 1274 } 1275 host := review.CLOpts.Host 1276 if host.Scheme != "http" && host.Scheme != "https" { 1277 return fmt.Errorf("Cannot set topic for gerrit host %q. Please use a host url with 'https' scheme or run with '--set-topic=false'.", host.String()) 1278 } 1279 if err := review.jirix.Gerrit(host).SetTopic(changeID, review.CLOpts); err != nil { 1280 return fmt.Errorf("failed to set topic for %v, %#v: %v", changeID, review.CLOpts, err) 1281 } 1282 return nil 1283 } 1284 1285 // updateReviewMessage writes the commit message to the given file. 1286 func (review *review) updateReviewMessage(file string) error { 1287 git := gitutil.New(review.jirix.NewSeq()) 1288 if err := git.CheckoutBranch(review.reviewBranch); err != nil { 1289 return err 1290 } 1291 newMessage, err := git.LatestCommitMessage() 1292 if err != nil { 1293 return err 1294 } 1295 // update MultiPart metadata. 1296 mpart := review.readMultiPart() 1297 newMessage = multiPartRE.ReplaceAllLiteralString(newMessage, mpart) 1298 s := review.jirix.NewSeq() 1299 // For the initial commit where the commit message file doesn't exist, 1300 // add/remove labels after users finish editing the commit message. 1301 // 1302 // This behavior is consistent with how Change-ID is added for the 1303 // initial commit so we don't confuse users. 1304 if _, err := s.Stat(file); err != nil { 1305 if runutil.IsNotExist(err) { 1306 newMessage = review.processLabelsAndCommitFile(newMessage) 1307 if err := git.CommitAmendWithMessage(newMessage); err != nil { 1308 return err 1309 } 1310 } else { 1311 return err 1312 } 1313 } 1314 topLevel, err := git.TopLevel() 1315 if err != nil { 1316 return err 1317 } 1318 newMetadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir, review.CLOpts.Branch) 1319 if err := s.MkdirAll(newMetadataDir, os.FileMode(0755)). 1320 WriteFile(file, []byte(newMessage), 0644).Done(); err != nil { 1321 return err 1322 } 1323 return nil 1324 } 1325 1326 // cmdCLNew represents the "jiri cl new" command. 1327 var cmdCLNew = &cmdline.Command{ 1328 Runner: jiri.RunnerFunc(runCLNew), 1329 Name: "new", 1330 Short: "Create a new local branch for a changelist", 1331 Long: fmt.Sprintf(` 1332 Command "new" creates a new local branch for a changelist. In 1333 particular, it forks a new branch with the given name from the current 1334 branch and records the relationship between the current branch and the 1335 new branch in the %v metadata directory. The information recorded in 1336 the %v metadata directory tracks dependencies between CLs and is used 1337 by the "jiri cl sync" and "jiri cl mail" commands. 1338 `, jiri.ProjectMetaDir, jiri.ProjectMetaDir), 1339 ArgsName: "<name>", 1340 ArgsLong: "<name> is the changelist name.", 1341 } 1342 1343 func runCLNew(jirix *jiri.X, args []string) error { 1344 if got, want := len(args), 1; got != want { 1345 return jirix.UsageErrorf("unexpected number of arguments: got %v, want %v", got, want) 1346 } 1347 return newCL(jirix, args) 1348 } 1349 1350 func newCL(jirix *jiri.X, args []string) error { 1351 git := gitutil.New(jirix.NewSeq()) 1352 topLevel, err := git.TopLevel() 1353 if err != nil { 1354 return err 1355 } 1356 originalBranch, err := git.CurrentBranchName() 1357 if err != nil { 1358 return err 1359 } 1360 1361 // Create a new branch using the current branch. 1362 newBranch := args[0] 1363 if err := git.CreateAndCheckoutBranch(newBranch); err != nil { 1364 return err 1365 } 1366 1367 // Register a cleanup handler in case of subsequent errors. 1368 cleanup := true 1369 defer func() { 1370 if cleanup { 1371 git.CheckoutBranch(originalBranch, gitutil.ForceOpt(true)) 1372 git.DeleteBranch(newBranch, gitutil.ForceOpt(true)) 1373 } 1374 }() 1375 1376 s := jirix.NewSeq() 1377 // Record the dependent CLs for the new branch. The dependent CLs 1378 // are recorded in a <dependencyPathFileName> file as a 1379 // newline-separated list of branch names. 1380 branches, err := getDependentCLs(jirix, originalBranch) 1381 if err != nil { 1382 return err 1383 } 1384 branches = append(branches, originalBranch) 1385 newMetadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir, newBranch) 1386 if err := s.MkdirAll(newMetadataDir, os.FileMode(0755)).Done(); err != nil { 1387 return err 1388 } 1389 file, err := getDependencyPathFileName(jirix, newBranch) 1390 if err != nil { 1391 return err 1392 } 1393 if err := s.WriteFile(file, []byte(strings.Join(branches, "\n")), os.FileMode(0644)).Done(); err != nil { 1394 return err 1395 } 1396 1397 cleanup = false 1398 return nil 1399 } 1400 1401 // cmdCLSync represents the "jiri cl sync" command. 1402 var cmdCLSync = &cmdline.Command{ 1403 Runner: jiri.RunnerFunc(runCLSync), 1404 Name: "sync", 1405 Short: "Bring a changelist up to date", 1406 Long: fmt.Sprintf(` 1407 Command "sync" brings the CL identified by the current branch up to 1408 date with the branch tracking the remote branch this CL pertains 1409 to. To do that, the command uses the information recorded in the %v 1410 metadata directory to identify the sequence of dependent CLs leading 1411 to the current branch. The command then iterates over this sequence 1412 bringing each of the CLs up to date with its ancestor. The end result 1413 of this process is that all CLs in the sequence are up to date with 1414 the branch that tracks the remote branch this CL pertains to. 1415 1416 NOTE: It is possible that the command cannot automatically merge 1417 changes in an ancestor into its dependent. When that occurs, the 1418 command is aborted and prints instructions that need to be followed 1419 before the command can be retried. 1420 `, jiri.ProjectMetaDir), 1421 } 1422 1423 func runCLSync(jirix *jiri.X, _ []string) error { 1424 return syncCL(jirix) 1425 } 1426 1427 func syncCL(jirix *jiri.X) (e error) { 1428 git := gitutil.New(jirix.NewSeq()) 1429 stashed, err := git.Stash() 1430 if err != nil { 1431 return err 1432 } 1433 if stashed { 1434 defer collect.Error(func() error { return git.StashPop() }, &e) 1435 } 1436 1437 // Register a cleanup handler in case of subsequent errors. 1438 forceOriginalBranch := true 1439 originalBranch, err := git.CurrentBranchName() 1440 if err != nil { 1441 return err 1442 } 1443 originalWd, err := os.Getwd() 1444 if err != nil { 1445 return err 1446 } 1447 1448 defer func() { 1449 if forceOriginalBranch { 1450 git.CheckoutBranch(originalBranch, gitutil.ForceOpt(true)) 1451 } 1452 jirix.NewSeq().Chdir(originalWd) 1453 }() 1454 1455 s := jirix.NewSeq() 1456 // Switch to an existing directory in master so we can run commands. 1457 topLevel, err := git.TopLevel() 1458 if err != nil { 1459 return err 1460 } 1461 if err := s.Chdir(topLevel).Done(); err != nil { 1462 return err 1463 } 1464 1465 // Identify the dependents CLs leading to (and including) the 1466 // current branch. 1467 branches, err := getDependentCLs(jirix, originalBranch) 1468 if err != nil { 1469 return err 1470 } 1471 branches = append(branches, originalBranch) 1472 1473 // Sync from upstream. 1474 if err := git.CheckoutBranch(branches[0]); err != nil { 1475 return err 1476 } 1477 if err := git.Pull("origin", branches[0]); err != nil { 1478 return err 1479 } 1480 1481 // Bring all CLs in the sequence of dependent CLs leading to the 1482 // current branch up to date with the <remoteBranchFlag> branch. 1483 for i := 1; i < len(branches); i++ { 1484 if err := git.CheckoutBranch(branches[i]); err != nil { 1485 return err 1486 } 1487 if err := git.Merge(branches[i-1]); err != nil { 1488 return fmt.Errorf(`Failed to automatically merge branch %v into branch %v: %v 1489 The following steps are needed before the operation can be retried: 1490 $ git checkout %v 1491 $ git merge %v 1492 # resolve all conflicts 1493 $ git commit -a 1494 $ git checkout %v 1495 # retry the original operation 1496 `, branches[i], branches[i-1], err, branches[i], branches[i-1], originalBranch) 1497 } 1498 } 1499 1500 forceOriginalBranch = false 1501 return nil 1502 }