v.io/jiri: extend cl mail to handle multi part CLs and...
- cl mail now handles multi part CLs, it uses a file in
the jiri per-branch metadata dir to store the message for
each repo and then uses jiri runp to run itself in
each repo. Two flags are added to make this work:
--current-project-only disables the multi-part behaviour
--initial-commit-file
- cl mail now ignores error messages for pushing to gerrit
with no new changes..
MultiPart: 1/2
Change-Id: I1424c0a9f21226cdf45991e61dfa922f4291968e
diff --git a/cmd/jiri/cl.go b/cmd/jiri/cl.go
index 2ddc965..023ab18 100644
--- a/cmd/jiri/cl.go
+++ b/cmd/jiri/cl.go
@@ -6,10 +6,13 @@
import (
"fmt"
+ "io"
+ "io/ioutil"
"net/url"
"os"
"path/filepath"
"regexp"
+ "sort"
"strings"
"v.io/jiri"
@@ -22,25 +25,29 @@
)
const (
- commitMessageFileName = ".gerrit_commit_message"
- dependencyPathFileName = ".dependency_path"
+ commitMessageFileName = ".gerrit_commit_message"
+ dependencyPathFileName = ".dependency_path"
+ multiPartMetaDataFileName = "multipart_index"
)
var (
- autosubmitFlag bool
- ccsFlag string
- draftFlag bool
- editFlag bool
- forceFlag bool
- hostFlag string
- messageFlag string
- presubmitFlag string
- remoteBranchFlag string
- reviewersFlag string
- setTopicFlag bool
- topicFlag string
- uncommittedFlag bool
- verifyFlag bool
+ autosubmitFlag bool
+ ccsFlag string
+ draftFlag bool
+ editFlag bool
+ forceFlag bool
+ hostFlag string
+ messageFlag string
+ commitMessageBodyFlag string
+ presubmitFlag string
+ remoteBranchFlag string
+ reviewersFlag string
+ setTopicFlag bool
+ topicFlag string
+ uncommittedFlag bool
+ verifyFlag bool
+ currentProjectFlag bool
+ cleanupMultiPartFlag bool
)
// Special labels stored in the commit message.
@@ -51,9 +58,14 @@
// Change-Ids start with 'I' and are followed by 40 characters of hex.
changeIDRE *regexp.Regexp = regexp.MustCompile("Change-Id: (I[0123456789abcdefABCDEF]{40})")
+ // MultiPart messages are of the form: MultiPart: <n>/<m>
+ multiPartRE *regexp.Regexp = regexp.MustCompile(`(?m)^MultiPart: \d+/\d+$`)
+
// Presubmit test label.
// PresubmitTest: <type>
presubmitTestLabelRE *regexp.Regexp = regexp.MustCompile(`PresubmitTest:\s*(.*)`)
+
+ noChangesRE *regexp.Regexp = regexp.MustCompile(`! \[remote rejected\] HEAD -> refs/(for|drafts)/\S+ \(no new changes\)`)
)
// init carries out the package initialization.
@@ -66,6 +78,7 @@
cmdCLMail.Flags.BoolVar(&editFlag, "edit", true, `Open an editor to edit the CL description.`)
cmdCLMail.Flags.StringVar(&hostFlag, "host", "", `Gerrit host to use. Defaults to gerrit host specified in manifest.`)
cmdCLMail.Flags.StringVar(&messageFlag, "m", "", `CL description.`)
+ cmdCLMail.Flags.StringVar(&commitMessageBodyFlag, "commit-message-body-file", "", `file containing the body of the CL description, that is, text without a ChangeID, MultiPart etc.`)
cmdCLMail.Flags.StringVar(&presubmitFlag, "presubmit", string(gerrit.PresubmitTestTypeAll),
fmt.Sprintf("The type of presubmit tests to run. Valid values: %s.", strings.Join(gerrit.PresubmitTestTypes(), ",")))
cmdCLMail.Flags.StringVar(&remoteBranchFlag, "remote-branch", "master", `Name of the remote branch the CL pertains to, without the leading "origin/".`)
@@ -74,6 +87,8 @@
cmdCLMail.Flags.StringVar(&topicFlag, "topic", "", `CL topic, defaults to <username>-<branchname>.`)
cmdCLMail.Flags.BoolVar(&uncommittedFlag, "check-uncommitted", true, `Check that no uncommitted changes exist.`)
cmdCLMail.Flags.BoolVar(&verifyFlag, "verify", true, `Run pre-push git hooks.`)
+ cmdCLMail.Flags.BoolVar(¤tProjectFlag, "current-project-only", false, `Run mail in the current project only.`)
+ 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.`)
cmdCLSync.Flags.StringVar(&remoteBranchFlag, "remote-branch", "master", `Name of the remote branch the CL pertains to, without the leading "origin/".`)
}
@@ -116,8 +131,8 @@
// cmdCL represents the "jiri cl" command.
var cmdCL = &cmdline.Command{
Name: "cl",
- Short: "Manage project changelists",
- Long: "Manage project changelists.",
+ Short: "Manage changelists for multiple projects",
+ Long: "Manage changelists for multiple projects.",
Children: []*cmdline.Command{cmdCLCleanup, cmdCLMail, cmdCLNew, cmdCLSync},
}
@@ -335,19 +350,15 @@
// currentProject returns the Project containing the current working directory.
// The current working directory must be inside JIRI_ROOT.
-func currentProject(jirix *jiri.X, cmd string) (project.Project, error) {
+func currentProject(jirix *jiri.X) (project.Project, error) {
dir, err := os.Getwd()
if err != nil {
return project.Project{}, fmt.Errorf("os.Getwd() failed: %v", err)
}
- // Error if current working dir is not inside jirix.Root.
- if !strings.HasPrefix(dir, jirix.Root) {
- return project.Project{}, fmt.Errorf("'%s' must be run from within a project in JIRI_ROOT", cmd)
- }
-
// Walk up the path until we find a project at that path, or hit the jirix.Root.
- for dir != jirix.Root {
+ // Note that we can't just compare path prefixes because of soft links.
+ for dir != jirix.Root && dir != string(filepath.Separator) {
p, err := project.ProjectAtPath(jirix, dir)
if err != nil {
dir = filepath.Dir(dir)
@@ -358,8 +369,209 @@
return project.Project{}, fmt.Errorf("directory %q is not contained in a project", dir)
}
-// runCLMail is a wrapper that sets up and runs a review instance.
-func runCLMail(jirix *jiri.X, _ []string) error {
+type multiPart struct {
+ clean, current bool
+ currentKey project.ProjectKey
+ states map[project.ProjectKey]*project.ProjectState
+ keys project.ProjectKeys
+}
+
+// initForMultiPart determines the actions to be taken
+// based on command line flags and project state.
+func initForMultiPart(jirix *jiri.X) (*multiPart, error) {
+ mp := &multiPart{}
+ mp.clean = cleanupMultiPartFlag
+ if currentProjectFlag {
+ mp.current = true
+ return mp, nil
+ }
+ if mp.clean {
+ states, keys, err := projectStates(jirix, true)
+ if err != nil {
+ return nil, err
+ }
+ mp.states = states
+ mp.keys = keys
+ return mp, nil
+ }
+ states, keys, err := projectStates(jirix, false)
+ if err != nil {
+ return nil, err
+ }
+ current, err := currentProject(jirix)
+ if err != nil {
+ return nil, err
+ }
+ mp.currentKey = current.Key()
+ if len(keys) == 1 {
+ filename := filepath.Join(states[keys[0]].Project.Path, jiri.ProjectMetaDir, multiPartMetaDataFileName)
+ os.Remove(filename)
+ if mp.currentKey == states[keys[0]].Project.Key() {
+ mp.current = true
+ return mp, nil
+ }
+ }
+ mp.states = states
+ mp.keys = keys
+ return mp, nil
+}
+
+// projectStates returns a map with all projects that are on the same
+// current branch as the current project, as well as a slice of their
+// project keys sorted lexicographically. Unless "allowdirty" is true,
+// an error is returned if any matching project has uncommitted changes.
+// The keys are returned, sorted, to avoid the caller having to recreate
+// the them by iterating over the map.
+func projectStates(jirix *jiri.X, allowdirty bool) (map[project.ProjectKey]*project.ProjectState, project.ProjectKeys, error) {
+ git := gitutil.New(jirix.NewSeq())
+ branch, err := git.CurrentBranchName()
+ if err != nil {
+ return nil, nil, err
+ }
+ states, err := project.GetProjectStates(jirix, false)
+ if err != nil {
+ return nil, nil, err
+ }
+ uncommitted := []string{}
+ var keys project.ProjectKeys
+ for _, s := range states {
+ if s.CurrentBranch == branch {
+ key := s.Project.Key()
+ fullState, err := project.GetProjectState(jirix, key, true)
+ if err != nil {
+ return nil, nil, err
+ }
+ if !allowdirty && fullState.HasUncommitted {
+ uncommitted = append(uncommitted, string(key))
+ } else {
+ keys = append(keys, key)
+ }
+ }
+ }
+ if len(uncommitted) > 0 {
+ return nil, nil, fmt.Errorf("the following projects have uncommitted changes: %s", strings.Join(uncommitted, ", "))
+ }
+ members := map[project.ProjectKey]*project.ProjectState{}
+ for _, key := range keys {
+ members[key] = states[key]
+ }
+ if len(members) == 0 {
+ return nil, nil, nil
+ }
+ sort.Sort(keys)
+ return members, keys, nil
+}
+
+func (mp *multiPart) writeMultiPartMetadata(jirix *jiri.X) error {
+ total := len(mp.states)
+ index := 1
+ s := jirix.NewSeq()
+ for _, key := range mp.keys {
+ state := mp.states[key]
+ filename := filepath.Join(state.Project.Path, jiri.ProjectMetaDir, multiPartMetaDataFileName)
+ if total < 2 {
+ os.Remove(filename)
+ continue
+ }
+ msg := fmt.Sprintf("MultiPart: %d/%d\n", index, total)
+ if err := s.WriteFile(filename, []byte(msg), os.FileMode(0644)).Done(); err != nil {
+ return err
+ }
+ index++
+ }
+ return nil
+}
+
+func (mp *multiPart) cleanMultiPartMetadata(jirix *jiri.X) error {
+ s := jirix.NewSeq()
+ for _, state := range mp.states {
+ filename := filepath.Join(state.Project.Path, jiri.ProjectMetaDir, multiPartMetaDataFileName)
+ ok, err := s.IsFile(filename)
+ if err != nil {
+ return err
+ }
+ if ok {
+ if err := s.Remove(filename).Done(); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func (mp *multiPart) commandline(excludeKey project.ProjectKey, args []string) []string {
+ keyflag := "--projects="
+ for _, k := range mp.keys {
+ if k == excludeKey {
+ continue
+ }
+ keyflag += string(k) + ","
+ }
+ keyflag = strings.TrimSuffix(keyflag, ",")
+ clargs := []string{
+ "runp",
+ "--interactive",
+ keyflag,
+ "jiri",
+ "cl",
+ "mail",
+ "--current-project-only=true",
+ }
+ return append(clargs, args...)
+}
+
+// runCLMail is a wrapper that sets up and runs a review instance across
+// multiple projects.
+func runCLMail(jirix *jiri.X, args []string) error {
+ mp, err := initForMultiPart(jirix)
+ if err != nil {
+ return err
+ }
+ if mp.clean {
+ if err := mp.cleanMultiPartMetadata(jirix); err != nil {
+ return err
+ }
+ return nil
+ }
+ if mp.current {
+ return runCLMailCurrent(jirix, []string{})
+ }
+ // multipart mode
+ if err := mp.writeMultiPartMetadata(jirix); err != nil {
+ mp.cleanMultiPartMetadata(jirix)
+ return err
+ }
+ if err := runCLMailCurrent(jirix, []string{}); err != nil {
+ return err
+ }
+ git := gitutil.New(jirix.NewSeq())
+ branch, err := git.CurrentBranchName()
+ if err != nil {
+ return err
+ }
+ initialMessage, err := strippedGerritCommitMessage(jirix, branch)
+ if err != nil {
+ return err
+ }
+ s := jirix.NewSeq()
+ tmp, err := s.TempFile("", branch+"-")
+ if err != nil {
+ return err
+ }
+ defer func() {
+ tmp.Close()
+ os.Remove(tmp.Name())
+ }()
+ if _, err := io.WriteString(tmp, initialMessage); err != nil {
+ return err
+ }
+ // Use Capture to make sure that all output from the subcommands is
+ // sent to stdout/stderr.
+ return s.Capture(jirix.Stdout(), jirix.Stderr()).Last("jiri", mp.commandline(mp.currentKey, append([]string{"--edit=false", "--commit-message-body-file=" + tmp.Name()}, args...))...)
+
+}
+
+func runCLMailCurrent(jirix *jiri.X, _ []string) error {
// Check that working dir exist on remote branch. Otherwise checking out
// remote branch will break the users working dir.
git := gitutil.New(jirix.NewSeq())
@@ -385,7 +597,7 @@
strings.Join(gerrit.PresubmitTestTypes(), ","))
}
- p, err := currentProject(jirix, "jiri cl mail")
+ p, err := currentProject(jirix)
if err != nil {
return err
}
@@ -409,7 +621,7 @@
gerritRemote.Path = projectRemoteUrl.Path
// Create and run the review.
- review, err := newReview(jirix, gerrit.CLOpts{
+ review, err := newReview(jirix, p, gerrit.CLOpts{
Autosubmit: autosubmitFlag,
Ccs: parseEmails(ccsFlag),
Draft: draftFlag,
@@ -429,7 +641,13 @@
} else if !confirmed {
return nil
}
- return review.run()
+ err = review.run()
+ // Ignore the error that is returned when there are no differences
+ // between the local and gerrit branches.
+ if err != nil && noChangesRE.MatchString(err.Error()) {
+ return nil
+ }
+ return err
}
// parseEmails input a list of comma separated tokens and outputs a
@@ -488,10 +706,11 @@
type review struct {
jirix *jiri.X
reviewBranch string
+ project project.Project
gerrit.CLOpts
}
-func newReview(jirix *jiri.X, opts gerrit.CLOpts) (*review, error) {
+func newReview(jirix *jiri.X, project project.Project, opts gerrit.CLOpts) (*review, error) {
// Sync all CLs in the sequence of dependent CLs ending in the
// current branch.
if err := syncCL(jirix); err != nil {
@@ -524,6 +743,7 @@
}
return &review{
jirix: jirix,
+ project: project,
reviewBranch: branch + "-REVIEW",
CLOpts: opts,
}, nil
@@ -750,18 +970,59 @@
return nil
}
-// defaultCommitMessage creates the default commit message from the
-// list of commits on the branch.
-func (review *review) defaultCommitMessage() (string, error) {
- commitMessages, err := gitutil.New(review.jirix.NewSeq()).CommitMessages(review.CLOpts.Branch, review.reviewBranch)
+func (review *review) readMultiPart() string {
+ s := review.jirix.NewSeq()
+ filename := filepath.Join(review.project.Path, jiri.ProjectMetaDir, multiPartMetaDataFileName)
+ mpart, err := s.ReadFile(filename)
+ if err != nil {
+ return ""
+ }
+ return strings.TrimSpace(string(mpart))
+}
+
+// strippedGerritCommitMessage returns the commit message stripped of variable
+// meta-data such as multipart messages, or change IDs:
+func strippedGerritCommitMessage(jirix *jiri.X, branch string) (string, error) {
+ filename, err := getCommitMessageFileName(jirix, branch)
if err != nil {
return "", err
}
+ msg, err := jirix.NewSeq().ReadFile(filename)
+ if err != nil {
+ return "", err
+ }
+ // Strip "MultiPart ..." from the commit messages.
+ strippedMessages := multiPartRE.ReplaceAllLiteralString(string(msg), "")
// Strip "Change-Id: ..." from the commit messages.
- strippedMessages := changeIDRE.ReplaceAllLiteralString(commitMessages, "")
+ strippedMessages = changeIDRE.ReplaceAllLiteralString(strippedMessages, "")
+ return strippedMessages, nil
+}
+
+// defaultCommitMessage creates the default commit message from the
+// list of commits on the branch.
+func (review *review) defaultCommitMessage() (string, error) {
+ commitMessages := ""
+ var err error
+ if commitMessageBodyFlag != "" {
+ msg, tmpErr := ioutil.ReadFile(commitMessageBodyFlag)
+ commitMessages = string(msg)
+ err = tmpErr
+ } else {
+ commitMessages, err = gitutil.New(review.jirix.NewSeq()).CommitMessages(review.CLOpts.Branch, review.reviewBranch)
+ }
+ if err != nil {
+ return "", err
+ }
+ // Strip "MultiPart ..." from the commit messages.
+ strippedMessages := multiPartRE.ReplaceAllLiteralString(commitMessages, "")
+ // Strip "Change-Id: ..." from the commit messages.
+ strippedMessages = changeIDRE.ReplaceAllLiteralString(strippedMessages, "")
// Add comment markers (#) to every line.
commentedMessages := "# " + strings.Replace(strippedMessages, "\n", "\n# ", -1)
message := defaultMessageHeader + commentedMessages
+ if multipart := review.readMultiPart(); multipart != "" {
+ message = message + "\n" + multipart + "\n"
+ }
return message, nil
}
@@ -779,15 +1040,24 @@
return nil
}
-// processLabels adds/removes labels for the given commit message.
-func (review *review) processLabels(message string) string {
- // Find the Change-ID line.
+// processLabelsAndCommitFile adds/removes labels for the given commit
+// message and merges in the contents of the initial-message-file.
+func (review *review) processLabelsAndCommitFile(message string) string {
+ // Find the Change-ID and MultiPart lines.
changeIDLine := changeIDRE.FindString(message)
+ multiPartLine := multiPartRE.FindString(message)
+
+ if commitMessageBodyFlag != "" {
+ if msg, err := ioutil.ReadFile(commitMessageBodyFlag); err == nil {
+ message = string(msg)
+ }
+ }
// Strip existing labels and change-ID.
message = autosubmitLabelRE.ReplaceAllLiteralString(message, "")
message = presubmitTestLabelRE.ReplaceAllLiteralString(message, "")
message = changeIDRE.ReplaceAllLiteralString(message, "")
+ message = multiPartRE.ReplaceAllLiteralString(message, "")
// Insert labels and change-ID back.
if review.CLOpts.Autosubmit {
@@ -796,11 +1066,21 @@
if review.CLOpts.Presubmit != gerrit.PresubmitTestTypeAll {
message += fmt.Sprintf("PresubmitTest: %s\n", review.CLOpts.Presubmit)
}
+ if multiPartLine != "" && !strings.HasSuffix(message, "\n") {
+ message += "\n"
+ } else {
+ if multipart := review.readMultiPart(); multipart != "" {
+ if !strings.HasSuffix(message, "\n") {
+ message += "\n"
+ }
+ multiPartLine = multipart
+ }
+ }
+ message += multiPartLine
if changeIDLine != "" && !strings.HasSuffix(message, "\n") {
message += "\n"
}
message += changeIDLine
-
return message
}
@@ -838,10 +1118,12 @@
return err
}
defer collect.Error(func() error { return review.jirix.NewSeq().Chdir(wd).Done() }, &e)
+
file, err := getCommitMessageFileName(review.jirix, review.CLOpts.Branch)
if err != nil {
return err
}
+
message := messageFlag
if message == "" {
// Message was not passed in flag. Attempt to read it from file.
@@ -863,7 +1145,7 @@
// message is edited by users, which happens in the
// updateReviewMessage method.
if message != "" {
- message = review.processLabels(message)
+ message = review.processLabelsAndCommitFile(message)
}
if err := review.createReviewBranch(message); err != nil {
return err
@@ -893,7 +1175,7 @@
return nil
}
-// getChangeID reads the commit message and extracts the change-Id.
+// getChangeID reads the commit message and extracts the change-Id
func (review *review) getChangeID() (string, error) {
file, err := getCommitMessageFileName(review.jirix, review.CLOpts.Branch)
if err != nil {
@@ -922,7 +1204,7 @@
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())
}
if err := review.jirix.Gerrit(host).SetTopic(changeID, review.CLOpts); err != nil {
- return err
+ return fmt.Errorf("failed to set topic for %v, %#v: %v", changeID, review.CLOpts, err)
}
return nil
}
@@ -937,6 +1219,9 @@
if err != nil {
return err
}
+ // update MultiPart metadata.
+ mpart := review.readMultiPart()
+ newMessage = multiPartRE.ReplaceAllLiteralString(newMessage, mpart)
s := review.jirix.NewSeq()
// For the initial commit where the commit message file doesn't exist,
// add/remove labels after users finish editing the commit message.
@@ -945,7 +1230,7 @@
// initial commit so we don't confuse users.
if _, err := s.Stat(file); err != nil {
if runutil.IsNotExist(err) {
- newMessage = review.processLabels(newMessage)
+ newMessage = review.processLabelsAndCommitFile(newMessage)
if err := git.CommitAmendWithMessage(newMessage); err != nil {
return err
}
diff --git a/cmd/jiri/cl_test.go b/cmd/jiri/cl_test.go
index 680f0ff..86da915 100644
--- a/cmd/jiri/cl_test.go
+++ b/cmd/jiri/cl_test.go
@@ -6,9 +6,12 @@
import (
"bytes"
+ "fmt"
+ "io/ioutil"
"os"
"path"
"path/filepath"
+ "reflect"
"runtime"
"strings"
"testing"
@@ -17,6 +20,7 @@
"v.io/jiri/gerrit"
"v.io/jiri/gitutil"
"v.io/jiri/jiritest"
+ "v.io/jiri/project"
"v.io/jiri/runutil"
)
@@ -155,10 +159,13 @@
return repoPath
}
-// Simple commit-msg hook that adds a fake Change Id.
+// Simple commit-msg hook that removes any existing Change-Id and adds a
+// fake one.
var commitMsgHook string = `#!/bin/sh
MSG="$1"
-echo "Change-Id: I0000000000000000000000000000000000000000" >> $MSG
+cat $MSG | sed -e "/Change-Id/d" > $MSG.tmp
+echo "Change-Id: I0000000000000000000000000000000000000000" >> $MSG.tmp
+mv $MSG.tmp $MSG
`
// installCommitMsgHook links the gerrit commit-msg hook into a different repo.
@@ -314,7 +321,7 @@
}
files := []string{"file1", "file2", "file3"}
commitFiles(t, fake.X, files)
- review, err := newReview(fake.X, gerrit.CLOpts{})
+ review, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{})
if err != nil {
t.Fatalf("%v", err)
}
@@ -346,7 +353,7 @@
if err := gitutil.New(fake.X.NewSeq()).CreateAndCheckoutBranch(branch); err != nil {
t.Fatalf("%v", err)
}
- review, err := newReview(fake.X, gerrit.CLOpts{Remote: branch})
+ review, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{Remote: branch})
if err != nil {
t.Fatalf("%v", err)
}
@@ -372,7 +379,7 @@
commitFiles(t, fake.X, files)
{
// Test with draft = false, no reviewiers, and no ccs.
- review, err := newReview(fake.X, gerrit.CLOpts{Remote: gerritPath})
+ review, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{Remote: gerritPath})
if err != nil {
t.Fatalf("%v", err)
}
@@ -384,7 +391,7 @@
}
{
// Test with draft = true, no reviewers, and no ccs.
- review, err := newReview(fake.X, gerrit.CLOpts{
+ review, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{
Draft: true,
Remote: gerritPath,
})
@@ -399,7 +406,7 @@
}
{
// Test with draft = false, reviewers, and no ccs.
- review, err := newReview(fake.X, gerrit.CLOpts{
+ review, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{
Remote: gerritPath,
Reviewers: parseEmails("reviewer1,reviewer2@example.org"),
})
@@ -414,7 +421,7 @@
}
{
// Test with draft = true, reviewers, and ccs.
- review, err := newReview(fake.X, gerrit.CLOpts{
+ review, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{
Ccs: parseEmails("cc1@example.org,cc2"),
Draft: true,
Remote: gerritPath,
@@ -442,7 +449,7 @@
t.Fatalf("%v", err)
}
commitFiles(t, fake.X, []string{"file1"})
- review, err := newReview(fake.X, gerrit.CLOpts{Remote: gerritPath})
+ review, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{Remote: gerritPath})
if err != nil {
t.Fatalf("%v", err)
}
@@ -465,7 +472,7 @@
}
files := []string{"file1", "file2", "file3"}
commitFiles(t, fake.X, files)
- review, err := newReview(fake.X, gerrit.CLOpts{Remote: gerritPath})
+ review, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{Remote: gerritPath})
if err != nil {
t.Fatalf("%v", err)
}
@@ -503,7 +510,7 @@
// Test setting -presubmit=none and autosubmit.
files := []string{"file1", "file2", "file3"}
commitFiles(t, fake.X, files)
- review, err := newReview(fake.X, gerrit.CLOpts{
+ review, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{
Autosubmit: true,
Presubmit: gerrit.PresubmitTestTypeNone,
Remote: gerritPath,
@@ -546,7 +553,7 @@
}
// Test setting -presubmit=all but keep autosubmit=true.
- review, err = newReview(fake.X, gerrit.CLOpts{
+ review, err = newReview(fake.X, project.Project{}, gerrit.CLOpts{
Autosubmit: true,
Remote: gerritPath,
Reviewers: parseEmails("run2"),
@@ -575,7 +582,7 @@
}
// Test setting autosubmit=false.
- review, err = newReview(fake.X, gerrit.CLOpts{
+ review, err = newReview(fake.X, project.Project{}, gerrit.CLOpts{
Remote: gerritPath,
Reviewers: parseEmails("run3"),
})
@@ -638,7 +645,7 @@
if err := s.WriteFile(untrackedFile, []byte(untrackedFileContent), 0644).Done(); err != nil {
t.Fatalf("WriteFile(%v, %v) failed: %v", untrackedFile, untrackedFileContent, err)
}
- review, err := newReview(fake.X, gerrit.CLOpts{Remote: gerritPath})
+ review, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{Remote: gerritPath})
if err != nil {
t.Fatalf("%v", err)
}
@@ -684,7 +691,7 @@
files := []string{path.Join(subdir, "file1")}
commitFiles(t, fake.X, files)
chdir(t, fake.X, subdir)
- review, err := newReview(fake.X, gerrit.CLOpts{Remote: gerritPath})
+ review, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{Remote: gerritPath})
if err != nil {
t.Fatalf("%v", err)
}
@@ -787,14 +794,14 @@
},
}
for _, test := range testCases {
- review, err := newReview(fake.X, gerrit.CLOpts{
+ review, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{
Autosubmit: test.autosubmit,
Presubmit: test.presubmitType,
})
if err != nil {
t.Fatalf("%v", err)
}
- if got := review.processLabels(test.originalMessage); got != test.expectedMessage {
+ if got := review.processLabelsAndCommitFile(test.originalMessage); got != test.expectedMessage {
t.Fatalf("want %s, got %s", test.expectedMessage, got)
}
}
@@ -868,7 +875,7 @@
if err := gitutil.New(fake.X.NewSeq()).CommitWithMessage("editing stuff"); err != nil {
t.Fatalf("git commit failed: %v", err)
}
- review, err := newReview(fake.X, gerrit.CLOpts{
+ review, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{
Remote: gerritPath,
Reviewers: parseEmails("run1"), // See hack note about TestLabelsInCommitMessage
})
@@ -889,7 +896,7 @@
if err := gitutil.New(fake.X.NewSeq()).CommitWithMessage("deleting stuff"); err != nil {
t.Fatalf("git commit failed: %v", err)
}
- review, err = newReview(fake.X, gerrit.CLOpts{
+ review, err = newReview(fake.X, project.Project{}, gerrit.CLOpts{
Remote: gerritPath,
Reviewers: parseEmails("run2"),
})
@@ -928,7 +935,7 @@
createCLWithFiles(t, fake.X, "feature1-B", "B")
commitFile(t, fake.X, "A", "Don't tread on me.")
- reviewB, err := newReview(fake.X, gerrit.CLOpts{Remote: gerritBPath})
+ reviewB, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{Remote: gerritBPath})
if err != nil {
t.Fatalf("%v", err)
}
@@ -949,7 +956,7 @@
t.Fatalf("%v", err)
}
- reviewA, err := newReview(fake.X, gerrit.CLOpts{Remote: gerritAPath})
+ reviewA, err := newReview(fake.X, project.Project{}, gerrit.CLOpts{Remote: gerritAPath})
if err == nil {
t.Fatalf("creating a review did not fail when it should")
}
@@ -979,7 +986,7 @@
}
// Retry review.
- reviewA, err = newReview(fake.X, gerrit.CLOpts{Remote: gerritAPath})
+ reviewA, err = newReview(fake.X, project.Project{}, gerrit.CLOpts{Remote: gerritAPath})
if err != nil {
t.Fatalf("review failed: %v", err)
}
@@ -1031,3 +1038,277 @@
assertFilesExist(t, fake.X, []string{"test"})
}
}
+
+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
+
+ 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
+ }
+
+ 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
+
+ git := func(dir string) *gitutil.Git {
+ return gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(dir))
+ }
+
+ // 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()}); !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, projectPaths ...string) {
+ for i, dir := range projectPaths {
+ filename := filepath.Join(dir, jiri.ProjectMetaDir, multiPartMetaDataFileName)
+ msg, err := ioutil.ReadFile(filename)
+ if err != nil {
+ t.Fatal(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(projectPaths ...string) {
+ for _, dir := range projectPaths {
+ filename := filepath.Join(fake.X.Root, dir, jiri.ProjectMetaDir, multiPartMetaDataFileName)
+ _, err := os.Stat(filename)
+ if !os.IsNotExist(err) {
+ t.Fatalf("%s should not exist", 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), ra, rb, rc)
+ hasNoMetaData(t1)
+ if err := mp.cleanMultiPartMetadata(fake.X); err != nil {
+ t.Fatal(err)
+ }
+ hasNoMetaData(ra, 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), 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])
+ return
+
+ cl := mp.commandline("", []string{"jiri", "cl", "mail"})
+ expected := []string{
+ "runp",
+ "--interactive",
+ "--show-key-prefix",
+ "--projects=" + string(projects[0].Key()) + "," + string(projects[1].Key()) + "," + string(projects[2].Key()),
+ "--current-project-only",
+ "jiri",
+ "cl",
+ "mail",
+ }
+ 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{"jiri", "cl", "mail"})
+ expected[3] = "--projects=" + string(projects[1].Key()) + "," + string(projects[2].Key())
+ 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), 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])
+}
diff --git a/cmd/jiri/doc.go b/cmd/jiri/doc.go
index 66c86db..87c2702 100644
--- a/cmd/jiri/doc.go
+++ b/cmd/jiri/doc.go
@@ -12,7 +12,7 @@
jiri [flags] <command>
The jiri commands are:
- cl Manage project changelists
+ cl Manage changelists for multiple projects
import Adds imports to .jiri_manifest file
profile Display information about installed profiles
project Manage the jiri projects
@@ -39,9 +39,9 @@
-time=false
Dump timing information to stderr before exiting the program.
-Jiri cl - Manage project changelists
+Jiri cl - Manage changelists for multiple projects
-Manage project changelists.
+Manage changelists for multiple projects.
Usage:
jiri cl [flags] <command>
@@ -100,6 +100,14 @@
Comma-seperated list of emails or LDAPs to cc.
-check-uncommitted=true
Check that no uncommitted changes exist.
+ -clean-multipart-metadata=false
+ Cleanup the metadata associated with multipart CLs pertaining the MultiPart:
+ x/y message without mailing any CLs.
+ -commit-message-body-file=
+ file containing the body of the CL description, that is, text without a
+ ChangeID, MultiPart etc.
+ -current-project-only=false
+ Run mail in the current project only.
-d=false
Send a draft changelist.
-edit=true
diff --git a/cmd/jiri/runp.go b/cmd/jiri/runp.go
index 8d0eb2b..9708723 100644
--- a/cmd/jiri/runp.go
+++ b/cmd/jiri/runp.go
@@ -312,7 +312,12 @@
var err error
if profilescmdline.IsFlagSet(cmd.ParsedFlags, "projects") {
- keysRE, err = regexp.Compile(runpFlags.projectKeys)
+ re := ""
+ for _, pre := range strings.Split(runpFlags.projectKeys, ",") {
+ re += pre + "|"
+ }
+ re = strings.TrimRight(re, "|")
+ keysRE, err = regexp.Compile(re)
if err != nil {
return fmt.Errorf("failed to compile projects regexp: %q: %v", runpFlags.projectKeys, err)
}
@@ -327,6 +332,9 @@
for _, f := range []string{"show-key-prefix", "show-name-prefix"} {
if profilescmdline.IsFlagSet(cmd.ParsedFlags, f) {
+ if runpFlags.interactive && profilescmdline.IsFlagSet(cmd.ParsedFlags, "interactive") {
+ fmt.Fprintf(jirix.Stderr(), "WARNING: interactive mode being disabled because %s was set\n", f)
+ }
runpFlags.interactive = false
runpFlags.collateOutput = true
break
diff --git a/cmd/jiri/runp_test.go b/cmd/jiri/runp_test.go
index 325bbc8..c1bd932 100644
--- a/cmd/jiri/runp_test.go
+++ b/cmd/jiri/runp_test.go
@@ -48,7 +48,7 @@
}
p := project.Project{
Name: projectPath,
- Path: projectPath,
+ Path: filepath.Join(fake.X.Root, projectPath),
Remote: fake.Projects[projectPath],
RemoteBranch: "master",
}
@@ -97,7 +97,7 @@
defer os.Chdir(cwd)
chdir := func(dir string) {
- if err := os.Chdir(filepath.Join(fake.X.Root, dir)); err != nil {
+ if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
}
@@ -157,15 +157,15 @@
s := fake.X.NewSeq()
newfile := func(dir, file string) {
- testfile := filepath.Join(fake.X.Root, dir, file)
+ testfile := filepath.Join(dir, file)
_, err := s.Create(testfile)
if err != nil {
t.Errorf("failed to create %s: %v", testfile, err)
}
}
- git := func(root, dir string) *gitutil.Git {
- return gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(filepath.Join(fake.X.Root, dir)))
+ git := func(dir string) *gitutil.Git {
+ return gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(dir))
}
newfile(rb, "untracked.go")
@@ -182,7 +182,7 @@
newfile(rc, "uncommitted.go")
- if err := git(fake.X.Root, rc).Add("uncommitted.go"); err != nil {
+ if err := git(rc).Add("uncommitted.go"); err != nil {
t.Error(err)
}
@@ -208,10 +208,10 @@
t.Errorf("got %v, want %v", got, want)
}
- git(fake.X.Root, rb).CreateAndCheckoutBranch("a1")
- git(fake.X.Root, rb).CreateAndCheckoutBranch("b2")
- git(fake.X.Root, rc).CreateAndCheckoutBranch("b2")
- git(fake.X.Root, t1).CreateAndCheckoutBranch("a1")
+ git(rb).CreateAndCheckoutBranch("a1")
+ git(rb).CreateAndCheckoutBranch("b2")
+ git(rc).CreateAndCheckoutBranch("b2")
+ git(t1).CreateAndCheckoutBranch("a1")
chdir(rc)
@@ -227,13 +227,13 @@
t.Errorf("got %v, want %v", got, want)
}
- if err := s.MkdirAll(filepath.Join(fake.X.Root, rb, ".jiri", "a1"), os.FileMode(0755)).Done(); err != nil {
+ 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(fake.X.Root, rb).CheckoutBranch("a1")
- git(fake.X.Root, t1).CheckoutBranch("a1")
+ git(rb).CheckoutBranch("a1")
+ git(t1).CheckoutBranch("a1")
chdir(t1)
got = run(sh, dir, "jiri", "runp", "--has-gerrit-message", "--show-name-prefix", "echo")