github.com/vanadium-archive/go.jiri@v0.0.0-20160715023856-abfb8b131290/cmd/jiri/cl.go (about)

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