func newCL(jirix *jiri.X, args []string) error { git := gitutil.New(jirix.NewSeq()) topLevel, err := git.TopLevel() if err != nil { return err } originalBranch, err := git.CurrentBranchName() if err != nil { return err } // Create a new branch using the current branch. newBranch := args[0] if err := git.CreateAndCheckoutBranch(newBranch); err != nil { return err } // Register a cleanup handler in case of subsequent errors. cleanup := true defer func() { if cleanup { git.CheckoutBranch(originalBranch, gitutil.ForceOpt(true)) git.DeleteBranch(newBranch, gitutil.ForceOpt(true)) } }() s := jirix.NewSeq() // Record the dependent CLs for the new branch. The dependent CLs // are recorded in a <dependencyPathFileName> file as a // newline-separated list of branch names. branches, err := getDependentCLs(jirix, originalBranch) if err != nil { return err } branches = append(branches, originalBranch) newMetadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir, newBranch) if err := s.MkdirAll(newMetadataDir, os.FileMode(0755)).Done(); err != nil { return err } file, err := getDependencyPathFileName(jirix, newBranch) if err != nil { return err } if err := s.WriteFile(file, []byte(strings.Join(branches, "\n")), os.FileMode(0644)).Done(); err != nil { return err } cleanup = false return nil }
func newCL(ctx *tool.Context, args []string) error { topLevel, err := ctx.Git().TopLevel() if err != nil { return err } originalBranch, err := ctx.Git().CurrentBranchName() if err != nil { return err } // Create a new branch using the current branch. newBranch := args[0] if err := ctx.Git().CreateAndCheckoutBranch(newBranch); err != nil { return err } // Register a cleanup handler in case of subsequent errors. cleanup := true defer func() { if cleanup { ctx.Git().CheckoutBranch(originalBranch, gitutil.ForceOpt(true)) ctx.Git().DeleteBranch(newBranch, gitutil.ForceOpt(true)) } }() // Record the dependent CLs for the new branch. The dependent CLs // are recorded in a <dependencyPathFileName> file as a // newline-separated list of branch names. branches, err := getDependentCLs(ctx, originalBranch) if err != nil { return err } branches = append(branches, originalBranch) newMetadataDir := filepath.Join(topLevel, project.MetadataDirName(), newBranch) if err := ctx.Run().MkdirAll(newMetadataDir, os.FileMode(0755)); err != nil { return err } file, err := getDependencyPathFileName(ctx, newBranch) if err != nil { return err } if err := ctx.Run().WriteFile(file, []byte(strings.Join(branches, "\n")), os.FileMode(0644)); err != nil { return err } cleanup = false return nil }
// resetLocalProject checks out the master branch, cleans up untracked files // and uncommitted changes, and optionally deletes all the other branches. func resetLocalProject(ctx *tool.Context, cleanupBranches bool, remoteBranch string) error { // Check out master and clean up changes. curBranchName, err := ctx.Git().CurrentBranchName() if err != nil { return err } if curBranchName != "master" { if err := ctx.Git().CheckoutBranch("master", gitutil.ForceOpt(true)); err != nil { return err } } if err := ctx.Git().RemoveUntrackedFiles(); err != nil { return err } // Discard any uncommitted changes. if remoteBranch == "" { remoteBranch = "master" } if err := ctx.Git().Reset("origin/" + remoteBranch); err != nil { return err } // Delete all the other branches. // At this point we should be at the master branch. branches, _, err := ctx.Git().GetBranches() if err != nil { return err } for _, branch := range branches { if branch == "master" { continue } if cleanupBranches { if err := ctx.Git().DeleteBranch(branch, gitutil.ForceOpt(true)); err != nil { return nil } } } return nil }
// cleanup cleans up after the review. func (review *review) cleanup(stashed bool) error { if err := review.ctx.Git().CheckoutBranch(review.CLOpts.Branch); err != nil { return err } if review.ctx.Git().BranchExists(review.reviewBranch) { if err := review.ctx.Git().DeleteBranch(review.reviewBranch, gitutil.ForceOpt(true)); err != nil { return err } } if stashed { if err := review.ctx.Git().StashPop(); err != nil { return err } } return nil }
// cleanup cleans up after the review. func (review *review) cleanup(stashed bool) error { git := gitutil.New(review.jirix.NewSeq()) if err := git.CheckoutBranch(review.CLOpts.Branch); err != nil { return err } if git.BranchExists(review.reviewBranch) { if err := git.DeleteBranch(review.reviewBranch, gitutil.ForceOpt(true)); err != nil { return err } } if stashed { if err := git.StashPop(); err != nil { return err } } return nil }
func TestMultiPart(t *testing.T) { fake, cleanup := jiritest.NewFakeJiriRoot(t) defer cleanup() projects := addProjects(t, fake) origCleanupFlag, origCurrentProjectFlag := cleanupMultiPartFlag, currentProjectFlag defer func() { cleanupMultiPartFlag, currentProjectFlag = origCleanupFlag, origCurrentProjectFlag }() cleanupMultiPartFlag, currentProjectFlag = false, false name, err := gitutil.New(fake.X.NewSeq()).CurrentBranchName() if err != nil { t.Fatal(err) } if name == "master" { // The test cases below assume that they are run on a feature-branch, // but this is not necessarily always the case when running under // jenkins, so if it's run on a master branch it will create // a feature branch. if err := gitutil.New(fake.X.NewSeq()).CreateAndCheckoutBranch("feature-branch"); err != nil { t.Fatal(err) } defer func() { git := gitutil.New(fake.X.NewSeq()) git.CheckoutBranch("master", gitutil.ForceOpt(true)) git.DeleteBranch("feature-branch", gitutil.ForceOpt(true)) }() } cwd, err := os.Getwd() if err != nil { t.Fatal(err) } defer os.Chdir(cwd) relchdir := func(dir string) { chdir(t, fake.X, dir) } initMP := func() *multiPart { mp, err := initForMultiPart(fake.X) if err != nil { _, file, line, _ := runtime.Caller(1) t.Fatalf("%s:%d: %v", filepath.Base(file), line, err) } return mp } wr := func(mp *multiPart) *multiPart { return mp } git := func(dir string) *gitutil.Git { return gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(dir)) } cleanupMultiPartFlag = true if got, want := initMP(), wr(&multiPart{clean: true}); !reflect.DeepEqual(got, want) { t.Errorf("got %#v, want %#v", got, want) } currentProjectFlag = true if got, want := initMP(), wr(&multiPart{clean: true, current: true}); !reflect.DeepEqual(got, want) { t.Errorf("got %#v, want %#v", got, want) } cleanupMultiPartFlag, currentProjectFlag = false, false // Test metadata generation. ra := projects[0].Path rb := projects[1].Path rc := projects[2].Path t1 := projects[3].Path git(ra).CreateAndCheckoutBranch("a1") relchdir(ra) if got, want := initMP(), wr(&multiPart{current: true, currentKey: projects[0].Key(), currentBranch: "a1"}); !reflect.DeepEqual(got, want) { t.Errorf("got %#v, want %#v", got, want) } git(rb).CreateAndCheckoutBranch("a1") mp := initMP() if mp.current != false || mp.clean != false { t.Errorf("current or clean not false: %v, %v", mp.current, mp.clean) } if got, want := len(mp.keys), 2; got != want { t.Errorf("got %v, want %v", got, want) } tmp := &multiPart{ keys: project.ProjectKeys{projects[0].Key(), projects[1].Key()}, } for i, k := range mp.keys { if got, want := k, tmp.keys[i]; got != want { t.Errorf("got %v, want %v", got, want) } } if got, want := len(mp.states), 2; got != want { t.Errorf("got %v, want %v", got, want) } git(rc).CreateAndCheckoutBranch("a1") git(t1).CreateAndCheckoutBranch("a2") mp = initMP() if got, want := len(mp.keys), 3; got != want { t.Errorf("got %v, want %v", got, want) } if err := mp.writeMultiPartMetadata(fake.X); err != nil { t.Fatal(err) } hasMetaData := func(total int, branch string, projectPaths ...string) { _, file, line, _ := runtime.Caller(1) loc := fmt.Sprintf("%s:%d", filepath.Base(file), line) for i, dir := range projectPaths { filename := filepath.Join(dir, jiri.ProjectMetaDir, branch, multiPartMetaDataFileName) msg, err := ioutil.ReadFile(filename) if err != nil { t.Fatalf("%s: %v", loc, err) } if got, want := string(msg), fmt.Sprintf("MultiPart: %d/%d\n", i+1, total); got != want { t.Errorf("%v: got %v, want %v", dir, got, want) } } } hasNoMetaData := func(branch string, projectPaths ...string) { _, file, line, _ := runtime.Caller(1) loc := fmt.Sprintf("%s:%d", filepath.Base(file), line) for _, dir := range projectPaths { filename := filepath.Join(fake.X.Root, dir, jiri.ProjectMetaDir, branch, multiPartMetaDataFileName) _, err := os.Stat(filename) if !os.IsNotExist(err) { t.Fatalf("%s: %s should not exist", loc, filename) } } } newFile := func(dir, file string) { testfile := filepath.Join(dir, file) _, err := fake.X.NewSeq().Create(testfile) if err != nil { t.Errorf("failed to create %s: %v", testfile, err) } } hasMetaData(len(mp.keys), "a1", ra, rb, rc) hasNoMetaData(t1, "a2") if err := mp.cleanMultiPartMetadata(fake.X); err != nil { t.Fatal(err) } hasNoMetaData(ra, "a1", rb, rc, t1) // Test CL messages. for _, p := range projects { // Install commit hook so that Change-Id is written. installCommitMsgHook(t, fake.X, p.Path) } // Create a fake jiri root for the fake gerrit repos. gerritFake, gerritCleanup := jiritest.NewFakeJiriRoot(t) defer gerritCleanup() relchdir(ra) if err := mp.writeMultiPartMetadata(fake.X); err != nil { t.Fatal(err) } hasMetaData(len(mp.keys), "a1", ra, rb, rc) gitAddFiles := func(name string, repos ...string) { for _, dir := range repos { newFile(dir, name) if err := git(dir).Add(name); err != nil { t.Error(err) } } } gitCommit := func(msg string, repos ...string) { for _, dir := range repos { committer := git(dir).NewCommitter(false) if err := committer.Commit(msg); err != nil { t.Error(err) } } } gitAddFiles("new-file", ra, rb, rc) _, err = initForMultiPart(fake.X) if err == nil || !strings.Contains(err.Error(), "uncommitted changes:") { t.Fatalf("expected an error about uncommitted changes: got %v", err) } gitCommit("oh multipart test\n", ra, rb, rc) bodyMessage := "xyz\n\na simple message\n" messageFile := filepath.Join(fake.X.Root, jiri.RootMetaDir, "message-body") if err := ioutil.WriteFile(messageFile, []byte(bodyMessage), 0666); err != nil { t.Fatal(err) } mp = initMP() setTopicFlag = false commitMessageBodyFlag = messageFile testCommitMsgs := func(branch string, cls ...*project.Project) { _, file, line, _ := runtime.Caller(1) loc := fmt.Sprintf("%s:%d", filepath.Base(file), line) total := len(cls) for index, p := range cls { // Create a new gerrit repo each time we commit, since we can't // push more than once to the fake gerrit repo without actually // running gerrit. gp := createRepoFromOrigin(t, gerritFake.X, "gerrit", p.Remote) defer os.Remove(gp) relchdir(p.Path) review, err := newReview(fake.X, *p, gerrit.CLOpts{ Presubmit: gerrit.PresubmitTestTypeNone, Remote: gp, }) if err != nil { t.Fatalf("%v: %v: %v", loc, p.Path, err) } // use the default commit message if err := review.run(); err != nil { t.Fatalf("%v: %v, %v", loc, p.Path, err) } filename, err := getCommitMessageFileName(fake.X, branch) if err != nil { t.Fatalf("%v: %v", loc, err) } msg, err := ioutil.ReadFile(filename) if err != nil { t.Fatalf("%v: %v", loc, err) } if total < 2 { if strings.Contains(string(msg), "MultiPart") { t.Errorf("%v: commit message contains MultiPart when it should not: %v", loc, string(msg)) } continue } expected := fmt.Sprintf("\nMultiPart: %d/%d\n", index+1, total) if !strings.Contains(string(msg), expected) { t.Errorf("%v: commit message for %v does not contain %v: %v", loc, p.Path, expected, string(msg)) } if got, want := string(msg), bodyMessage+"PresubmitTest: none"+expected+"Change-Id: I0000000000000000000000000000000000000000"; got != want { t.Errorf("got %v, want %v", got, want) } } } testCommitMsgs("a1", projects[0], projects[1], projects[2]) cl := mp.commandline("", []string{"-r=alice"}) expected := []string{ "runp", "--interactive", "--projects=" + string(projects[0].Key()) + "," + string(projects[1].Key()) + "," + string(projects[2].Key()), "jiri", "cl", "mail", "--current-project-only=true", "-r=alice", } if got, want := strings.Join(cl, " "), strings.Join(expected, " "); got != want { t.Errorf("got %v, want %v", got, want) } cl = mp.commandline(projects[0].Key(), []string{"-r=bob"}) expected[2] = "--projects=" + string(projects[1].Key()) + "," + string(projects[2].Key()) expected[len(expected)-1] = "-r=bob" if got, want := strings.Join(cl, " "), strings.Join(expected, " "); got != want { t.Errorf("got %v, want %v", got, want) } git(rb).CreateAndCheckoutBranch("a2") gitAddFiles("new-file1", ra, rc) gitCommit("oh multipart test: 2\n", ra, rc) mp = initMP() if err := mp.writeMultiPartMetadata(fake.X); err != nil { t.Fatal(err) } hasMetaData(len(mp.keys), "a1", ra, rc) testCommitMsgs("a1", projects[0], projects[2]) git(ra).CreateAndCheckoutBranch("a2") mp = initMP() if err := mp.writeMultiPartMetadata(fake.X); err != nil { t.Fatal(err) } hasNoMetaData(rc) testCommitMsgs("a1", projects[2]) }
// squashBranches iterates over the given list of branches, creating // one commit per branch in the current branch by squashing all // commits of each individual branch. // // TODO(jsimsa): Consider using "git rebase --onto" to avoid having to // deal with merge conflicts. func (review *review) squashBranches(branches []string, message string) (e error) { for i := 1; i < len(branches); i++ { // We want to merge the <branches[i]> branch on top of the review // branch, forcing all conflicts to be reviewed in favor of the // <branches[i]> branch. Unfortunately, git merge does not offer a // strategy that would do that for us. The solution implemented // here is based on: // // http://stackoverflow.com/questions/173919/is-there-a-theirs-version-of-git-merge-s-ours if err := review.ctx.Git().Merge(branches[i], gitutil.SquashOpt(true), gitutil.StrategyOpt("ours")); err != nil { return changeConflictError{ localBranch: branches[i], remoteBranch: review.CLOpts.RemoteBranch, message: err.Error(), } } // Fetch the timestamp of the last commit of <branches[i]> and use // it to create the squashed commit. This is needed to make sure // that the commit hash of the squashed commit stays the same as // long as the squashed sequence of commits does not change. If // this was not the case, consecutive invocations of "jiri cl mail" // could fail if some, but not all, of the dependent CLs submitted // to Gerrit have changed. output, err := review.ctx.Git().Log(branches[i], branches[i]+"^", "%ad%n%cd") if err != nil { return err } if len(output) < 1 || len(output[0]) < 2 { return fmt.Errorf("unexpected output length: %v", output) } authorDate := tool.AuthorDateOpt(output[0][0]) committerDate := tool.CommitterDateOpt(output[0][1]) if i < len(branches)-1 { file, err := getCommitMessageFileName(review.ctx, branches[i]) if err != nil { return err } message, err := review.ctx.Run().ReadFile(file) if err != nil { return err } if err := review.ctx.Git(authorDate, committerDate).CommitWithMessage(string(message)); err != nil { return err } } else { committer := review.ctx.Git(authorDate, committerDate).NewCommitter(review.CLOpts.Edit) if err := committer.Commit(message); err != nil { return err } } tmpBranch := review.reviewBranch + "-" + branches[i] + "-TMP" if err := review.ctx.Git().CreateBranch(tmpBranch); err != nil { return err } defer collect.Error(func() error { return review.ctx.Git().DeleteBranch(tmpBranch, gitutil.ForceOpt(true)) }, &e) if err := review.ctx.Git().Reset(branches[i]); err != nil { return err } if err := review.ctx.Git().Reset(tmpBranch, gitutil.ModeOpt("soft")); err != nil { return err } if err := review.ctx.Git(authorDate, committerDate).CommitAmend(); err != nil { return err } } return nil }
// createReviewBranch creates a clean review branch from the remote // branch this CL pertains to and then iterates over the sequence of // dependent CLs leading to the current branch, creating one commit // per CL by squashing all commits of each individual CL. The commit // message for all but that last CL is derived from their // <commitMessageFileName>, while the <message> argument is used as // the commit message for the last commit. func (review *review) createReviewBranch(message string) (e error) { // Create the review branch. if err := review.ctx.Git().FetchRefspec("origin", review.CLOpts.RemoteBranch); err != nil { return err } if review.ctx.Git().BranchExists(review.reviewBranch) { if err := review.ctx.Git().DeleteBranch(review.reviewBranch, gitutil.ForceOpt(true)); err != nil { return err } } upstream := "origin/" + review.CLOpts.RemoteBranch if err := review.ctx.Git().CreateBranchWithUpstream(review.reviewBranch, upstream); err != nil { return err } if err := review.ctx.Git().CheckoutBranch(review.reviewBranch); err != nil { return err } // Register a cleanup handler in case of subsequent errors. cleanup := true defer collect.Error(func() error { if !cleanup { return review.ctx.Git().CheckoutBranch(review.CLOpts.Branch) } review.ctx.Git().CheckoutBranch(review.CLOpts.Branch, gitutil.ForceOpt(true)) review.ctx.Git().DeleteBranch(review.reviewBranch, gitutil.ForceOpt(true)) return nil }, &e) // Report an error if the CL is empty. if !review.ctx.DryRun() { hasDiff, err := review.ctx.Git().BranchesDiffer(review.CLOpts.Branch, review.reviewBranch) if err != nil { return err } if !hasDiff { return emptyChangeError(struct{}{}) } } // If <message> is empty, replace it with the default message. if len(message) == 0 { var err error message, err = review.defaultCommitMessage() if err != nil { return err } } // Iterate over all dependent CLs leading to (and including) the // current branch, creating one commit in the review branch per CL // by squashing all commits of each individual CL. branches, err := getDependentCLs(review.ctx, review.CLOpts.Branch) if err != nil { return err } branches = append(branches, review.CLOpts.Branch) if err := review.squashBranches(branches, message); err != nil { return err } cleanup = false return nil }
func cleanupBranch(ctx *tool.Context, branch string) error { if err := ctx.Git().CheckoutBranch(branch); err != nil { return err } if !forceFlag { trackingBranch := "origin/" + remoteBranchFlag if err := ctx.Git().Merge(trackingBranch); err != nil { return err } files, err := ctx.Git().ModifiedFiles(trackingBranch, branch) if err != nil { return err } if len(files) != 0 { return fmt.Errorf("unmerged changes in\n%s", strings.Join(files, "\n")) } } if err := ctx.Git().CheckoutBranch(remoteBranchFlag); err != nil { return err } if err := ctx.Git().DeleteBranch(branch, gitutil.ForceOpt(true)); err != nil { return err } reviewBranch := branch + "-REVIEW" if ctx.Git().BranchExists(reviewBranch) { if err := ctx.Git().DeleteBranch(reviewBranch, gitutil.ForceOpt(true)); err != nil { return err } } // Delete branch metadata. topLevel, err := ctx.Git().TopLevel() if err != nil { return err } metadataDir := filepath.Join(topLevel, project.MetadataDirName()) if err := ctx.Run().RemoveAll(filepath.Join(metadataDir, branch)); err != nil { return err } // Remove the branch from all dependency paths. fileInfos, err := ctx.Run().ReadDir(metadataDir) if err != nil { return err } for _, fileInfo := range fileInfos { if !fileInfo.IsDir() { continue } file, err := getDependencyPathFileName(ctx, fileInfo.Name()) if err != nil { return err } data, err := ctx.Run().ReadFile(file) if err != nil { if !os.IsNotExist(err) { return err } continue } branches := strings.Split(string(data), "\n") for i, tmpBranch := range branches { if branch == tmpBranch { data := []byte(strings.Join(append(branches[:i], branches[i+1:]...), "\n")) if err := ctx.Run().WriteFile(file, data, os.FileMode(0644)); err != nil { return err } break } } } return nil }
func syncCL(ctx *tool.Context) (e error) { stashed, err := ctx.Git().Stash() if err != nil { return err } if stashed { defer collect.Error(func() error { return ctx.Git().StashPop() }, &e) } // Register a cleanup handler in case of subsequent errors. forceOriginalBranch := true originalBranch, err := ctx.Git().CurrentBranchName() if err != nil { return err } originalWd, err := os.Getwd() if err != nil { return err } defer func() { if forceOriginalBranch { ctx.Git().CheckoutBranch(originalBranch, gitutil.ForceOpt(true)) } ctx.Run().Chdir(originalWd) }() // Switch to an existing directory in master so we can run commands. topLevel, err := ctx.Git().TopLevel() if err != nil { return err } if err := ctx.Run().Chdir(topLevel); err != nil { return err } // Identify the dependents CLs leading to (and including) the // current branch. branches, err := getDependentCLs(ctx, originalBranch) if err != nil { return err } branches = append(branches, originalBranch) // Sync from upstream. if err := ctx.Git().CheckoutBranch(branches[0]); err != nil { return err } if err := ctx.Git().Pull("origin", branches[0]); err != nil { return err } // Bring all CLs in the sequence of dependent CLs leading to the // current branch up to date with the <remoteBranchFlag> branch. for i := 1; i < len(branches); i++ { if err := ctx.Git().CheckoutBranch(branches[i]); err != nil { return err } if err := ctx.Git().Merge(branches[i-1]); err != nil { return fmt.Errorf(`Failed to automatically merge branch %v into branch %v: %v The following steps are needed before the operation can be retried: $ git checkout %v $ git merge %v # resolve all conflicts $ git commit -a $ git checkout %v # retry the original operation `, branches[i], branches[i-1], err, branches[i], branches[i-1], originalBranch) } } forceOriginalBranch = false return nil }