Пример #1
0
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
}
Пример #2
0
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
}
Пример #3
0
// 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
}
Пример #4
0
// 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
}
Пример #5
0
// 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
}
Пример #6
0
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])
}
Пример #7
0
// 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
}
Пример #8
0
// 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
}
Пример #9
0
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
}
Пример #10
0
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
}