Exemplo n.º 1
0
// DisableRemoteManifestPush disables pushes to the remote manifest
// repository.
func (fake FakeJiriRoot) DisableRemoteManifestPush() error {
	dir := gitutil.RootDirOpt(filepath.Join(fake.remote, manifestProjectPath))
	if err := gitutil.New(fake.X.NewSeq(), dir).CheckoutBranch("master"); err != nil {
		return err
	}
	return nil
}
Exemplo n.º 2
0
// EnableRemoteManifestPush enables pushes to the remote manifest
// repository.
func (fake FakeJiriRoot) EnableRemoteManifestPush() error {
	dir := gitutil.RootDirOpt(filepath.Join(fake.remote, manifestProjectPath))
	if !gitutil.New(fake.X.NewSeq(), dir).BranchExists("non-master") {
		if err := gitutil.New(fake.X.NewSeq(), dir).CreateBranch("non-master"); err != nil {
			return err
		}
	}
	if err := gitutil.New(fake.X.NewSeq(), dir).CheckoutBranch("non-master"); err != nil {
		return err
	}
	return nil
}
Exemplo n.º 3
0
func (fake FakeJiriRoot) writeManifest(manifest *project.Manifest, dir, path string) error {
	git := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(dir))
	if err := manifest.ToFile(fake.X, path); err != nil {
		return err
	}
	if err := git.Add(path); err != nil {
		return err
	}
	if err := git.Commit(); err != nil {
		return err
	}
	return nil
}
Exemplo n.º 4
0
// CreateRemoteProject creates a new remote project.
func (fake FakeJiriRoot) CreateRemoteProject(name string) error {
	projectDir := filepath.Join(fake.remote, name)
	if err := fake.X.NewSeq().MkdirAll(projectDir, os.FileMode(0700)).Done(); err != nil {
		return err
	}
	if err := gitutil.New(fake.X.NewSeq()).Init(projectDir); err != nil {
		return err
	}
	if err := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(projectDir)).CommitWithMessage("initial commit"); err != nil {
		return err
	}
	fake.Projects[name] = projectDir
	return nil
}
Exemplo n.º 5
0
func setProjectState(jirix *jiri.X, state *ProjectState, checkDirty bool, ch chan<- error) {
	var err error
	switch state.Project.Protocol {
	case "git":
		scm := gitutil.New(jirix.NewSeq(), gitutil.RootDirOpt(state.Project.Path))
		var branches []string
		branches, state.CurrentBranch, err = scm.GetBranches()
		if err != nil {
			ch <- err
			return
		}
		for _, branch := range branches {
			file := filepath.Join(state.Project.Path, jiri.ProjectMetaDir, branch, ".gerrit_commit_message")
			hasFile := true
			if _, err := jirix.NewSeq().Stat(file); err != nil {
				if !runutil.IsNotExist(err) {
					ch <- err
					return
				}
				hasFile = false
			}
			state.Branches = append(state.Branches, BranchState{
				Name:             branch,
				HasGerritMessage: hasFile,
			})
		}
		if checkDirty {
			state.HasUncommitted, err = scm.HasUncommittedChanges()
			if err != nil {
				ch <- err
				return
			}
			state.HasUntracked, err = scm.HasUntrackedFiles()
			if err != nil {
				ch <- err
				return
			}
		}
	default:
		ch <- UnsupportedProtocolErr(state.Project.Protocol)
		return
	}
	ch <- nil
}
Exemplo n.º 6
0
// TestUpdateUniverseWithRevision checks that UpdateUniverse will pull remote
// projects at the specified revision.
func TestUpdateUniverseWithRevision(t *testing.T) {
	localProjects, fake, cleanup := setupUniverse(t)
	defer cleanup()
	s := fake.X.NewSeq()

	// Set project 1's revision in the manifest to the current revision.
	git := gitutil.New(s, gitutil.RootDirOpt(fake.Projects[localProjects[1].Name]))
	rev, err := git.CurrentRevision()
	if err != nil {
		t.Fatal(err)
	}
	m, err := fake.ReadRemoteManifest()
	if err != nil {
		t.Fatal(err)
	}
	projects := []project.Project{}
	for _, p := range m.Projects {
		if p.Name == localProjects[1].Name {
			p.Revision = rev
		}
		projects = append(projects, p)
	}
	m.Projects = projects
	if err := fake.WriteRemoteManifest(m); err != nil {
		t.Fatal(err)
	}
	// Update README in all projects.
	for _, remoteProjectDir := range fake.Projects {
		writeReadme(t, fake.X, remoteProjectDir, "new revision")
	}
	// Check that calling UpdateUniverse() updates all projects except for
	// project 1.
	if err := fake.UpdateUniverse(false); err != nil {
		t.Fatal(err)
	}
	for i, p := range localProjects {
		if i == 1 {
			checkReadme(t, fake.X, p, "initial readme")
		} else {
			checkReadme(t, fake.X, p, "new revision")
		}
	}
}
Exemplo n.º 7
0
// TestCreatePushRemote checks that creating a snapshot with the -push-remote
// flag causes the snapshot to be committed and pushed upstream.
func TestCreatePushRemote(t *testing.T) {
	resetFlags()
	defer resetFlags()

	fake, cleanup := jiritest.NewFakeJiriRoot(t)
	defer cleanup()

	fake.EnableRemoteManifestPush()
	defer fake.DisableRemoteManifestPush()

	manifestDir := filepath.Join(fake.X.Root, "manifest")
	snapshotDir := filepath.Join(manifestDir, "snapshot")
	label := "test"

	git := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(manifestDir))
	commitCount, err := git.CountCommits("master", "")
	if err != nil {
		t.Fatalf("git.CountCommits(\"master\", \"\") failed: %v", err)
	}

	// Create snapshot with -push-remote flag set to true.
	snapshotDirFlag = snapshotDir
	pushRemoteFlag = true
	if err := runSnapshotCreate(fake.X, []string{label}); err != nil {
		t.Fatalf("%v", err)
	}

	// Check that repo has one new commit.
	newCommitCount, err := git.CountCommits("master", "")
	if err != nil {
		t.Fatalf("git.CountCommits(\"master\", \"\") failed: %v", err)
	}
	if got, want := newCommitCount, commitCount+1; got != want {
		t.Errorf("unexpected commit count: got %v want %v", got, want)
	}

	// Check that new label is commited.
	labelFile := filepath.Join(snapshotDir, "labels", label)
	if !git.IsFileCommitted(labelFile) {
		t.Errorf("expected file %v to be committed but it was not", labelFile)
	}
}
Exemplo n.º 8
0
// TestUpdateUniverseRemoteBranch checks that UpdateUniverse can pull from a
// non-master remote branch.
func TestUpdateUniverseRemoteBranch(t *testing.T) {
	localProjects, fake, cleanup := setupUniverse(t)
	defer cleanup()
	s := fake.X.NewSeq()
	if err := fake.UpdateUniverse(false); err != nil {
		t.Fatal(err)
	}

	// Commit to master branch of a project 1.
	writeReadme(t, fake.X, fake.Projects[localProjects[1].Name], "master commit")
	// Create and checkout a new branch of project 1 and make a new commit.
	git := gitutil.New(s, gitutil.RootDirOpt(fake.Projects[localProjects[1].Name]))
	if err := git.CreateAndCheckoutBranch("non-master"); err != nil {
		t.Fatal(err)
	}
	writeReadme(t, fake.X, fake.Projects[localProjects[1].Name], "non-master commit")
	// Point the manifest to the new non-master branch.
	m, err := fake.ReadRemoteManifest()
	if err != nil {
		t.Fatal(err)
	}
	projects := []project.Project{}
	for _, p := range m.Projects {
		if p.Name == localProjects[1].Name {
			p.RemoteBranch = "non-master"
		}
		projects = append(projects, p)
	}
	m.Projects = projects
	if err := fake.WriteRemoteManifest(m); err != nil {
		t.Fatal(err)
	}
	// Check that UpdateUniverse pulls the commit from the non-master branch.
	if err := fake.UpdateUniverse(false); err != nil {
		t.Fatal(err)
	}
	checkReadme(t, fake.X, localProjects[1], "non-master commit")
}
Exemplo n.º 9
0
func TestRunP(t *testing.T) {
	fake, cleanup := jiritest.NewFakeJiriRoot(t)
	defer cleanup()
	projects := addProjects(t, fake)
	dir, sh := buildJiri(t), gosh.NewShell(t)

	if got, want := len(projects), 5; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	cwd, err := os.Getwd()
	if err != nil {
		t.Fatal(err)
	}
	defer os.Chdir(cwd)

	chdir := func(dir string) {
		if err := os.Chdir(dir); err != nil {
			t.Fatal(err)
		}
	}

	manifestKey := strings.Replace(string(projects[0].Key()), "r.a", "manifest", -1)
	keys := []string{manifestKey}
	for _, p := range projects {
		keys = append(keys, string(p.Key()))
	}

	chdir(projects[0].Path)

	got := run(sh, dir, "jiri", "runp", "--show-name-prefix", "-v", "echo")
	hdr := "Project Names: manifest r.a r.b r.c r.t1 r.t2\n"
	hdr += "Project Keys: " + strings.Join(keys, " ") + "\n"

	if want := hdr + "manifest: \nr.a: \nr.b: \nr.c: \nr.t1: \nr.t2:"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	got = run(sh, dir, "jiri", "runp", "-v", "--interactive=false", "basename", "$(", "jiri", "project", "info", "-f", "{{.Project.Path}}", ")")
	if want := hdr + "manifest\nr.a\nr.b\nr.c\nr.t1\nr.t2"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	got = run(sh, dir, "jiri", "runp", "--interactive=false", "git", "rev-parse", "--abbrev-ref", "HEAD")
	if want := "master\nmaster\nmaster\nmaster\nmaster\nmaster"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	got = run(sh, dir, "jiri", "runp", "-interactive=false", "--show-name-prefix=true", "git", "rev-parse", "--abbrev-ref", "HEAD")
	if want := "manifest: master\nr.a: master\nr.b: master\nr.c: master\nr.t1: master\nr.t2: master"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	got = run(sh, dir, "jiri", "runp", "--interactive=false", "--show-key-prefix=true", "git", "rev-parse", "--abbrev-ref", "HEAD")
	if want := strings.Join(keys, ": master\n") + ": master"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	uncollated := run(sh, dir, "jiri", "runp", "--interactive=false", "--collate-stdout=false", "--show-name-prefix=true", "git", "rev-parse", "--abbrev-ref", "HEAD")
	split := strings.Split(uncollated, "\n")
	sort.Strings(split)
	got = strings.TrimSpace(strings.Join(split, "\n"))
	if want := "manifest: master\nr.a: master\nr.b: master\nr.c: master\nr.t1: master\nr.t2: master"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	got = run(sh, dir, "jiri", "runp", "--show-name-prefix", "--projects=r.t[12]", "echo")
	if want := "r.t1: \nr.t2:"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	rb := projects[1].Path
	rc := projects[2].Path
	t1 := projects[3].Path

	s := fake.X.NewSeq()
	newfile := func(dir, file string) {
		testfile := filepath.Join(dir, file)
		_, err := s.Create(testfile)
		if err != nil {
			t.Errorf("failed to create %s: %v", testfile, err)
		}
	}

	git := func(dir string) *gitutil.Git {
		return gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(dir))
	}

	newfile(rb, "untracked.go")

	got = run(sh, dir, "jiri", "runp", "--has-untracked", "--show-name-prefix", "echo")
	if want := "r.b:"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	got = run(sh, dir, "jiri", "runp", "--has-untracked=false", "--show-name-prefix", "echo")
	if want := "manifest: \nr.a: \nr.c: \nr.t1: \nr.t2:"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	newfile(rc, "uncommitted.go")

	if err := git(rc).Add("uncommitted.go"); err != nil {
		t.Error(err)
	}

	got = run(sh, dir, "jiri", "runp", "--has-uncommitted", "--show-name-prefix", "echo")
	if want := "r.c:"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	got = run(sh, dir, "jiri", "runp", "--has-uncommitted=false", "--show-name-prefix", "echo")
	if want := "manifest: \nr.a: \nr.b: \nr.t1: \nr.t2:"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	// test ordering of has-<x> flags
	newfile(rc, "untracked.go")
	got = run(sh, dir, "jiri", "runp", "--has-untracked", "--has-uncommitted", "--show-name-prefix", "echo")
	if want := "r.c:"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	got = run(sh, dir, "jiri", "runp", "--has-uncommitted", "--has-untracked", "--show-name-prefix", "echo")
	if want := "r.c:"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	git(rb).CreateAndCheckoutBranch("a1")
	git(rb).CreateAndCheckoutBranch("b2")
	git(rc).CreateAndCheckoutBranch("b2")
	git(t1).CreateAndCheckoutBranch("a1")

	chdir(rc)

	// Just the projects with branch b2.
	got = run(sh, dir, "jiri", "runp", "--show-name-prefix", "echo")
	if want := "r.b: \nr.c:"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	// All projects since --projects takes precendence over branches.
	got = run(sh, dir, "jiri", "runp", "--projects=.*", "--show-name-prefix", "echo")
	if want := "manifest: \nr.a: \nr.b: \nr.c: \nr.t1: \nr.t2:"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	if err := s.MkdirAll(filepath.Join(rb, ".jiri", "a1"), os.FileMode(0755)).Done(); err != nil {
		t.Fatal(err)
	}
	newfile(rb, filepath.Join(".jiri", "a1", ".gerrit_commit_message"))

	git(rb).CheckoutBranch("a1")
	git(t1).CheckoutBranch("a1")
	chdir(t1)

	got = run(sh, dir, "jiri", "runp", "--has-gerrit-message", "--show-name-prefix", "echo")
	if want := "r.b:"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

	got = run(sh, dir, "jiri", "runp", "--has-gerrit-message=false", "--show-name-prefix", "echo")
	if want := "r.t1:"; got != want {
		t.Errorf("got %v, want %v", got, want)
	}

}
Exemplo n.º 10
0
// TestLocalProjects tests the behavior of the LocalProjects method with
// different ScanModes.
func TestLocalProjects(t *testing.T) {
	jirix, cleanup := jiritest.NewX(t)
	defer cleanup()

	// Create some projects.
	numProjects, projectPaths := 3, []string{}
	for i := 0; i < numProjects; i++ {
		s := jirix.NewSeq()
		name := projectName(i)
		path := filepath.Join(jirix.Root, name)
		if err := s.MkdirAll(path, 0755).Done(); err != nil {
			t.Fatal(err)
		}

		// Initialize empty git repository.  The commit is necessary, otherwise
		// "git rev-parse master" fails.
		git := gitutil.New(s, gitutil.RootDirOpt(path))
		if err := git.Init(path); err != nil {
			t.Fatal(err)
		}
		if err := git.Commit(); err != nil {
			t.Fatal(err)
		}

		// Write project metadata.
		p := project.Project{
			Path: path,
			Name: name,
		}
		if err := project.InternalWriteMetadata(jirix, p, path); err != nil {
			t.Fatalf("writeMetadata %v %v) failed: %v\n", p, path, err)
		}
		projectPaths = append(projectPaths, path)
	}

	// Create a latest update snapshot but only tell it about the first project.
	manifest := project.Manifest{
		Projects: []project.Project{
			{
				Name: projectName(0),
				Path: projectPaths[0],
			},
		},
	}
	if err := jirix.NewSeq().MkdirAll(jirix.UpdateHistoryDir(), 0755).Done(); err != nil {
		t.Fatalf("MkdirAll(%v) failed: %v", jirix.UpdateHistoryDir(), err)
	}
	if err := manifest.ToFile(jirix, jirix.UpdateHistoryLatestLink()); err != nil {
		t.Fatalf("manifest.ToFile(%v) failed: %v", jirix.UpdateHistoryLatestLink(), err)
	}

	// LocalProjects with scanMode = FastScan should only find the first
	// project.
	foundProjects, err := project.LocalProjects(jirix, project.FastScan)
	if err != nil {
		t.Fatalf("LocalProjects(%v) failed: %v", project.FastScan, err)
	}
	checkProjectsMatchPaths(t, foundProjects, projectPaths[:1])

	// LocalProjects with scanMode = FullScan should find all projects.
	foundProjects, err = project.LocalProjects(jirix, project.FullScan)
	if err != nil {
		t.Fatalf("LocalProjects(%v) failed: %v", project.FastScan, err)
	}
	checkProjectsMatchPaths(t, foundProjects, projectPaths[:])

	// Check that deleting a project forces LocalProjects to run a full scan,
	// even if FastScan is specified.
	if err := jirix.NewSeq().RemoveAll(projectPaths[0]).Done(); err != nil {
		t.Fatalf("RemoveAll(%v) failed: %v", projectPaths[0])
	}
	foundProjects, err = project.LocalProjects(jirix, project.FastScan)
	if err != nil {
		t.Fatalf("LocalProjects(%v) failed: %v", project.FastScan, err)
	}
	checkProjectsMatchPaths(t, foundProjects, projectPaths[1:])
}
Exemplo n.º 11
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])
}