v.io/jiri: copy into v.io/jiri/cmd/jiri
Copy the top level command into cmd/jiri. We need to copy since
a separate manifest repo needs to be updated independently of this
change.
Change-Id: I47574a72186454e19fe98f869ef2bbc746ed4566
diff --git a/cmd/jiri/bootstrap_jiri_test.go b/cmd/jiri/bootstrap_jiri_test.go
new file mode 100644
index 0000000..3bdca29
--- /dev/null
+++ b/cmd/jiri/bootstrap_jiri_test.go
@@ -0,0 +1,62 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "v.io/x/lib/gosh"
+)
+
+func TestBootstrapJiri(t *testing.T) {
+ sh := gosh.NewShell(gosh.Opts{Fatalf: t.Fatalf, Logf: t.Logf, PropagateChildOutput: true})
+ defer sh.Cleanup()
+
+ bootstrap, err := filepath.Abs("./scripts/bootstrap_jiri")
+ if err != nil {
+ t.Fatalf("couldn't determine absolute path to bootstrap_jiri script")
+ }
+ rootDir := filepath.Join(sh.MakeTempDir(), "root")
+ stdout, stderr := sh.Cmd(bootstrap, rootDir).StdoutStderr()
+ if got, want := stdout, fmt.Sprintf("Please add %s to your PATH.\n", filepath.Join(rootDir, ".jiri_root", "scripts")); got != want {
+ t.Errorf("stdout got %q, want %q", got, want)
+ }
+ if got, want := stderr, ""; got != want {
+ t.Errorf("stderr got %q, want %q", got, want)
+ }
+ if _, err := os.Stat(filepath.Join(rootDir, ".jiri_root", "bin", "jiri")); err != nil {
+ t.Error(err)
+ }
+ if _, err := os.Stat(filepath.Join(rootDir, ".jiri_root", "scripts", "jiri")); err != nil {
+ t.Error(err)
+ }
+}
+
+func TestBootstrapJiriAlreadyExists(t *testing.T) {
+ sh := gosh.NewShell(gosh.Opts{Fatalf: t.Fatalf, Logf: t.Logf, PropagateChildOutput: true})
+ defer sh.Cleanup()
+
+ bootstrap, err := filepath.Abs("./scripts/bootstrap_jiri")
+ if err != nil {
+ t.Fatalf("couldn't determine absolute path to bootstrap_jiri script")
+ }
+ rootDir := sh.MakeTempDir()
+ c := sh.Cmd(bootstrap, rootDir)
+ c.ExitErrorIsOk = true
+ stdout, stderr := c.StdoutStderr()
+ if c.Err == nil {
+ t.Errorf("error got %q, want nil", c.Err)
+ }
+ if got, want := stdout, ""; got != want {
+ t.Errorf("stdout got %q, want %q", got, want)
+ }
+ if got, want := stderr, rootDir+" already exists"; !strings.Contains(got, want) {
+ t.Errorf("stderr got %q, want substr %q", got, want)
+ }
+}
diff --git a/cmd/jiri/cl.go b/cmd/jiri/cl.go
new file mode 100644
index 0000000..b5a8721
--- /dev/null
+++ b/cmd/jiri/cl.go
@@ -0,0 +1,1120 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "net/url"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "v.io/jiri/collect"
+ "v.io/jiri/gerrit"
+ "v.io/jiri/gitutil"
+ "v.io/jiri/jiri"
+ "v.io/jiri/project"
+ "v.io/jiri/runutil"
+ "v.io/x/lib/cmdline"
+)
+
+const (
+ commitMessageFileName = ".gerrit_commit_message"
+ dependencyPathFileName = ".dependency_path"
+)
+
+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
+)
+
+// Special labels stored in the commit message.
+var (
+ // Auto submit label.
+ autosubmitLabelRE *regexp.Regexp = regexp.MustCompile("AutoSubmit")
+
+ // Change-Ids start with 'I' and are followed by 40 characters of hex.
+ changeIDRE *regexp.Regexp = regexp.MustCompile("Change-Id: (I[0123456789abcdefABCDEF]{40})")
+
+ // Presubmit test label.
+ // PresubmitTest: <type>
+ presubmitTestLabelRE *regexp.Regexp = regexp.MustCompile(`PresubmitTest:\s*(.*)`)
+)
+
+// init carries out the package initialization.
+func init() {
+ cmdCLCleanup.Flags.BoolVar(&forceFlag, "f", false, `Ignore unmerged changes.`)
+ cmdCLCleanup.Flags.StringVar(&remoteBranchFlag, "remote-branch", "master", `Name of the remote branch the CL pertains to, without the leading "origin/".`)
+ cmdCLMail.Flags.BoolVar(&autosubmitFlag, "autosubmit", false, `Automatically submit the changelist when feasible.`)
+ cmdCLMail.Flags.StringVar(&ccsFlag, "cc", "", `Comma-seperated list of emails or LDAPs to cc.`)
+ cmdCLMail.Flags.BoolVar(&draftFlag, "d", false, `Send a draft changelist.`)
+ 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(&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/".`)
+ cmdCLMail.Flags.StringVar(&reviewersFlag, "r", "", `Comma-seperated list of emails or LDAPs to request review.`)
+ cmdCLMail.Flags.BoolVar(&setTopicFlag, "set-topic", true, `Set Gerrit CL topic.`)
+ 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.`)
+ cmdCLSync.Flags.StringVar(&remoteBranchFlag, "remote-branch", "master", `Name of the remote branch the CL pertains to, without the leading "origin/".`)
+}
+
+func getCommitMessageFileName(jirix *jiri.X, branch string) (string, error) {
+ topLevel, err := gitutil.New(jirix.NewSeq()).TopLevel()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(topLevel, jiri.ProjectMetaDir, branch, commitMessageFileName), nil
+}
+
+func getDependencyPathFileName(jirix *jiri.X, branch string) (string, error) {
+ topLevel, err := gitutil.New(jirix.NewSeq()).TopLevel()
+ if err != nil {
+ return "", err
+ }
+ return filepath.Join(topLevel, jiri.ProjectMetaDir, branch, dependencyPathFileName), nil
+}
+
+func getDependentCLs(jirix *jiri.X, branch string) ([]string, error) {
+ file, err := getDependencyPathFileName(jirix, branch)
+ if err != nil {
+ return nil, err
+ }
+ data, err := jirix.NewSeq().ReadFile(file)
+ var branches []string
+ if err != nil {
+ if !runutil.IsNotExist(err) {
+ return nil, err
+ }
+ if branch != remoteBranchFlag {
+ branches = []string{remoteBranchFlag}
+ }
+ } else {
+ branches = strings.Split(strings.TrimSpace(string(data)), "\n")
+ }
+ return branches, nil
+}
+
+// cmdCL represents the "jiri cl" command.
+var cmdCL = &cmdline.Command{
+ Name: "cl",
+ Short: "Manage project changelists",
+ Long: "Manage project changelists.",
+ Children: []*cmdline.Command{cmdCLCleanup, cmdCLMail, cmdCLNew, cmdCLSync},
+}
+
+// cmdCLCleanup represents the "jiri cl cleanup" command.
+//
+// TODO(jsimsa): Replace this with a "submit" command that talks to
+// Gerrit to submit the CL and then (optionally) removes it locally.
+var cmdCLCleanup = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runCLCleanup),
+ Name: "cleanup",
+ Short: "Clean up changelists that have been merged",
+ Long: `
+Command "cleanup" checks that the given branches have been merged into
+the corresponding remote branch. If a branch differs from the
+corresponding remote branch, the command reports the difference and
+stops. Otherwise, it deletes the given branches.
+`,
+ ArgsName: "<branches>",
+ ArgsLong: "<branches> is a list of branches to cleanup.",
+}
+
+func cleanupCL(jirix *jiri.X, branches []string) (e error) {
+ originalBranch, err := gitutil.New(jirix.NewSeq()).CurrentBranchName()
+ if err != nil {
+ return err
+ }
+ stashed, err := gitutil.New(jirix.NewSeq()).Stash()
+ if err != nil {
+ return err
+ }
+ if stashed {
+ defer collect.Error(func() error { return gitutil.New(jirix.NewSeq()).StashPop() }, &e)
+ }
+ if err := gitutil.New(jirix.NewSeq()).CheckoutBranch(remoteBranchFlag); err != nil {
+ return err
+ }
+ checkoutOriginalBranch := true
+ defer collect.Error(func() error {
+ if checkoutOriginalBranch {
+ return gitutil.New(jirix.NewSeq()).CheckoutBranch(originalBranch)
+ }
+ return nil
+ }, &e)
+ if err := gitutil.New(jirix.NewSeq()).FetchRefspec("origin", remoteBranchFlag); err != nil {
+ return err
+ }
+ s := jirix.NewSeq()
+ for _, branch := range branches {
+ cleanupFn := func() error { return cleanupBranch(jirix, branch) }
+ if err := s.Call(cleanupFn, "Cleaning up branch: %s", branch).Done(); err != nil {
+ return err
+ }
+ if branch == originalBranch {
+ checkoutOriginalBranch = false
+ }
+ }
+ return nil
+}
+
+func cleanupBranch(jirix *jiri.X, branch string) error {
+ if err := gitutil.New(jirix.NewSeq()).CheckoutBranch(branch); err != nil {
+ return err
+ }
+ if !forceFlag {
+ trackingBranch := "origin/" + remoteBranchFlag
+ if err := gitutil.New(jirix.NewSeq()).Merge(trackingBranch); err != nil {
+ return err
+ }
+ files, err := gitutil.New(jirix.NewSeq()).ModifiedFiles(trackingBranch, branch)
+ if err != nil {
+ return err
+ }
+ if len(files) != 0 {
+ return fmt.Errorf("unmerged changes in\n%s", strings.Join(files, "\n"))
+ }
+ }
+ if err := gitutil.New(jirix.NewSeq()).CheckoutBranch(remoteBranchFlag); err != nil {
+ return err
+ }
+ if err := gitutil.New(jirix.NewSeq()).DeleteBranch(branch, gitutil.ForceOpt(true)); err != nil {
+ return err
+ }
+ reviewBranch := branch + "-REVIEW"
+ if gitutil.New(jirix.NewSeq()).BranchExists(reviewBranch) {
+ if err := gitutil.New(jirix.NewSeq()).DeleteBranch(reviewBranch, gitutil.ForceOpt(true)); err != nil {
+ return err
+ }
+ }
+ // Delete branch metadata.
+ topLevel, err := gitutil.New(jirix.NewSeq()).TopLevel()
+ if err != nil {
+ return err
+ }
+ s := jirix.NewSeq()
+ // Remove the branch from all dependency paths.
+ metadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir)
+ fileInfos, err := s.RemoveAll(filepath.Join(metadataDir, branch)).
+ ReadDir(metadataDir)
+ if err != nil {
+ return err
+ }
+ for _, fileInfo := range fileInfos {
+ if !fileInfo.IsDir() {
+ continue
+ }
+ file, err := getDependencyPathFileName(jirix, fileInfo.Name())
+ if err != nil {
+ return err
+ }
+ data, err := s.ReadFile(file)
+ if err != nil {
+ if !runutil.IsNotExist(err) {
+ return err
+ }
+ continue
+ }
+ branches := strings.Split(string(data), "\n")
+ for i, tmpBranch := range branches {
+ if branch == tmpBranch {
+ data := []byte(strings.Join(append(branches[:i], branches[i+1:]...), "\n"))
+ if err := s.WriteFile(file, data, os.FileMode(0644)).Done(); err != nil {
+ return err
+ }
+ break
+ }
+ }
+ }
+ return nil
+}
+
+func runCLCleanup(jirix *jiri.X, args []string) error {
+ if len(args) == 0 {
+ return jirix.UsageErrorf("cleanup requires at least one argument")
+ }
+ return cleanupCL(jirix, args)
+}
+
+// cmdCLMail represents the "jiri cl mail" command.
+var cmdCLMail = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runCLMail),
+ Name: "mail",
+ Short: "Mail a changelist for review",
+ Long: `
+Command "mail" squashes all commits of a local branch into a single
+"changelist" and mails this changelist to Gerrit as a single
+commit. First time the command is invoked, it generates a Change-Id
+for the changelist, which is appended to the commit
+message. Consecutive invocations of the command use the same Change-Id
+by default, informing Gerrit that the incomming commit is an update of
+an existing changelist.
+`,
+}
+
+type changeConflictError struct {
+ localBranch string
+ message string
+ remoteBranch string
+}
+
+func (e changeConflictError) Error() string {
+ result := "changelist conflicts with the remote " + e.remoteBranch + " branch\n\n"
+ result += "To resolve this problem, run 'git pull origin " + e.remoteBranch + ":" + e.localBranch + "',\n"
+ result += "resolve the conflicts identified below, and then try again.\n"
+ result += e.message
+ return result
+}
+
+type emptyChangeError struct{}
+
+func (_ emptyChangeError) Error() string {
+ return "current branch has no commits"
+}
+
+type gerritError string
+
+func (e gerritError) Error() string {
+ result := "sending code review failed\n\n"
+ result += string(e)
+ return result
+}
+
+type noChangeIDError struct{}
+
+func (_ noChangeIDError) Error() string {
+ result := "changelist is missing a Change-ID"
+ return result
+}
+
+type uncommittedChangesError []string
+
+func (e uncommittedChangesError) Error() string {
+ result := "uncommitted local changes in files:\n"
+ result += " " + strings.Join(e, "\n ")
+ return result
+}
+
+var defaultMessageHeader = `
+# Describe your changelist, specifying what package(s) your change
+# pertains to, followed by a short summary and, in case of non-trivial
+# changelists, provide a detailed description.
+#
+# For example:
+#
+# rpc/stream/proxy: add publish address
+#
+# The listen address is not always the same as the address that external
+# users need to connect to. This CL adds a new argument to proxy.New()
+# to specify the published address that clients should connect to.
+
+# FYI, you are about to submit the following local commits for review:
+#
+`
+
+// currentProject returns the Project containing the current working directory.
+// The current working directory must be inside JIRI_ROOT.
+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("'jiri cl mail' must be run from within a project in JIRI_ROOT")
+ }
+
+ // Walk up the path until we find a project at that path, or hit the jirix.Root.
+ for dir != jirix.Root {
+ p, err := project.ProjectAtPath(jirix, dir)
+ if err != nil {
+ dir = filepath.Dir(dir)
+ continue
+ }
+ return p, nil
+ }
+ 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 {
+ // Sanity checks for the <presubmitFlag> flag.
+ if !checkPresubmitFlag() {
+ return jirix.UsageErrorf("invalid value for the -presubmit flag. Valid values: %s.",
+ strings.Join(gerrit.PresubmitTestTypes(), ","))
+ }
+
+ p, err := currentProject(jirix)
+ if err != nil {
+ return err
+ }
+
+ host := hostFlag
+ if host == "" {
+ if p.GerritHost == "" {
+ return fmt.Errorf("No gerrit host found. Please use the '--host' flag, or add a 'gerrithost' attribute for project %q.", p.Name)
+ }
+ host = p.GerritHost
+ }
+ hostUrl, err := url.Parse(host)
+ if err != nil {
+ return fmt.Errorf("invalid Gerrit host %q: %v", host, err)
+ }
+ projectRemoteUrl, err := url.Parse(p.Remote)
+ if err != nil {
+ return fmt.Errorf("invalid project remote: %v", p.Remote, err)
+ }
+ gerritRemote := *hostUrl
+ gerritRemote.Path = projectRemoteUrl.Path
+
+ // Create and run the review.
+ review, err := newReview(jirix, gerrit.CLOpts{
+ Autosubmit: autosubmitFlag,
+ Ccs: parseEmails(ccsFlag),
+ Draft: draftFlag,
+ Edit: editFlag,
+ Remote: gerritRemote.String(),
+ Host: hostUrl,
+ Presubmit: gerrit.PresubmitTestType(presubmitFlag),
+ RemoteBranch: remoteBranchFlag,
+ Reviewers: parseEmails(reviewersFlag),
+ Verify: verifyFlag,
+ })
+ if err != nil {
+ return err
+ }
+ if confirmed, err := review.confirmFlagChanges(); err != nil {
+ return err
+ } else if !confirmed {
+ return nil
+ }
+ return review.run()
+}
+
+// parseEmails input a list of comma separated tokens and outputs a
+// list of email addresses. The tokens can either be email addresses
+// or Google LDAPs in which case the suffix @google.com is appended to
+// them to turn them into email addresses.
+func parseEmails(value string) []string {
+ var emails []string
+ tokens := strings.Split(value, ",")
+ for _, token := range tokens {
+ if token == "" {
+ continue
+ }
+ if !strings.Contains(token, "@") {
+ token += "@google.com"
+ }
+ emails = append(emails, token)
+ }
+ return emails
+}
+
+// checkDependents makes sure that all CLs in the sequence of
+// dependent CLs leading to (but not including) the current branch
+// have been exported to Gerrit.
+func checkDependents(jirix *jiri.X) (e error) {
+ originalBranch, err := gitutil.New(jirix.NewSeq()).CurrentBranchName()
+ if err != nil {
+ return err
+ }
+ branches, err := getDependentCLs(jirix, originalBranch)
+ if err != nil {
+ return err
+ }
+ for i := 1; i < len(branches); i++ {
+ file, err := getCommitMessageFileName(jirix, branches[i])
+ if err != nil {
+ return err
+ }
+ if _, err := jirix.NewSeq().Stat(file); err != nil {
+ if !runutil.IsNotExist(err) {
+ return err
+ }
+ return fmt.Errorf(`Failed to export the branch %q to Gerrit because its ancestor %q has not been exported to Gerrit yet.
+The following steps are needed before the operation can be retried:
+$ git checkout %v
+$ jiri cl mail
+$ git checkout %v
+# retry the original command
+`, originalBranch, branches[i], branches[i], originalBranch)
+ }
+ }
+
+ return nil
+}
+
+type review struct {
+ jirix *jiri.X
+ reviewBranch string
+ gerrit.CLOpts
+}
+
+func newReview(jirix *jiri.X, 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 {
+ return nil, err
+ }
+
+ // Make sure that all CLs in the above sequence (possibly except for
+ // the current branch) have been exported to Gerrit. This is needed
+ // to make sure we have commit messages for all but the last CL.
+ //
+ // NOTE: The alternative here is to prompt the user for multiple
+ // commit messages, which seems less user friendly.
+ if err := checkDependents(jirix); err != nil {
+ return nil, err
+ }
+
+ branch, err := gitutil.New(jirix.NewSeq()).CurrentBranchName()
+ if err != nil {
+ return nil, err
+ }
+ opts.Branch = branch
+ if opts.Topic == "" {
+ opts.Topic = fmt.Sprintf("%s-%s", os.Getenv("USER"), branch) // use <username>-<branchname> as the default
+ }
+ if opts.Presubmit == gerrit.PresubmitTestType("") {
+ opts.Presubmit = gerrit.PresubmitTestTypeAll // use gerrit.PresubmitTestTypeAll as the default
+ }
+ if opts.RemoteBranch == "" {
+ opts.RemoteBranch = "master" // use master as the default
+ }
+ return &review{
+ jirix: jirix,
+ reviewBranch: branch + "-REVIEW",
+ CLOpts: opts,
+ }, nil
+}
+
+func checkPresubmitFlag() bool {
+ for _, t := range gerrit.PresubmitTestTypes() {
+ if presubmitFlag == t {
+ return true
+ }
+ }
+ return false
+}
+
+// confirmFlagChanges asks users to confirm if any of the
+// presubmit and autosubmit flags changes.
+func (review *review) confirmFlagChanges() (bool, error) {
+ file, err := getCommitMessageFileName(review.jirix, review.CLOpts.Branch)
+ if err != nil {
+ return false, err
+ }
+ bytes, err := review.jirix.NewSeq().ReadFile(file)
+ if err != nil {
+ if runutil.IsNotExist(err) {
+ return true, nil
+ }
+ return false, err
+ }
+ content := string(bytes)
+ changes := []string{}
+
+ // Check presubmit label change.
+ prevPresubmitType := string(gerrit.PresubmitTestTypeAll)
+ matches := presubmitTestLabelRE.FindStringSubmatch(content)
+ if matches != nil {
+ prevPresubmitType = matches[1]
+ }
+ if presubmitFlag != prevPresubmitType {
+ changes = append(changes, fmt.Sprintf("- presubmit=%s to presubmit=%s", prevPresubmitType, presubmitFlag))
+ }
+
+ // Check autosubmit label change.
+ prevAutosubmit := autosubmitLabelRE.MatchString(content)
+ if autosubmitFlag != prevAutosubmit {
+ changes = append(changes, fmt.Sprintf("- autosubmit=%v to autosubmit=%v", prevAutosubmit, autosubmitFlag))
+
+ }
+
+ if len(changes) > 0 {
+ fmt.Printf("Changes:\n%s\n", strings.Join(changes, "\n"))
+ fmt.Print("Are you sure you want to make the above changes? y/N:")
+ var response string
+ if _, err := fmt.Scanf("%s\n", &response); err != nil || response != "y" {
+ return false, nil
+ }
+ }
+ return true, nil
+}
+
+// cleanup cleans up after the review.
+func (review *review) cleanup(stashed bool) error {
+ if err := gitutil.New(review.jirix.NewSeq()).CheckoutBranch(review.CLOpts.Branch); err != nil {
+ return err
+ }
+ if gitutil.New(review.jirix.NewSeq()).BranchExists(review.reviewBranch) {
+ if err := gitutil.New(review.jirix.NewSeq()).DeleteBranch(review.reviewBranch, gitutil.ForceOpt(true)); err != nil {
+ return err
+ }
+ }
+ if stashed {
+ if err := gitutil.New(review.jirix.NewSeq()).StashPop(); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// createReviewBranch creates a clean review branch from the remote
+// branch this CL pertains to and then iterates over the sequence of
+// dependent CLs leading to the current branch, creating one commit
+// per CL by squashing all commits of each individual CL. The commit
+// message for all but that last CL is derived from their
+// <commitMessageFileName>, while the <message> argument is used as
+// the commit message for the last commit.
+func (review *review) createReviewBranch(message string) (e error) {
+ // Create the review branch.
+ if err := gitutil.New(review.jirix.NewSeq()).FetchRefspec("origin", review.CLOpts.RemoteBranch); err != nil {
+ return err
+ }
+ if gitutil.New(review.jirix.NewSeq()).BranchExists(review.reviewBranch) {
+ if err := gitutil.New(review.jirix.NewSeq()).DeleteBranch(review.reviewBranch, gitutil.ForceOpt(true)); err != nil {
+ return err
+ }
+ }
+ upstream := "origin/" + review.CLOpts.RemoteBranch
+ if err := gitutil.New(review.jirix.NewSeq()).CreateBranchWithUpstream(review.reviewBranch, upstream); err != nil {
+ return err
+ }
+ if err := gitutil.New(review.jirix.NewSeq()).CheckoutBranch(review.reviewBranch); err != nil {
+ return err
+ }
+ // Register a cleanup handler in case of subsequent errors.
+ cleanup := true
+ defer collect.Error(func() error {
+ if !cleanup {
+ return gitutil.New(review.jirix.NewSeq()).CheckoutBranch(review.CLOpts.Branch)
+ }
+ gitutil.New(review.jirix.NewSeq()).CheckoutBranch(review.CLOpts.Branch, gitutil.ForceOpt(true))
+ gitutil.New(review.jirix.NewSeq()).DeleteBranch(review.reviewBranch, gitutil.ForceOpt(true))
+ return nil
+ }, &e)
+
+ // Report an error if the CL is empty.
+ if !review.jirix.DryRun() {
+ hasDiff, err := gitutil.New(review.jirix.NewSeq()).BranchesDiffer(review.CLOpts.Branch, review.reviewBranch)
+ if err != nil {
+ return err
+ }
+ if !hasDiff {
+ return emptyChangeError(struct{}{})
+ }
+ }
+
+ // If <message> is empty, replace it with the default message.
+ if len(message) == 0 {
+ var err error
+ message, err = review.defaultCommitMessage()
+ if err != nil {
+ return err
+ }
+ }
+
+ // Iterate over all dependent CLs leading to (and including) the
+ // current branch, creating one commit in the review branch per CL
+ // by squashing all commits of each individual CL.
+ branches, err := getDependentCLs(review.jirix, review.CLOpts.Branch)
+ if err != nil {
+ return err
+ }
+ branches = append(branches, review.CLOpts.Branch)
+ if err := review.squashBranches(branches, message); err != nil {
+ return err
+ }
+
+ cleanup = false
+ return nil
+}
+
+// squashBranches iterates over the given list of branches, creating
+// one commit per branch in the current branch by squashing all
+// commits of each individual branch.
+//
+// TODO(jsimsa): Consider using "git rebase --onto" to avoid having to
+// deal with merge conflicts.
+func (review *review) squashBranches(branches []string, message string) (e error) {
+ for i := 1; i < len(branches); i++ {
+ // We want to merge the <branches[i]> branch on top of the review
+ // branch, forcing all conflicts to be reviewed in favor of the
+ // <branches[i]> branch. Unfortunately, git merge does not offer a
+ // strategy that would do that for us. The solution implemented
+ // here is based on:
+ //
+ // http://stackoverflow.com/questions/173919/is-there-a-theirs-version-of-git-merge-s-ours
+ if err := gitutil.New(review.jirix.NewSeq()).Merge(branches[i], gitutil.SquashOpt(true), gitutil.StrategyOpt("ours")); err != nil {
+ return changeConflictError{
+ localBranch: branches[i],
+ remoteBranch: review.CLOpts.RemoteBranch,
+ message: err.Error(),
+ }
+ }
+ // Fetch the timestamp of the last commit of <branches[i]> and use
+ // it to create the squashed commit. This is needed to make sure
+ // that the commit hash of the squashed commit stays the same as
+ // long as the squashed sequence of commits does not change. If
+ // this was not the case, consecutive invocations of "jiri cl mail"
+ // could fail if some, but not all, of the dependent CLs submitted
+ // to Gerrit have changed.
+ output, err := gitutil.New(review.jirix.NewSeq()).Log(branches[i], branches[i]+"^", "%ad%n%cd")
+ if err != nil {
+ return err
+ }
+ if len(output) < 1 || len(output[0]) < 2 {
+ return fmt.Errorf("unexpected output length: %v", output)
+ }
+ authorDate := gitutil.AuthorDateOpt(output[0][0])
+ committer := gitutil.CommitterDateOpt(output[0][1])
+ git := gitutil.New(review.jirix.NewSeq(), authorDate, committer)
+ if i < len(branches)-1 {
+ file, err := getCommitMessageFileName(review.jirix, branches[i])
+ if err != nil {
+ return err
+ }
+ message, err := review.jirix.NewSeq().ReadFile(file)
+ if err != nil {
+ return err
+ }
+ if err := git.CommitWithMessage(string(message)); err != nil {
+ return err
+ }
+ } else {
+ committer := git.NewCommitter(review.CLOpts.Edit)
+ if err := committer.Commit(message); err != nil {
+ return err
+ }
+ }
+ tmpBranch := review.reviewBranch + "-" + branches[i] + "-TMP"
+ if err := git.CreateBranch(tmpBranch); err != nil {
+ return err
+ }
+ defer collect.Error(func() error {
+ return gitutil.New(review.jirix.NewSeq()).DeleteBranch(tmpBranch, gitutil.ForceOpt(true))
+ }, &e)
+ if err := git.Reset(branches[i]); err != nil {
+ return err
+ }
+ if err := git.Reset(tmpBranch, gitutil.ModeOpt("soft")); err != nil {
+ return err
+ }
+ if err := git.CommitAmend(); err != nil {
+ return err
+ }
+ }
+ 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)
+ if err != nil {
+ return "", err
+ }
+ // Strip "Change-Id: ..." from the commit messages.
+ strippedMessages := changeIDRE.ReplaceAllLiteralString(commitMessages, "")
+ // Add comment markers (#) to every line.
+ commentedMessages := "# " + strings.Replace(strippedMessages, "\n", "\n# ", -1)
+ message := defaultMessageHeader + commentedMessages
+ return message, nil
+}
+
+// ensureChangeID makes sure that the last commit contains a Change-Id, and
+// returns an error if it does not.
+func (review *review) ensureChangeID() error {
+ latestCommitMessage, err := gitutil.New(review.jirix.NewSeq()).LatestCommitMessage()
+ if err != nil {
+ return err
+ }
+ changeID := changeIDRE.FindString(latestCommitMessage)
+ if changeID == "" {
+ return noChangeIDError(struct{}{})
+ }
+ return nil
+}
+
+// processLabels adds/removes labels for the given commit message.
+func (review *review) processLabels(message string) string {
+ // Find the Change-ID line.
+ changeIDLine := changeIDRE.FindString(message)
+
+ // Strip existing labels and change-ID.
+ message = autosubmitLabelRE.ReplaceAllLiteralString(message, "")
+ message = presubmitTestLabelRE.ReplaceAllLiteralString(message, "")
+ message = changeIDRE.ReplaceAllLiteralString(message, "")
+
+ // Insert labels and change-ID back.
+ if review.CLOpts.Autosubmit {
+ message += fmt.Sprintf("AutoSubmit\n")
+ }
+ if review.CLOpts.Presubmit != gerrit.PresubmitTestTypeAll {
+ message += fmt.Sprintf("PresubmitTest: %s\n", review.CLOpts.Presubmit)
+ }
+ if changeIDLine != "" && !strings.HasSuffix(message, "\n") {
+ message += "\n"
+ }
+ message += changeIDLine
+
+ return message
+}
+
+// run implements checks that the review passes all local checks
+// and then mails it to Gerrit.
+func (review *review) run() (e error) {
+ if uncommittedFlag {
+ changes, err := gitutil.New(review.jirix.NewSeq()).FilesWithUncommittedChanges()
+ if err != nil {
+ return err
+ }
+ if len(changes) != 0 {
+ return uncommittedChangesError(changes)
+ }
+ }
+ if review.CLOpts.Branch == remoteBranchFlag {
+ return fmt.Errorf("cannot do a review from the %q branch.", remoteBranchFlag)
+ }
+ stashed, err := gitutil.New(review.jirix.NewSeq()).Stash()
+ if err != nil {
+ return err
+ }
+ wd, err := os.Getwd()
+ if err != nil {
+ return fmt.Errorf("Getwd() failed: %v", err)
+ }
+ defer collect.Error(func() error { return review.jirix.NewSeq().Chdir(wd).Done() }, &e)
+ topLevel, err := gitutil.New(review.jirix.NewSeq()).TopLevel()
+ if err != nil {
+ return err
+ }
+ s := review.jirix.NewSeq()
+ if err := s.Chdir(topLevel).Done(); err != nil {
+ return err
+ }
+ defer collect.Error(func() error { return review.cleanup(stashed) }, &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.
+ data, err := s.ReadFile(file)
+ if err != nil {
+ if !runutil.IsNotExist(err) {
+ return err
+ }
+ } else {
+ message = string(data)
+ }
+ }
+
+ // Add/remove labels to/from the commit message before asking users
+ // to edit it. We do this only when this is not the initial commit
+ // where the message is empty.
+ //
+ // For the initial commit, the labels will be processed after the
+ // message is edited by users, which happens in the
+ // updateReviewMessage method.
+ if message != "" {
+ message = review.processLabels(message)
+ }
+ if err := review.createReviewBranch(message); err != nil {
+ return err
+ }
+ if err := review.updateReviewMessage(file); err != nil {
+ return err
+ }
+ if err := review.send(); err != nil {
+ return err
+ }
+ if setTopicFlag {
+ if err := review.setTopic(); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// send mails the current branch out for review.
+func (review *review) send() error {
+ if !review.jirix.DryRun() {
+ if err := review.ensureChangeID(); err != nil {
+ return err
+ }
+ }
+ if err := gerrit.Push(review.jirix.NewSeq(), review.CLOpts); err != nil {
+ return gerritError(err.Error())
+ }
+ return nil
+}
+
+// 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 {
+ return "", err
+ }
+ bytes, err := review.jirix.NewSeq().ReadFile(file)
+ if err != nil {
+ return "", err
+ }
+ changeID := changeIDRE.FindSubmatch(bytes)
+ if changeID == nil || len(changeID) < 2 {
+ return "", fmt.Errorf("could not find Change-Id in:\n%s", bytes)
+ }
+ return string(changeID[1]), nil
+}
+
+// setTopic sets the topic for the CL corresponding to the branch the
+// review was created for.
+func (review *review) setTopic() error {
+ changeID, err := review.getChangeID()
+ if err != nil {
+ return err
+ }
+ host := review.CLOpts.Host
+ if host.Scheme != "http" && host.Scheme != "https" {
+ 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 nil
+}
+
+// updateReviewMessage writes the commit message to the given file.
+func (review *review) updateReviewMessage(file string) error {
+ if err := gitutil.New(review.jirix.NewSeq()).CheckoutBranch(review.reviewBranch); err != nil {
+ return err
+ }
+ newMessage, err := gitutil.New(review.jirix.NewSeq()).LatestCommitMessage()
+ if err != nil {
+ return err
+ }
+ 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.
+ //
+ // This behavior is consistent with how Change-ID is added for the
+ // initial commit so we don't confuse users.
+ if _, err := s.Stat(file); err != nil {
+ if runutil.IsNotExist(err) {
+ newMessage = review.processLabels(newMessage)
+ if err := gitutil.New(review.jirix.NewSeq()).CommitAmendWithMessage(newMessage); err != nil {
+ return err
+ }
+ } else {
+ return err
+ }
+ }
+ topLevel, err := gitutil.New(review.jirix.NewSeq()).TopLevel()
+ if err != nil {
+ return err
+ }
+ newMetadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir, review.CLOpts.Branch)
+ if err := s.MkdirAll(newMetadataDir, os.FileMode(0755)).
+ WriteFile(file, []byte(newMessage), 0644).Done(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// cmdCLNew represents the "jiri cl new" command.
+var cmdCLNew = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runCLNew),
+ Name: "new",
+ Short: "Create a new local branch for a changelist",
+ Long: fmt.Sprintf(`
+Command "new" creates a new local branch for a changelist. In
+particular, it forks a new branch with the given name from the current
+branch and records the relationship between the current branch and the
+new branch in the %v metadata directory. The information recorded in
+the %v metadata directory tracks dependencies between CLs and is used
+by the "jiri cl sync" and "jiri cl mail" commands.
+`, jiri.ProjectMetaDir, jiri.ProjectMetaDir),
+ ArgsName: "<name>",
+ ArgsLong: "<name> is the changelist name.",
+}
+
+func runCLNew(jirix *jiri.X, args []string) error {
+ if got, want := len(args), 1; got != want {
+ return jirix.UsageErrorf("unexpected number of arguments: got %v, want %v", got, want)
+ }
+ return newCL(jirix, args)
+}
+
+func newCL(jirix *jiri.X, args []string) error {
+ topLevel, err := gitutil.New(jirix.NewSeq()).TopLevel()
+ if err != nil {
+ return err
+ }
+ originalBranch, err := gitutil.New(jirix.NewSeq()).CurrentBranchName()
+ if err != nil {
+ return err
+ }
+
+ // Create a new branch using the current branch.
+ newBranch := args[0]
+ if err := gitutil.New(jirix.NewSeq()).CreateAndCheckoutBranch(newBranch); err != nil {
+ return err
+ }
+
+ // Register a cleanup handler in case of subsequent errors.
+ cleanup := true
+ defer func() {
+ if cleanup {
+ gitutil.New(jirix.NewSeq()).CheckoutBranch(originalBranch, gitutil.ForceOpt(true))
+ gitutil.New(jirix.NewSeq()).DeleteBranch(newBranch, gitutil.ForceOpt(true))
+ }
+ }()
+
+ s := jirix.NewSeq()
+ // Record the dependent CLs for the new branch. The dependent CLs
+ // are recorded in a <dependencyPathFileName> file as a
+ // newline-separated list of branch names.
+ branches, err := getDependentCLs(jirix, originalBranch)
+ if err != nil {
+ return err
+ }
+ branches = append(branches, originalBranch)
+ newMetadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir, newBranch)
+ if err := s.MkdirAll(newMetadataDir, os.FileMode(0755)).Done(); err != nil {
+ return err
+ }
+ file, err := getDependencyPathFileName(jirix, newBranch)
+ if err != nil {
+ return err
+ }
+ if err := s.WriteFile(file, []byte(strings.Join(branches, "\n")), os.FileMode(0644)).Done(); err != nil {
+ return err
+ }
+
+ cleanup = false
+ return nil
+}
+
+// cmdCLSync represents the "jiri cl sync" command.
+var cmdCLSync = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runCLSync),
+ Name: "sync",
+ Short: "Bring a changelist up to date",
+ Long: fmt.Sprintf(`
+Command "sync" brings the CL identified by the current branch up to
+date with the branch tracking the remote branch this CL pertains
+to. To do that, the command uses the information recorded in the %v
+metadata directory to identify the sequence of dependent CLs leading
+to the current branch. The command then iterates over this sequence
+bringing each of the CLs up to date with its ancestor. The end result
+of this process is that all CLs in the sequence are up to date with
+the branch that tracks the remote branch this CL pertains to.
+
+NOTE: It is possible that the command cannot automatically merge
+changes in an ancestor into its dependent. When that occurs, the
+command is aborted and prints instructions that need to be followed
+before the command can be retried.
+`, jiri.ProjectMetaDir),
+}
+
+func runCLSync(jirix *jiri.X, _ []string) error {
+ return syncCL(jirix)
+}
+
+func syncCL(jirix *jiri.X) (e error) {
+ stashed, err := gitutil.New(jirix.NewSeq()).Stash()
+ if err != nil {
+ return err
+ }
+ if stashed {
+ defer collect.Error(func() error { return gitutil.New(jirix.NewSeq()).StashPop() }, &e)
+ }
+
+ // Register a cleanup handler in case of subsequent errors.
+ forceOriginalBranch := true
+ originalBranch, err := gitutil.New(jirix.NewSeq()).CurrentBranchName()
+ if err != nil {
+ return err
+ }
+ originalWd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if forceOriginalBranch {
+ gitutil.New(jirix.NewSeq()).CheckoutBranch(originalBranch, gitutil.ForceOpt(true))
+ }
+ jirix.NewSeq().Chdir(originalWd)
+ }()
+
+ s := jirix.NewSeq()
+ // Switch to an existing directory in master so we can run commands.
+ topLevel, err := gitutil.New(jirix.NewSeq()).TopLevel()
+ if err != nil {
+ return err
+ }
+ if err := s.Chdir(topLevel).Done(); err != nil {
+ return err
+ }
+
+ // Identify the dependents CLs leading to (and including) the
+ // current branch.
+ branches, err := getDependentCLs(jirix, originalBranch)
+ if err != nil {
+ return err
+ }
+ branches = append(branches, originalBranch)
+
+ // Sync from upstream.
+ if err := gitutil.New(jirix.NewSeq()).CheckoutBranch(branches[0]); err != nil {
+ return err
+ }
+ if err := gitutil.New(jirix.NewSeq()).Pull("origin", branches[0]); err != nil {
+ return err
+ }
+
+ // Bring all CLs in the sequence of dependent CLs leading to the
+ // current branch up to date with the <remoteBranchFlag> branch.
+ for i := 1; i < len(branches); i++ {
+ if err := gitutil.New(jirix.NewSeq()).CheckoutBranch(branches[i]); err != nil {
+ return err
+ }
+ if err := gitutil.New(jirix.NewSeq()).Merge(branches[i-1]); err != nil {
+ return fmt.Errorf(`Failed to automatically merge branch %v into branch %v: %v
+The following steps are needed before the operation can be retried:
+$ git checkout %v
+$ git merge %v
+# resolve all conflicts
+$ git commit -a
+$ git checkout %v
+# retry the original operation
+`, branches[i], branches[i-1], err, branches[i], branches[i-1], originalBranch)
+ }
+ }
+
+ forceOriginalBranch = false
+ return nil
+}
diff --git a/cmd/jiri/cl_test.go b/cmd/jiri/cl_test.go
new file mode 100644
index 0000000..e92f35b
--- /dev/null
+++ b/cmd/jiri/cl_test.go
@@ -0,0 +1,1033 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "bytes"
+ "os"
+ "path"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+
+ "v.io/jiri/gerrit"
+ "v.io/jiri/gitutil"
+ "v.io/jiri/jiri"
+ "v.io/jiri/jiritest"
+ "v.io/jiri/runutil"
+)
+
+// assertCommitCount asserts that the commit count between two
+// branches matches the expectedCount.
+func assertCommitCount(t *testing.T, jirix *jiri.X, branch, baseBranch string, expectedCount int) {
+ got, err := gitutil.New(jirix.NewSeq()).CountCommits(branch, baseBranch)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if want := 1; got != want {
+ t.Fatalf("unexpected number of commits: got %v, want %v", got, want)
+ }
+}
+
+// assertFileContent asserts that the content of the given file
+// matches the expected content.
+func assertFileContent(t *testing.T, jirix *jiri.X, file, want string) {
+ got, err := jirix.NewSeq().ReadFile(file)
+ if err != nil {
+ t.Fatalf("%v\n", err)
+ }
+ if string(got) != want {
+ t.Fatalf("unexpected content of file %v: got %v, want %v", file, got, want)
+ }
+}
+
+// assertFilesExist asserts that the files exist.
+func assertFilesExist(t *testing.T, jirix *jiri.X, files []string) {
+ s := jirix.NewSeq()
+ for _, file := range files {
+ if _, err := s.Stat(file); err != nil {
+ if runutil.IsNotExist(err) {
+ t.Fatalf("expected file %v to exist but it did not", file)
+ }
+ t.Fatalf("%v", err)
+ }
+ }
+}
+
+// assertFilesDoNotExist asserts that the files do not exist.
+func assertFilesDoNotExist(t *testing.T, jirix *jiri.X, files []string) {
+ s := jirix.NewSeq()
+ for _, file := range files {
+ if _, err := s.Stat(file); err != nil && !runutil.IsNotExist(err) {
+ t.Fatalf("%v", err)
+ } else if err == nil {
+ t.Fatalf("expected file %v to not exist but it did", file)
+ }
+ }
+}
+
+// assertFilesCommitted asserts that the files exist and are committed
+// in the current branch.
+func assertFilesCommitted(t *testing.T, jirix *jiri.X, files []string) {
+ assertFilesExist(t, jirix, files)
+ for _, file := range files {
+ if !gitutil.New(jirix.NewSeq()).IsFileCommitted(file) {
+ t.Fatalf("expected file %v to be committed but it is not", file)
+ }
+ }
+}
+
+// assertFilesNotCommitted asserts that the files exist and are *not*
+// committed in the current branch.
+func assertFilesNotCommitted(t *testing.T, jirix *jiri.X, files []string) {
+ assertFilesExist(t, jirix, files)
+ for _, file := range files {
+ if gitutil.New(jirix.NewSeq()).IsFileCommitted(file) {
+ t.Fatalf("expected file %v not to be committed but it is", file)
+ }
+ }
+}
+
+// assertFilesPushedToRef asserts that the given files have been
+// pushed to the given remote repository reference.
+func assertFilesPushedToRef(t *testing.T, jirix *jiri.X, repoPath, gerritPath, pushedRef string, files []string) {
+ chdir(t, jirix, gerritPath)
+ assertCommitCount(t, jirix, pushedRef, "master", 1)
+ if err := gitutil.New(jirix.NewSeq()).CheckoutBranch(pushedRef); err != nil {
+ t.Fatalf("%v", err)
+ }
+ assertFilesCommitted(t, jirix, files)
+ chdir(t, jirix, repoPath)
+}
+
+// assertStashSize asserts that the stash size matches the expected
+// size.
+func assertStashSize(t *testing.T, jirix *jiri.X, want int) {
+ got, err := gitutil.New(jirix.NewSeq()).StashSize()
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if got != want {
+ t.Fatalf("unxpected stash size: got %v, want %v", got, want)
+ }
+}
+
+// commitFile commits a file with the specified content into a branch
+func commitFile(t *testing.T, jirix *jiri.X, filename string, content string) {
+ s := jirix.NewSeq()
+ if err := s.WriteFile(filename, []byte(content), 0644).Done(); err != nil {
+ t.Fatalf("%v", err)
+ }
+ commitMessage := "Commit " + filename
+ if err := gitutil.New(jirix.NewSeq()).CommitFile(filename, commitMessage); err != nil {
+ t.Fatalf("%v", err)
+ }
+}
+
+// commitFiles commits the given files into to current branch.
+func commitFiles(t *testing.T, jirix *jiri.X, filenames []string) {
+ // Create and commit the files one at a time.
+ for _, filename := range filenames {
+ content := "This is file " + filename
+ commitFile(t, jirix, filename, content)
+ }
+}
+
+// createRepo creates a new repository with the given prefix.
+func createRepo(t *testing.T, jirix *jiri.X, prefix string) string {
+ s := jirix.NewSeq()
+ repoPath, err := s.TempDir(jirix.Root, "repo-"+prefix)
+ if err != nil {
+ t.Fatalf("TempDir() failed: %v", err)
+ }
+ if err := os.Chmod(repoPath, 0777); err != nil {
+ t.Fatalf("Chmod(%v) failed: %v", repoPath, err)
+ }
+ if err := gitutil.New(jirix.NewSeq()).Init(repoPath); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := s.MkdirAll(filepath.Join(repoPath, jiri.ProjectMetaDir), os.FileMode(0755)).Done(); err != nil {
+ t.Fatalf("%v", err)
+ }
+ return repoPath
+}
+
+// Simple commit-msg hook that adds a fake Change Id.
+var commitMsgHook string = `#!/bin/sh
+MSG="$1"
+echo "Change-Id: I0000000000000000000000000000000000000000" >> $MSG
+`
+
+// installCommitMsgHook links the gerrit commit-msg hook into a different repo.
+func installCommitMsgHook(t *testing.T, jirix *jiri.X, repoPath string) {
+ hookLocation := path.Join(repoPath, ".git/hooks/commit-msg")
+ if err := jirix.NewSeq().WriteFile(hookLocation, []byte(commitMsgHook), 0755).Done(); err != nil {
+ t.Fatalf("WriteFile(%v) failed: %v", hookLocation, err)
+ }
+}
+
+// chdir changes the runtime working directory and traps any errors.
+func chdir(t *testing.T, jirix *jiri.X, path string) {
+ if err := jirix.NewSeq().Chdir(path).Done(); err != nil {
+ _, file, line, _ := runtime.Caller(1)
+ t.Fatalf("%s: %d: Chdir(%v) failed: %v", file, line, path, err)
+ }
+}
+
+// createRepoFromOrigin creates a Git repo tracking origin/master.
+func createRepoFromOrigin(t *testing.T, jirix *jiri.X, subpath string, originPath string) string {
+ repoPath := createRepo(t, jirix, subpath)
+ chdir(t, jirix, repoPath)
+ if err := gitutil.New(jirix.NewSeq()).AddRemote("origin", originPath); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := gitutil.New(jirix.NewSeq()).Pull("origin", "master"); err != nil {
+ t.Fatalf("%v", err)
+ }
+ return repoPath
+}
+
+// createTestRepos sets up three local repositories: origin, gerrit,
+// and the main test repository which pulls from origin and can push
+// to gerrit.
+func createTestRepos(t *testing.T, jirix *jiri.X) (string, string, string) {
+ // Create origin.
+ originPath := createRepo(t, jirix, "origin")
+ chdir(t, jirix, originPath)
+ if err := gitutil.New(jirix.NewSeq()).CommitWithMessage("initial commit"); err != nil {
+ t.Fatalf("%v", err)
+ }
+ // Create test repo.
+ repoPath := createRepoFromOrigin(t, jirix, "test", originPath)
+ // Add Gerrit remote.
+ gerritPath := createRepoFromOrigin(t, jirix, "gerrit", originPath)
+ // Switch back to test repo.
+ chdir(t, jirix, repoPath)
+ return repoPath, originPath, gerritPath
+}
+
+// submit mocks a Gerrit review submit by pushing the Gerrit remote to origin.
+// Actually origin pulls from Gerrit since origin isn't actually a bare git repo.
+// Some of our tests actually rely on accessing .git in origin, so it must be non-bare.
+func submit(t *testing.T, jirix *jiri.X, originPath string, gerritPath string, review *review) {
+ cwd, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("Getwd() failed: %v", err)
+ }
+ chdir(t, jirix, originPath)
+ expectedRef := gerrit.Reference(review.CLOpts)
+ if err := gitutil.New(jirix.NewSeq()).Pull(gerritPath, expectedRef); err != nil {
+ t.Fatalf("Pull gerrit to origin failed: %v", err)
+ }
+ chdir(t, jirix, cwd)
+}
+
+// setupTest creates a setup for testing the review tool.
+func setupTest(t *testing.T, installHook bool) (fake *jiritest.FakeJiriRoot, repoPath, originPath, gerritPath string, cleanup func()) {
+ oldWD, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("Getwd() failed: %v", err)
+ }
+ var cleanupFake func()
+ if fake, cleanupFake = jiritest.NewFakeJiriRoot(t); err != nil {
+ t.Fatalf("%v", err)
+ }
+ repoPath, originPath, gerritPath = createTestRepos(t, fake.X)
+ if installHook == true {
+ for _, path := range []string{repoPath, originPath, gerritPath} {
+ installCommitMsgHook(t, fake.X, path)
+ }
+ }
+ chdir(t, fake.X, repoPath)
+ cleanup = func() {
+ chdir(t, fake.X, oldWD)
+ cleanupFake()
+ }
+ return
+}
+
+func createCLWithFiles(t *testing.T, jirix *jiri.X, branch string, files ...string) {
+ if err := newCL(jirix, []string{branch}); err != nil {
+ t.Fatalf("%v", err)
+ }
+ commitFiles(t, jirix, files)
+}
+
+// TestCleanupClean checks that cleanup succeeds if the branch to be
+// cleaned up has been merged with the master.
+func TestCleanupClean(t *testing.T) {
+ fake, repoPath, originPath, _, cleanup := setupTest(t, true)
+ defer cleanup()
+ branch := "my-branch"
+ if err := gitutil.New(fake.X.NewSeq()).CreateAndCheckoutBranch(branch); err != nil {
+ t.Fatalf("%v", err)
+ }
+ commitFiles(t, fake.X, []string{"file1", "file2"})
+ if err := gitutil.New(fake.X.NewSeq()).CheckoutBranch("master"); err != nil {
+ t.Fatalf("%v", err)
+ }
+ chdir(t, fake.X, originPath)
+ commitFiles(t, fake.X, []string{"file1", "file2"})
+ chdir(t, fake.X, repoPath)
+ if err := cleanupCL(fake.X, []string{branch}); err != nil {
+ t.Fatalf("cleanup() failed: %v", err)
+ }
+ if gitutil.New(fake.X.NewSeq()).BranchExists(branch) {
+ t.Fatalf("cleanup failed to remove the feature branch")
+ }
+}
+
+// TestCleanupDirty checks that cleanup is a no-op if the branch to be
+// cleaned up has unmerged changes.
+func TestCleanupDirty(t *testing.T) {
+ fake, _, _, _, cleanup := setupTest(t, true)
+ defer cleanup()
+ branch := "my-branch"
+ if err := gitutil.New(fake.X.NewSeq()).CreateAndCheckoutBranch(branch); err != nil {
+ t.Fatalf("%v", err)
+ }
+ files := []string{"file1", "file2"}
+ commitFiles(t, fake.X, files)
+ if err := gitutil.New(fake.X.NewSeq()).CheckoutBranch("master"); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := cleanupCL(fake.X, []string{branch}); err == nil {
+ t.Fatalf("cleanup did not fail when it should")
+ }
+ if err := gitutil.New(fake.X.NewSeq()).CheckoutBranch(branch); err != nil {
+ t.Fatalf("%v", err)
+ }
+ assertFilesCommitted(t, fake.X, files)
+}
+
+// TestCreateReviewBranch checks that the temporary review branch is
+// created correctly.
+func TestCreateReviewBranch(t *testing.T) {
+ fake, _, _, _, cleanup := setupTest(t, true)
+ defer cleanup()
+ branch := "my-branch"
+ if err := gitutil.New(fake.X.NewSeq()).CreateAndCheckoutBranch(branch); err != nil {
+ t.Fatalf("%v", err)
+ }
+ files := []string{"file1", "file2", "file3"}
+ commitFiles(t, fake.X, files)
+ review, err := newReview(fake.X, gerrit.CLOpts{})
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if expected, got := branch+"-REVIEW", review.reviewBranch; expected != got {
+ t.Fatalf("Unexpected review branch name: expected %v, got %v", expected, got)
+ }
+ commitMessage := "squashed commit"
+ if err := review.createReviewBranch(commitMessage); err != nil {
+ t.Fatalf("%v", err)
+ }
+ // Verify that the branch exists.
+ if !gitutil.New(fake.X.NewSeq()).BranchExists(review.reviewBranch) {
+ t.Fatalf("review branch not found")
+ }
+ if err := gitutil.New(fake.X.NewSeq()).CheckoutBranch(review.reviewBranch); err != nil {
+ t.Fatalf("%v", err)
+ }
+ assertCommitCount(t, fake.X, review.reviewBranch, "master", 1)
+ assertFilesCommitted(t, fake.X, files)
+}
+
+// TestCreateReviewBranchWithEmptyChange checks that running
+// createReviewBranch() on a branch with no changes will result in an
+// EmptyChangeError.
+func TestCreateReviewBranchWithEmptyChange(t *testing.T) {
+ fake, _, _, _, cleanup := setupTest(t, true)
+ defer cleanup()
+ branch := "my-branch"
+ if err := gitutil.New(fake.X.NewSeq()).CreateAndCheckoutBranch(branch); err != nil {
+ t.Fatalf("%v", err)
+ }
+ review, err := newReview(fake.X, gerrit.CLOpts{Remote: branch})
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ commitMessage := "squashed commit"
+ err = review.createReviewBranch(commitMessage)
+ if err == nil {
+ t.Fatalf("creating a review did not fail when it should")
+ }
+ if _, ok := err.(emptyChangeError); !ok {
+ t.Fatalf("unexpected error type: %v", err)
+ }
+}
+
+// TestSendReview checks the various options for sending a review.
+func TestSendReview(t *testing.T) {
+ fake, repoPath, _, gerritPath, cleanup := setupTest(t, true)
+ defer cleanup()
+ branch := "my-branch"
+ if err := gitutil.New(fake.X.NewSeq()).CreateAndCheckoutBranch(branch); err != nil {
+ t.Fatalf("%v", err)
+ }
+ files := []string{"file1"}
+ commitFiles(t, fake.X, files)
+ {
+ // Test with draft = false, no reviewiers, and no ccs.
+ review, err := newReview(fake.X, gerrit.CLOpts{Remote: gerritPath})
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := review.send(); err != nil {
+ t.Fatalf("failed to send a review: %v", err)
+ }
+ expectedRef := gerrit.Reference(review.CLOpts)
+ assertFilesPushedToRef(t, fake.X, repoPath, gerritPath, expectedRef, files)
+ }
+ {
+ // Test with draft = true, no reviewers, and no ccs.
+ review, err := newReview(fake.X, gerrit.CLOpts{
+ Draft: true,
+ Remote: gerritPath,
+ })
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := review.send(); err != nil {
+ t.Fatalf("failed to send a review: %v", err)
+ }
+ expectedRef := gerrit.Reference(review.CLOpts)
+ assertFilesPushedToRef(t, fake.X, repoPath, gerritPath, expectedRef, files)
+ }
+ {
+ // Test with draft = false, reviewers, and no ccs.
+ review, err := newReview(fake.X, gerrit.CLOpts{
+ Remote: gerritPath,
+ Reviewers: parseEmails("reviewer1,reviewer2@example.org"),
+ })
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := review.send(); err != nil {
+ t.Fatalf("failed to send a review: %v", err)
+ }
+ expectedRef := gerrit.Reference(review.CLOpts)
+ assertFilesPushedToRef(t, fake.X, repoPath, gerritPath, expectedRef, files)
+ }
+ {
+ // Test with draft = true, reviewers, and ccs.
+ review, err := newReview(fake.X, gerrit.CLOpts{
+ Ccs: parseEmails("cc1@example.org,cc2"),
+ Draft: true,
+ Remote: gerritPath,
+ Reviewers: parseEmails("reviewer3@example.org,reviewer4"),
+ })
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := review.send(); err != nil {
+ t.Fatalf("failed to send a review: %v", err)
+ }
+ expectedRef := gerrit.Reference(review.CLOpts)
+ assertFilesPushedToRef(t, fake.X, repoPath, gerritPath, expectedRef, files)
+ }
+}
+
+// TestSendReviewNoChangeID checks that review.send() correctly errors when
+// not run with a commit hook that adds a Change-Id.
+func TestSendReviewNoChangeID(t *testing.T) {
+ // Pass 'false' to setup so it doesn't install the commit-msg hook.
+ fake, _, _, gerritPath, cleanup := setupTest(t, false)
+ defer cleanup()
+ branch := "my-branch"
+ if err := gitutil.New(fake.X.NewSeq()).CreateAndCheckoutBranch(branch); err != nil {
+ t.Fatalf("%v", err)
+ }
+ commitFiles(t, fake.X, []string{"file1"})
+ review, err := newReview(fake.X, gerrit.CLOpts{Remote: gerritPath})
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ err = review.send()
+ if err == nil {
+ t.Fatalf("sending a review did not fail when it should")
+ }
+ if _, ok := err.(noChangeIDError); !ok {
+ t.Fatalf("unexpected error type: %v", err)
+ }
+}
+
+// TestEndToEnd checks the end-to-end functionality of the review tool.
+func TestEndToEnd(t *testing.T) {
+ fake, repoPath, _, gerritPath, cleanup := setupTest(t, true)
+ defer cleanup()
+ branch := "my-branch"
+ if err := gitutil.New(fake.X.NewSeq()).CreateAndCheckoutBranch(branch); err != nil {
+ t.Fatalf("%v", err)
+ }
+ files := []string{"file1", "file2", "file3"}
+ commitFiles(t, fake.X, files)
+ review, err := newReview(fake.X, gerrit.CLOpts{Remote: gerritPath})
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ setTopicFlag = false
+ if err := review.run(); err != nil {
+ t.Fatalf("run() failed: %v", err)
+ }
+ expectedRef := gerrit.Reference(review.CLOpts)
+ assertFilesPushedToRef(t, fake.X, repoPath, gerritPath, expectedRef, files)
+}
+
+// TestLabelsInCommitMessage checks the labels are correctly processed
+// for the commit message.
+//
+// HACK ALERT: This test runs the review.run() function multiple
+// times. The function ends up pushing a commit to a fake "gerrit"
+// repository created by the setupTest() function. For the real gerrit
+// repository, it is possible to push to the refs/for/change reference
+// multiple times, because it is a special reference that "maps"
+// incoming commits to CL branches based on the commit message
+// Change-Id. The fake "gerrit" repository does not implement this
+// logic and thus the same reference cannot be pushed to multiple
+// times. To overcome this obstacle, the test takes advantage of the
+// fact that the reference name is a function of the reviewers and
+// uses different reviewers for different review runs.
+func TestLabelsInCommitMessage(t *testing.T) {
+ fake, repoPath, _, gerritPath, cleanup := setupTest(t, true)
+ defer cleanup()
+ s := fake.X.NewSeq()
+ branch := "my-branch"
+ if err := gitutil.New(fake.X.NewSeq()).CreateAndCheckoutBranch(branch); err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ // Test setting -presubmit=none and autosubmit.
+ files := []string{"file1", "file2", "file3"}
+ commitFiles(t, fake.X, files)
+ review, err := newReview(fake.X, gerrit.CLOpts{
+ Autosubmit: true,
+ Presubmit: gerrit.PresubmitTestTypeNone,
+ Remote: gerritPath,
+ Reviewers: parseEmails("run1"),
+ })
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ setTopicFlag = false
+ if err := review.run(); err != nil {
+ t.Fatalf("%v", err)
+ }
+ expectedRef := gerrit.Reference(review.CLOpts)
+ assertFilesPushedToRef(t, fake.X, repoPath, gerritPath, expectedRef, files)
+ // The last three lines of the gerrit commit message file should be:
+ // AutoSubmit
+ // PresubmitTest: none
+ // Change-Id: ...
+ file, err := getCommitMessageFileName(review.jirix, review.CLOpts.Branch)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ bytes, err := s.ReadFile(file)
+ if err != nil {
+ t.Fatalf("%v\n", err)
+ }
+ content := string(bytes)
+ lines := strings.Split(content, "\n")
+ // Make sure the Change-Id line is the last line.
+ if got := lines[len(lines)-1]; !strings.HasPrefix(got, "Change-Id") {
+ t.Fatalf("no Change-Id line found: %s", got)
+ }
+ // Make sure the "AutoSubmit" label exists.
+ if autosubmitLabelRE.FindString(content) == "" {
+ t.Fatalf("AutoSubmit label doesn't exist in the commit message: %s", content)
+ }
+ // Make sure the "PresubmitTest" label exists.
+ if presubmitTestLabelRE.FindString(content) == "" {
+ t.Fatalf("PresubmitTest label doesn't exist in the commit message: %s", content)
+ }
+
+ // Test setting -presubmit=all but keep autosubmit=true.
+ review, err = newReview(fake.X, gerrit.CLOpts{
+ Autosubmit: true,
+ Remote: gerritPath,
+ Reviewers: parseEmails("run2"),
+ })
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := review.run(); err != nil {
+ t.Fatalf("%v", err)
+ }
+ expectedRef = gerrit.Reference(review.CLOpts)
+ assertFilesPushedToRef(t, fake.X, repoPath, gerritPath, expectedRef, files)
+ bytes, err = s.ReadFile(file)
+ if err != nil {
+ t.Fatalf("%v\n", err)
+ }
+ content = string(bytes)
+ // Make sure there is no PresubmitTest=none any more.
+ match := presubmitTestLabelRE.FindString(content)
+ if match != "" {
+ t.Fatalf("want no presubmit label line, got: %s", match)
+ }
+ // Make sure the "AutoSubmit" label still exists.
+ if autosubmitLabelRE.FindString(content) == "" {
+ t.Fatalf("AutoSubmit label doesn't exist in the commit message: %s", content)
+ }
+
+ // Test setting autosubmit=false.
+ review, err = newReview(fake.X, gerrit.CLOpts{
+ Remote: gerritPath,
+ Reviewers: parseEmails("run3"),
+ })
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := review.run(); err != nil {
+ t.Fatalf("%v", err)
+ }
+ expectedRef = gerrit.Reference(review.CLOpts)
+ assertFilesPushedToRef(t, fake.X, repoPath, gerritPath, expectedRef, files)
+ bytes, err = s.ReadFile(file)
+ if err != nil {
+ t.Fatalf("%v\n", err)
+ }
+ content = string(bytes)
+ // Make sure there is no AutoSubmit label any more.
+ match = autosubmitLabelRE.FindString(content)
+ if match != "" {
+ t.Fatalf("want no AutoSubmit label line, got: %s", match)
+ }
+}
+
+// TestDirtyBranch checks that the tool correctly handles unstaged and
+// untracked changes in a working branch with stashed changes.
+func TestDirtyBranch(t *testing.T) {
+ fake, _, _, gerritPath, cleanup := setupTest(t, true)
+ defer cleanup()
+ s := fake.X.NewSeq()
+ branch := "my-branch"
+ if err := gitutil.New(fake.X.NewSeq()).CreateAndCheckoutBranch(branch); err != nil {
+ t.Fatalf("%v", err)
+ }
+ files := []string{"file1", "file2"}
+ commitFiles(t, fake.X, files)
+ assertStashSize(t, fake.X, 0)
+ stashedFile, stashedFileContent := "stashed-file", "stashed-file content"
+ if err := s.WriteFile(stashedFile, []byte(stashedFileContent), 0644).Done(); err != nil {
+ t.Fatalf("WriteFile(%v, %v) failed: %v", stashedFile, stashedFileContent, err)
+ }
+ if err := gitutil.New(fake.X.NewSeq()).Add(stashedFile); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if _, err := gitutil.New(fake.X.NewSeq()).Stash(); err != nil {
+ t.Fatalf("%v", err)
+ }
+ assertStashSize(t, fake.X, 1)
+ modifiedFile, modifiedFileContent := "file1", "modified-file content"
+ if err := s.WriteFile(modifiedFile, []byte(modifiedFileContent), 0644).Done(); err != nil {
+ t.Fatalf("WriteFile(%v, %v) failed: %v", modifiedFile, modifiedFileContent, err)
+ }
+ stagedFile, stagedFileContent := "file2", "staged-file content"
+ if err := s.WriteFile(stagedFile, []byte(stagedFileContent), 0644).Done(); err != nil {
+ t.Fatalf("WriteFile(%v, %v) failed: %v", stagedFile, stagedFileContent, err)
+ }
+ if err := gitutil.New(fake.X.NewSeq()).Add(stagedFile); err != nil {
+ t.Fatalf("%v", err)
+ }
+ untrackedFile, untrackedFileContent := "file3", "untracked-file content"
+ 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})
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ setTopicFlag = false
+ if err := review.run(); err == nil {
+ t.Fatalf("run() didn't fail when it should")
+ }
+ assertFilesNotCommitted(t, fake.X, []string{stagedFile})
+ assertFilesNotCommitted(t, fake.X, []string{untrackedFile})
+ assertFileContent(t, fake.X, modifiedFile, modifiedFileContent)
+ assertFileContent(t, fake.X, stagedFile, stagedFileContent)
+ assertFileContent(t, fake.X, untrackedFile, untrackedFileContent)
+ // As of git 2.4.3 "git stash pop" fails if there are uncommitted
+ // changes in the index. So we need to commit them first.
+ if err := gitutil.New(fake.X.NewSeq()).Commit(); err != nil {
+ t.Fatalf("%v", err)
+ }
+ assertStashSize(t, fake.X, 1)
+ if err := gitutil.New(fake.X.NewSeq()).StashPop(); err != nil {
+ t.Fatalf("%v", err)
+ }
+ assertStashSize(t, fake.X, 0)
+ assertFilesNotCommitted(t, fake.X, []string{stashedFile})
+ assertFileContent(t, fake.X, stashedFile, stashedFileContent)
+}
+
+// TestRunInSubdirectory checks that the command will succeed when run from
+// within a subdirectory of a branch that does not exist on master branch, and
+// will return the user to the subdirectory after completion.
+func TestRunInSubdirectory(t *testing.T) {
+ fake, repoPath, _, gerritPath, cleanup := setupTest(t, true)
+ defer cleanup()
+ s := fake.X.NewSeq()
+ branch := "my-branch"
+ if err := gitutil.New(fake.X.NewSeq()).CreateAndCheckoutBranch(branch); err != nil {
+ t.Fatalf("%v", err)
+ }
+ subdir := "sub/directory"
+ subdirPerms := os.FileMode(0744)
+ if err := s.MkdirAll(subdir, subdirPerms).Done(); err != nil {
+ t.Fatalf("MkdirAll(%v, %v) failed: %v", subdir, subdirPerms, err)
+ }
+ 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})
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ setTopicFlag = false
+ if err := review.run(); err != nil {
+ t.Fatalf("run() failed: %v", err)
+ }
+ path := path.Join(repoPath, subdir)
+ want, err := filepath.EvalSymlinks(path)
+ if err != nil {
+ t.Fatalf("EvalSymlinks(%v) failed: %v", path, err)
+ }
+ cwd, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ got, err := filepath.EvalSymlinks(cwd)
+ if err != nil {
+ t.Fatalf("EvalSymlinks(%v) failed: %v", cwd, err)
+ }
+ if got != want {
+ t.Fatalf("unexpected working directory: got %v, want %v", got, want)
+ }
+ expectedRef := gerrit.Reference(review.CLOpts)
+ assertFilesPushedToRef(t, fake.X, repoPath, gerritPath, expectedRef, files)
+}
+
+// TestProcessLabels checks that the processLabels function works as expected.
+func TestProcessLabels(t *testing.T) {
+ fake, _, _, _, cleanup := setupTest(t, true)
+ defer cleanup()
+ testCases := []struct {
+ autosubmit bool
+ presubmitType gerrit.PresubmitTestType
+ originalMessage string
+ expectedMessage string
+ }{
+ {
+ presubmitType: gerrit.PresubmitTestTypeNone,
+ originalMessage: "",
+ expectedMessage: "PresubmitTest: none\n",
+ },
+ {
+ autosubmit: true,
+ presubmitType: gerrit.PresubmitTestTypeNone,
+ originalMessage: "",
+ expectedMessage: "AutoSubmit\nPresubmitTest: none\n",
+ },
+ {
+ presubmitType: gerrit.PresubmitTestTypeNone,
+ originalMessage: "review message\n",
+ expectedMessage: "review message\nPresubmitTest: none\n",
+ },
+ {
+ autosubmit: true,
+ presubmitType: gerrit.PresubmitTestTypeNone,
+ originalMessage: "review message\n",
+ expectedMessage: "review message\nAutoSubmit\nPresubmitTest: none\n",
+ },
+ {
+ presubmitType: gerrit.PresubmitTestTypeNone,
+ originalMessage: `review message
+
+Change-Id: I0000000000000000000000000000000000000000`,
+ expectedMessage: `review message
+
+PresubmitTest: none
+Change-Id: I0000000000000000000000000000000000000000`,
+ },
+ {
+ autosubmit: true,
+ presubmitType: gerrit.PresubmitTestTypeNone,
+ originalMessage: `review message
+
+Change-Id: I0000000000000000000000000000000000000000`,
+ expectedMessage: `review message
+
+AutoSubmit
+PresubmitTest: none
+Change-Id: I0000000000000000000000000000000000000000`,
+ },
+ {
+ presubmitType: gerrit.PresubmitTestTypeAll,
+ originalMessage: "",
+ expectedMessage: "",
+ },
+ {
+ presubmitType: gerrit.PresubmitTestTypeAll,
+ originalMessage: "review message\n",
+ expectedMessage: "review message\n",
+ },
+ {
+ presubmitType: gerrit.PresubmitTestTypeAll,
+ originalMessage: `review message
+
+Change-Id: I0000000000000000000000000000000000000000`,
+ expectedMessage: `review message
+
+Change-Id: I0000000000000000000000000000000000000000`,
+ },
+ }
+ for _, test := range testCases {
+ review, err := newReview(fake.X, gerrit.CLOpts{
+ Autosubmit: test.autosubmit,
+ Presubmit: test.presubmitType,
+ })
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if got := review.processLabels(test.originalMessage); got != test.expectedMessage {
+ t.Fatalf("want %s, got %s", test.expectedMessage, got)
+ }
+ }
+}
+
+// TestCLNew checks the operation of the "jiri cl new" command.
+func TestCLNew(t *testing.T) {
+ fake, _, _, _, cleanup := setupTest(t, true)
+ defer cleanup()
+
+ // Create some dependent CLs.
+ if err := newCL(fake.X, []string{"feature1"}); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := newCL(fake.X, []string{"feature2"}); err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ // Check that their dependency paths have been recorded correctly.
+ testCases := []struct {
+ branch string
+ data []byte
+ }{
+ {
+ branch: "feature1",
+ data: []byte("master"),
+ },
+ {
+ branch: "feature2",
+ data: []byte("master\nfeature1"),
+ },
+ }
+ s := fake.X.NewSeq()
+ for _, testCase := range testCases {
+ file, err := getDependencyPathFileName(fake.X, testCase.branch)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ data, err := s.ReadFile(file)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if bytes.Compare(data, testCase.data) != 0 {
+ t.Fatalf("unexpected data:\ngot\n%v\nwant\n%v", string(data), string(testCase.data))
+ }
+ }
+}
+
+// TestDependentClsWithEditDelete exercises a previously observed failure case
+// where if a CL edits a file and a dependent CL deletes it, jiri cl mail after
+// the deletion failed with unrecoverable merge errors.
+func TestDependentClsWithEditDelete(t *testing.T) {
+ fake, repoPath, originPath, gerritPath, cleanup := setupTest(t, true)
+ defer cleanup()
+ chdir(t, fake.X, originPath)
+ commitFiles(t, fake.X, []string{"A", "B"})
+
+ chdir(t, fake.X, repoPath)
+ if err := syncCL(fake.X); err != nil {
+ t.Fatalf("%v", err)
+ }
+ assertFilesExist(t, fake.X, []string{"A", "B"})
+
+ createCLWithFiles(t, fake.X, "editme", "C")
+ if err := fake.X.NewSeq().WriteFile("B", []byte("Will I dream?"), 0644).Done(); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := gitutil.New(fake.X.NewSeq()).Add("B"); err != nil {
+ t.Fatalf("%v", err)
+ }
+ 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{
+ Remote: gerritPath,
+ Reviewers: parseEmails("run1"), // See hack note about TestLabelsInCommitMessage
+ })
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ setTopicFlag = false
+ if err := review.run(); err != nil {
+ t.Fatalf("run() failed: %v", err)
+ }
+
+ if err := newCL(fake.X, []string{"deleteme"}); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := gitutil.New(fake.X.NewSeq()).Remove("B", "C"); err != nil {
+ t.Fatalf("git rm B C failed: %v", err)
+ }
+ 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{
+ Remote: gerritPath,
+ Reviewers: parseEmails("run2"),
+ })
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := review.run(); err != nil {
+ t.Fatalf("run() failed: %v", err)
+ }
+
+ chdir(t, fake.X, gerritPath)
+ expectedRef := gerrit.Reference(review.CLOpts)
+ if err := gitutil.New(fake.X.NewSeq()).CheckoutBranch(expectedRef); err != nil {
+ t.Fatalf("%v", err)
+ }
+ assertFilesExist(t, fake.X, []string{"A"})
+ assertFilesDoNotExist(t, fake.X, []string{"B", "C"})
+}
+
+// TestParallelDev checks "jiri cl mail" behavior when parallel development has
+// been submitted upstream.
+func TestParallelDev(t *testing.T) {
+ fake, repoPath, originPath, gerritAPath, cleanup := setupTest(t, true)
+ defer cleanup()
+ gerritBPath := createRepoFromOrigin(t, fake.X, "gerritB", originPath)
+ chdir(t, fake.X, repoPath)
+
+ // Create parallel branches with:
+ // * non-conflicting changes in different files
+ // * conflicting changes in a file
+ createCLWithFiles(t, fake.X, "feature1-A", "A")
+
+ if err := gitutil.New(fake.X.NewSeq()).CheckoutBranch("master"); err != nil {
+ t.Fatalf("%v", err)
+ }
+ 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})
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ setTopicFlag = false
+ if err := reviewB.run(); err != nil {
+ t.Fatalf("run() failed: %v", err)
+ }
+
+ // Submit B and verify A doesn't revert it.
+ submit(t, fake.X, originPath, gerritBPath, reviewB)
+
+ // Assert files pushed to origin.
+ chdir(t, fake.X, originPath)
+ assertFilesExist(t, fake.X, []string{"A", "B"})
+ chdir(t, fake.X, repoPath)
+
+ if err := gitutil.New(fake.X.NewSeq()).CheckoutBranch("feature1-A"); err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ reviewA, err := newReview(fake.X, gerrit.CLOpts{Remote: gerritAPath})
+ if err == nil {
+ t.Fatalf("creating a review did not fail when it should")
+ }
+ // Assert state restored after failed review.
+ assertFileContent(t, fake.X, "A", "This is file A")
+ assertFilesDoNotExist(t, fake.X, []string{"B"})
+
+ // Manual conflict resolution.
+ if err := gitutil.New(fake.X.NewSeq()).Merge("master", gitutil.ResetOnFailureOpt(false)); err == nil {
+ t.Fatalf("merge applied cleanly when it shouldn't")
+ }
+ assertFilesNotCommitted(t, fake.X, []string{"A", "B"})
+ assertFileContent(t, fake.X, "B", "This is file B")
+
+ if err := fake.X.NewSeq().WriteFile("A", []byte("This is file A. Don't tread on me."), 0644).Done(); err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ if err := gitutil.New(fake.X.NewSeq()).Add("A"); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := gitutil.New(fake.X.NewSeq()).Add("B"); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := gitutil.New(fake.X.NewSeq()).CommitWithMessage("Conflict resolution"); err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ // Retry review.
+ reviewA, err = newReview(fake.X, gerrit.CLOpts{Remote: gerritAPath})
+ if err != nil {
+ t.Fatalf("review failed: %v", err)
+ }
+
+ if err := reviewA.run(); err != nil {
+ t.Fatalf("run() failed: %v", err)
+ }
+
+ chdir(t, fake.X, gerritAPath)
+ expectedRef := gerrit.Reference(reviewA.CLOpts)
+ if err := gitutil.New(fake.X.NewSeq()).CheckoutBranch(expectedRef); err != nil {
+ t.Fatalf("%v", err)
+ }
+ assertFilesExist(t, fake.X, []string{"B"})
+}
+
+// TestCLSync checks the operation of the "jiri cl sync" command.
+func TestCLSync(t *testing.T) {
+ fake, _, _, _, cleanup := setupTest(t, true)
+ defer cleanup()
+
+ // Create some dependent CLs.
+ if err := newCL(fake.X, []string{"feature1"}); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := newCL(fake.X, []string{"feature2"}); err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ // Add the "test" file to the master.
+ if err := gitutil.New(fake.X.NewSeq()).CheckoutBranch("master"); err != nil {
+ t.Fatalf("%v", err)
+ }
+ commitFiles(t, fake.X, []string{"test"})
+
+ // Sync the dependent CLs.
+ if err := gitutil.New(fake.X.NewSeq()).CheckoutBranch("feature2"); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := syncCL(fake.X); err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ // Check that the "test" file exists in the dependent CLs.
+ for _, branch := range []string{"feature1", "feature2"} {
+ if err := gitutil.New(fake.X.NewSeq()).CheckoutBranch(branch); err != nil {
+ t.Fatalf("%v", err)
+ }
+ assertFilesExist(t, fake.X, []string{"test"})
+ }
+}
diff --git a/cmd/jiri/cmd.go b/cmd/jiri/cmd.go
new file mode 100644
index 0000000..0d4d2ee
--- /dev/null
+++ b/cmd/jiri/cmd.go
@@ -0,0 +1,158 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// The following enables go generate to generate the doc.go file.
+//go:generate go run $JIRI_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go -env="" .
+
+package main
+
+import (
+ "runtime"
+
+ "v.io/jiri/tool"
+ "v.io/x/lib/cmdline"
+)
+
+func init() {
+ runtime.GOMAXPROCS(runtime.NumCPU())
+
+ tool.InitializeRunFlags(&cmdRoot.Flags)
+}
+
+func main() {
+ cmdline.Main(cmdRoot)
+}
+
+// cmdRoot represents the root of the jiri tool.
+var cmdRoot = &cmdline.Command{
+ Name: "jiri",
+ Short: "Multi-purpose tool for multi-repo development",
+ Long: `
+Command jiri is a multi-purpose tool for multi-repo development.
+`,
+ LookPath: true,
+ Children: []*cmdline.Command{
+ cmdCL,
+ cmdContributors,
+ cmdImport,
+ cmdProfile,
+ cmdProject,
+ cmdRebuild,
+ cmdSnapshot,
+ cmdUpdate,
+ cmdUpgrade,
+ cmdWhich,
+ },
+ Topics: []cmdline.Topic{
+ topicFileSystem,
+ topicManifest,
+ },
+}
+
+var topicFileSystem = cmdline.Topic{
+ Name: "filesystem",
+ Short: "Description of jiri file system layout",
+ Long: `
+All data managed by the jiri tool is located in the file system under a root
+directory, colloquially called the jiri root directory. The file system layout
+looks like this:
+
+ [root] # root directory (name picked by user)
+ [root]/.jiri_root # root metadata directory
+ [root]/.jiri_root/bin # contains tool binaries (jiri, etc.)
+ [root]/.jiri_root/update_history # contains history of update snapshots
+ [root]/.manifest # contains jiri manifests
+ [root]/[project1] # project directory (name picked by user)
+ [root]/[project1]/.jiri # project metadata directory
+ [root]/[project1]/.jiri/metadata.v2 # project metadata file
+ [root]/[project1]/.jiri/<<cls>> # project per-cl metadata directories
+ [root]/[project1]/<<files>> # project files
+ [root]/[project2]...
+
+The [root] and [projectN] directory names are picked by the user. The <<cls>>
+are named via jiri cl new, and the <<files>> are named as the user adds files
+and directories to their project. All other names above have special meaning to
+the jiri tool, and cannot be changed; you must ensure your path names don't
+collide with these special names.
+
+There are two ways to run the jiri tool:
+
+1) Shim script (recommended approach). This is a shell script that looks for
+the [root] directory. If the JIRI_ROOT environment variable is set, that is
+assumed to be the [root] directory. Otherwise the script looks for the
+.jiri_root directory, starting in the current working directory and walking up
+the directory chain. The search is terminated successfully when the .jiri_root
+directory is found; it fails after it reaches the root of the file system. Thus
+the shim must be invoked from the [root] directory or one of its subdirectories.
+
+Once the [root] is found, the JIRI_ROOT environment variable is set to its
+location, and [root]/.jiri_root/bin/jiri is invoked. That file contains the
+actual jiri binary.
+
+The point of the shim script is to make it easy to use the jiri tool with
+multiple [root] directories on your file system. Keep in mind that when "jiri
+update" is run, the jiri tool itself is automatically updated along with all
+projects. By using the shim script, you only need to remember to invoke the
+jiri tool from within the appropriate [root] directory, and the projects and
+tools under that [root] directory will be updated.
+
+The shim script is located at [root]/release/go/src/v.io/jiri/scripts/jiri
+
+2) Direct binary. This is the jiri binary, containing all of the actual jiri
+tool logic. The binary requires the JIRI_ROOT environment variable to point to
+the [root] directory.
+
+Note that if you have multiple [root] directories on your file system, you must
+remember to run the jiri binary corresponding to the setting of your JIRI_ROOT
+environment variable. Things may fail if you mix things up, since the jiri
+binary is updated with each call to "jiri update", and you may encounter version
+mismatches between the jiri binary and the various metadata files or other
+logic. This is the reason the shim script is recommended over running the
+binary directly.
+
+The binary is located at [root]/.jiri_root/bin/jiri
+`,
+}
+
+// TODO(toddw): Update the description of manifest files.
+var topicManifest = cmdline.Topic{
+ Name: "manifest",
+ Short: "Description of manifest files",
+ Long: `
+Jiri manifests are revisioned and stored in a "manifest" repository, that is
+available locally in $JIRI_ROOT/.manifest. The manifest uses the following XML
+schema:
+
+ <manifest>
+ <imports>
+ <import name="default"/>
+ ...
+ </imports>
+ <projects>
+ <project name="release.go.jiri"
+ path="release/go/src/v.io/jiri"
+ protocol="git"
+ name="https://vanadium.googlesource.com/release.go.jiri"
+ revision="HEAD"/>
+ ...
+ </projects>
+ <tools>
+ <tool name="jiri" package="v.io/jiri"/>
+ ...
+ </tools>
+ </manifest>
+
+The <import> element can be used to share settings across multiple
+manifests. Import names are interpreted relative to the $JIRI_ROOT/.manifest/v2
+directory. Import cycles are not allowed and if a project or a tool is specified
+multiple times, the last specification takes effect. In particular, the elements
+<project name="foo" exclude="true"/> and <tool name="bar" exclude="true"/> can
+be used to exclude previously included projects and tools.
+
+The tool identifies which manifest to use using the following algorithm. If the
+$JIRI_ROOT/.local_manifest file exists, then it is used. Otherwise, the
+$JIRI_ROOT/.manifest/v2/<manifest>.xml file is used, where <manifest> is the
+value of the -manifest command-line flag, which defaults to "default".
+`,
+}
diff --git a/cmd/jiri/contrib.go b/cmd/jiri/contrib.go
new file mode 100644
index 0000000..1fb31e6
--- /dev/null
+++ b/cmd/jiri/contrib.go
@@ -0,0 +1,223 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "encoding/xml"
+ "fmt"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+
+ "v.io/jiri/collect"
+ "v.io/jiri/gitutil"
+ "v.io/jiri/jiri"
+ "v.io/jiri/project"
+ "v.io/jiri/tool"
+ "v.io/x/lib/cmdline"
+ "v.io/x/lib/set"
+)
+
+const (
+ aliasesFileName = "aliases.v1.xml"
+)
+
+var (
+ countFlag bool
+ aliasesFlag string
+)
+
+func init() {
+ cmdContributors.Flags.BoolVar(&countFlag, "n", false, "Show number of contributions.")
+ cmdContributors.Flags.StringVar(&aliasesFlag, "aliases", "", "Path to the aliases file.")
+}
+
+// cmdContributors represents the "jiri contributors" command.
+var cmdContributors = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runContributors),
+ Name: "contributors",
+ Short: "List project contributors",
+ Long: `
+Lists project contributors. Projects to consider can be specified as
+an argument. If no projects are specified, all projects in the current
+manifest are considered by default.
+`,
+ ArgsName: "<projects>",
+ ArgsLong: "<projects> is a list of projects to consider.",
+}
+
+type contributor struct {
+ count int
+ email string
+ name string
+}
+
+var (
+ contributorRE = regexp.MustCompile("^(.*)\t(.*) <(.*)>$")
+)
+
+type aliasesSchema struct {
+ XMLName xml.Name `xml:"aliases"`
+ Names []nameSchema `xml:"name"`
+ Emails []emailSchema `xml:"email"`
+}
+
+type nameSchema struct {
+ Canonical string `xml:"canonical"`
+ Aliases []string `xml:"alias"`
+}
+
+type emailSchema struct {
+ Canonical string `xml:"canonical"`
+ Aliases []string `xml:"alias"`
+}
+
+type aliasMaps struct {
+ emails map[string]string
+ names map[string]string
+}
+
+func canonicalize(aliases *aliasMaps, email, name string) (string, string) {
+ canonicalEmail, canonicalName := email, name
+ if email, ok := aliases.emails[email]; ok {
+ canonicalEmail = email
+ }
+ if name, ok := aliases.names[name]; ok {
+ canonicalName = name
+ }
+ return canonicalEmail, canonicalName
+}
+
+func loadAliases(jirix *jiri.X) (*aliasMaps, error) {
+ aliasesFile := aliasesFlag
+ if aliasesFile == "" {
+ dataDir, err := project.DataDirPath(jirix, tool.Name)
+ if err != nil {
+ return nil, err
+ }
+ aliasesFile = filepath.Join(dataDir, aliasesFileName)
+ }
+ bytes, err := jirix.NewSeq().ReadFile(aliasesFile)
+ if err != nil {
+ return nil, err
+ }
+ var data aliasesSchema
+ if err := xml.Unmarshal(bytes, &data); err != nil {
+ return nil, fmt.Errorf("Unmarshal(%v) failed: %v", string(bytes), err)
+ }
+ aliases := &aliasMaps{
+ emails: map[string]string{},
+ names: map[string]string{},
+ }
+ for _, email := range data.Emails {
+ for _, alias := range email.Aliases {
+ aliases.emails[alias] = email.Canonical
+ }
+ }
+ for _, name := range data.Names {
+ for _, alias := range name.Aliases {
+ aliases.names[alias] = name.Canonical
+ }
+ }
+ return aliases, nil
+}
+
+func runContributors(jirix *jiri.X, args []string) error {
+ localProjects, err := project.LocalProjects(jirix, project.FastScan)
+ if err != nil {
+ return err
+ }
+ projectNames := map[string]struct{}{}
+ if len(args) != 0 {
+ projectNames = set.String.FromSlice(args)
+ } else {
+ for _, p := range localProjects {
+ projectNames[p.Name] = struct{}{}
+ }
+ }
+
+ aliases, err := loadAliases(jirix)
+ if err != nil {
+ return err
+ }
+ contributors := map[string]*contributor{}
+ for name, _ := range projectNames {
+ projects := localProjects.Find(name)
+ if len(projects) == 0 {
+ continue
+ }
+
+ for _, project := range projects {
+ if err := jirix.NewSeq().Chdir(project.Path).Done(); err != nil {
+ return err
+ }
+ switch project.Protocol {
+ case "git":
+ lines, err := listCommitters(jirix)
+ if err != nil {
+ return err
+ }
+ for _, line := range lines {
+ matches := contributorRE.FindStringSubmatch(line)
+ if got, want := len(matches), 4; got != want {
+ return fmt.Errorf("unexpected length of %v: got %v, want %v", matches, got, want)
+ }
+ count, err := strconv.Atoi(strings.TrimSpace(matches[1]))
+ if err != nil {
+ return fmt.Errorf("Atoi(%v) failed: %v", strings.TrimSpace(matches[1]), err)
+ }
+ c := &contributor{
+ count: count,
+ email: strings.TrimSpace(matches[3]),
+ name: strings.TrimSpace(matches[2]),
+ }
+ if c.email == "jenkins.veyron@gmail.com" || c.email == "jenkins.veyron.rw@gmail.com" {
+ continue
+ }
+ c.email, c.name = canonicalize(aliases, c.email, c.name)
+ if existing, ok := contributors[c.name]; ok {
+ existing.count += c.count
+ } else {
+ contributors[c.name] = c
+ }
+ }
+ }
+ }
+ }
+ names := []string{}
+ for name, _ := range contributors {
+ names = append(names, name)
+ }
+ sort.Strings(names)
+ for _, name := range names {
+ c := contributors[name]
+ if countFlag {
+ fmt.Fprintf(jirix.Stdout(), "%4d ", c.count)
+ }
+ fmt.Fprintf(jirix.Stdout(), "%v <%v>\n", c.name, c.email)
+ }
+ return nil
+}
+
+func listCommitters(jirix *jiri.X) (_ []string, e error) {
+ branch, err := gitutil.New(jirix.NewSeq()).CurrentBranchName()
+ if err != nil {
+ return nil, err
+ }
+ stashed, err := gitutil.New(jirix.NewSeq()).Stash()
+ if err != nil {
+ return nil, err
+ }
+ if stashed {
+ defer collect.Error(func() error { return gitutil.New(jirix.NewSeq()).StashPop() }, &e)
+ }
+ if err := gitutil.New(jirix.NewSeq()).CheckoutBranch("master"); err != nil {
+ return nil, err
+ }
+ defer collect.Error(func() error { return gitutil.New(jirix.NewSeq()).CheckoutBranch(branch) }, &e)
+ return gitutil.New(jirix.NewSeq()).Committers()
+}
diff --git a/cmd/jiri/doc.go b/cmd/jiri/doc.go
new file mode 100644
index 0000000..bfe847c
--- /dev/null
+++ b/cmd/jiri/doc.go
@@ -0,0 +1,963 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This file was auto-generated via go generate.
+// DO NOT UPDATE MANUALLY
+
+/*
+Command jiri is a multi-purpose tool for multi-repo development.
+
+Usage:
+ jiri [flags] <command>
+
+The jiri commands are:
+ cl Manage project changelists
+ contributors List project contributors
+ import Adds imports to .jiri_manifest file
+ profile Display information about installed profiles
+ project Manage the jiri projects
+ rebuild Rebuild all jiri tools
+ snapshot Manage project snapshots
+ update Update all jiri tools and projects
+ upgrade Upgrade jiri to new-style manifests
+ which Show path to the jiri tool
+ help Display help for commands or topics
+
+The jiri additional help topics are:
+ filesystem Description of jiri file system layout
+ manifest Description of manifest files
+
+The jiri flags are:
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+The global flags are:
+ -metadata=<just specify -metadata to activate>
+ Displays metadata for the program and exits.
+ -time=false
+ Dump timing information to stderr before exiting the program.
+
+Jiri cl - Manage project changelists
+
+Manage project changelists.
+
+Usage:
+ jiri cl [flags] <command>
+
+The jiri cl commands are:
+ cleanup Clean up changelists that have been merged
+ mail Mail a changelist for review
+ new Create a new local branch for a changelist
+ sync Bring a changelist up to date
+
+The jiri cl flags are:
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri cl cleanup - Clean up changelists that have been merged
+
+Command "cleanup" checks that the given branches have been merged into the
+corresponding remote branch. If a branch differs from the corresponding remote
+branch, the command reports the difference and stops. Otherwise, it deletes the
+given branches.
+
+Usage:
+ jiri cl cleanup [flags] <branches>
+
+<branches> is a list of branches to cleanup.
+
+The jiri cl cleanup flags are:
+ -f=false
+ Ignore unmerged changes.
+ -remote-branch=master
+ Name of the remote branch the CL pertains to, without the leading "origin/".
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri cl mail - Mail a changelist for review
+
+Command "mail" squashes all commits of a local branch into a single "changelist"
+and mails this changelist to Gerrit as a single commit. First time the command
+is invoked, it generates a Change-Id for the changelist, which is appended to
+the commit message. Consecutive invocations of the command use the same
+Change-Id by default, informing Gerrit that the incomming commit is an update of
+an existing changelist.
+
+Usage:
+ jiri cl mail [flags]
+
+The jiri cl mail flags are:
+ -autosubmit=false
+ Automatically submit the changelist when feasible.
+ -cc=
+ Comma-seperated list of emails or LDAPs to cc.
+ -check-uncommitted=true
+ Check that no uncommitted changes exist.
+ -d=false
+ Send a draft changelist.
+ -edit=true
+ Open an editor to edit the CL description.
+ -host=
+ Gerrit host to use. Defaults to gerrit host specified in manifest.
+ -m=
+ CL description.
+ -presubmit=all
+ The type of presubmit tests to run. Valid values: none,all.
+ -r=
+ Comma-seperated list of emails or LDAPs to request review.
+ -remote-branch=master
+ Name of the remote branch the CL pertains to, without the leading "origin/".
+ -set-topic=true
+ Set Gerrit CL topic.
+ -topic=
+ CL topic, defaults to <username>-<branchname>.
+ -verify=true
+ Run pre-push git hooks.
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri cl new - Create a new local branch for a changelist
+
+Command "new" creates a new local branch for a changelist. In particular, it
+forks a new branch with the given name from the current branch and records the
+relationship between the current branch and the new branch in the .jiri metadata
+directory. The information recorded in the .jiri metadata directory tracks
+dependencies between CLs and is used by the "jiri cl sync" and "jiri cl mail"
+commands.
+
+Usage:
+ jiri cl new [flags] <name>
+
+<name> is the changelist name.
+
+The jiri cl new flags are:
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri cl sync - Bring a changelist up to date
+
+Command "sync" brings the CL identified by the current branch up to date with
+the branch tracking the remote branch this CL pertains to. To do that, the
+command uses the information recorded in the .jiri metadata directory to
+identify the sequence of dependent CLs leading to the current branch. The
+command then iterates over this sequence bringing each of the CLs up to date
+with its ancestor. The end result of this process is that all CLs in the
+sequence are up to date with the branch that tracks the remote branch this CL
+pertains to.
+
+NOTE: It is possible that the command cannot automatically merge changes in an
+ancestor into its dependent. When that occurs, the command is aborted and prints
+instructions that need to be followed before the command can be retried.
+
+Usage:
+ jiri cl sync [flags]
+
+The jiri cl sync flags are:
+ -remote-branch=master
+ Name of the remote branch the CL pertains to, without the leading "origin/".
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri contributors - List project contributors
+
+Lists project contributors. Projects to consider can be specified as an
+argument. If no projects are specified, all projects in the current manifest are
+considered by default.
+
+Usage:
+ jiri contributors [flags] <projects>
+
+<projects> is a list of projects to consider.
+
+The jiri contributors flags are:
+ -aliases=
+ Path to the aliases file.
+ -n=false
+ Show number of contributions.
+
+ -color=true
+ Use color to format output.
+ -v=false
+ Print verbose output.
+
+Jiri import
+
+Command "import" adds imports to the $JIRI_ROOT/.jiri_manifest file, which
+specifies manifest information for the jiri tool. The file is created if it
+doesn't already exist, otherwise additional imports are added to the existing
+file.
+
+An <import> element is added to the manifest representing a remote manifest
+import. The manifest file path is relative to the root directory of the remote
+import repository.
+
+Example:
+ $ jiri import myfile https://foo.com/bar.git
+
+Run "jiri help manifest" for details on manifests.
+
+Usage:
+ jiri import [flags] <manifest> <remote>
+
+<manifest> specifies the manifest file to use.
+
+<remote> specifies the remote manifest repository.
+
+The jiri import flags are:
+ -name=
+ The name of the remote manifest project, used to disambiguate manifest
+ projects with the same remote. Typically empty.
+ -out=
+ The output file. Uses $JIRI_ROOT/.jiri_manifest if unspecified. Uses stdout
+ if set to "-".
+ -overwrite=false
+ Write a new .jiri_manifest file with the given specification. If it already
+ exists, the existing content will be ignored and the file will be
+ overwritten.
+ -protocol=git
+ The version control protocol used by the remote manifest project.
+ -remote-branch=master
+ The branch of the remote manifest project to track, without the leading
+ "origin/".
+ -root=
+ Root to store the manifest project locally.
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri profile - Display information about installed profiles
+
+Display information about installed profiles and their configuration.
+
+Usage:
+ jiri profile [flags] <command>
+
+The jiri profile commands are:
+ list List available or installed profiles
+ env Display profile environment variables
+ install Install the given profiles
+ uninstall Uninstall the given profiles
+ update Install the latest default version of the given profiles
+ cleanup Cleanup the locally installed profiles
+ available List the available profiles
+
+The jiri profile flags are:
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri profile list - List available or installed profiles
+
+List available or installed profiles.
+
+Usage:
+ jiri profile list [flags] [<profiles>]
+
+<profiles> is a list of profiles to list, defaulting to all profiles if none are
+specifically requested.
+
+The jiri profile list flags are:
+ -available=false
+ print the list of available profiles
+ -env=
+ specify an environment variable in the form: <var>=[<val>],...
+ -info=
+ The following fields for use with --profile-info are available:
+ SchemaVersion - the version of the profiles implementation.
+ Target.InstallationDir - the installation directory of the requested profile.
+ Target.CommandLineEnv - the environment variables specified via the command line when installing this profile target.
+ Target.Env - the environment variables computed by the profile installation process for this target.
+ Target.Command - a command that can be used to create this profile.
+ Note: if no --target is specified then the requested field will be displayed for all targets.
+ Profile.Description - description of the requested profile.
+ Profile.Root - the root directory of the requested profile.
+ Profile.Versions - the set of supported versions for this profile.
+ Profile.DefaultVersion - the default version of the requested profile.
+ Profile.LatestVersion - the latest version available for the requested profile.
+ Note: if no profiles are specified then the requested field will be displayed for all profiles.
+ -merge-policies=+CCFLAGS,+CGO_CFLAGS,+CGO_CXXFLAGS,+CGO_LDFLAGS,+CXXFLAGS,GOARCH,GOOS,GOPATH:,^GOROOT*,+LDFLAGS,:PATH,VDLPATH:
+ specify policies for merging environment variables
+ -profiles=base,jiri
+ a comma separated list of profiles to use
+ -profiles-db=$JIRI_ROOT/.jiri_root/profile_db
+ specify the profiles database directory or file.
+ -show-profiles-db=false
+ print out the profiles database file
+ -skip-profiles=false
+ if set, no profiles will be used
+ -target=<runtime.GOARCH>-<runtime.GOOS>
+ specifies a profile target in the following form: <arch>-<os>[@<version>]
+ -v=false
+ print more detailed information
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+
+Jiri profile env - Display profile environment variables
+
+List profile specific and target specific environment variables. If the
+requested environment variable name ends in = then only the value will be
+printed, otherwise both name and value are printed, i.e. GOPATH="foo" vs just
+"foo".
+
+If no environment variable names are requested then all will be printed in
+<name>=<val> format.
+
+Usage:
+ jiri profile env [flags] [<environment variable names>]
+
+[<environment variable names>] is an optional list of environment variables to
+display
+
+The jiri profile env flags are:
+ -env=
+ specify an environment variable in the form: <var>=[<val>],...
+ -merge-policies=+CCFLAGS,+CGO_CFLAGS,+CGO_CXXFLAGS,+CGO_LDFLAGS,+CXXFLAGS,GOARCH,GOOS,GOPATH:,^GOROOT*,+LDFLAGS,:PATH,VDLPATH:
+ specify policies for merging environment variables
+ -profiles=base,jiri
+ a comma separated list of profiles to use
+ -profiles-db=$JIRI_ROOT/.jiri_root/profile_db
+ specify the profiles database directory or file.
+ -skip-profiles=false
+ if set, no profiles will be used
+ -target=<runtime.GOARCH>-<runtime.GOOS>
+ specifies a profile target in the following form: <arch>-<os>[@<version>]
+ -v=false
+ print more detailed information
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+
+Jiri profile install - Install the given profiles
+
+Install the given profiles.
+
+Usage:
+ jiri profile install [flags] <profiles>
+
+<profiles> is a list of profiles to install.
+
+The jiri profile install flags are:
+ -env=
+ specify an environment variable in the form: <var>=[<val>],...
+ -force=false
+ force install the profile even if it is already installed
+ -profiles-db=$JIRI_ROOT/.jiri_root/profile_db
+ specify the profiles database directory or file.
+ -profiles-dir=.jiri_root/profiles
+ the directory, relative to JIRI_ROOT, that profiles are installed in
+ -target=<runtime.GOARCH>-<runtime.GOOS>
+ specifies a profile target in the following form: <arch>-<os>[@<version>]
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri profile uninstall - Uninstall the given profiles
+
+Uninstall the given profiles.
+
+Usage:
+ jiri profile uninstall [flags] <profiles>
+
+<profiles> is a list of profiles to uninstall.
+
+The jiri profile uninstall flags are:
+ -all-targets=false
+ apply to all targets for the specified profile(s)
+ -profiles-db=$JIRI_ROOT/.jiri_root/profile_db
+ specify the profiles database directory or file.
+ -profiles-dir=.jiri_root/profiles
+ the directory, relative to JIRI_ROOT, that profiles are installed in
+ -target=<runtime.GOARCH>-<runtime.GOOS>
+ specifies a profile target in the following form: <arch>-<os>[@<version>]
+ -v=false
+ print more detailed information
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+
+Jiri profile update - Install the latest default version of the given profiles
+
+Install the latest default version of the given profiles.
+
+Usage:
+ jiri profile update [flags] <profiles>
+
+<profiles> is a list of profiles to update, if omitted all profiles are updated.
+
+The jiri profile update flags are:
+ -profiles-db=$JIRI_ROOT/.jiri_root/profile_db
+ specify the profiles database directory or file.
+ -profiles-dir=.jiri_root/profiles
+ the directory, relative to JIRI_ROOT, that profiles are installed in
+ -v=false
+ print more detailed information
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+
+Jiri profile cleanup - Cleanup the locally installed profiles
+
+Cleanup the locally installed profiles. This is generally required when
+recovering from earlier bugs or when preparing for a subsequent change to the
+profiles implementation.
+
+Usage:
+ jiri profile cleanup [flags] <profiles>
+
+<profiles> is a list of profiles to cleanup, if omitted all profiles are
+cleaned.
+
+The jiri profile cleanup flags are:
+ -gc=false
+ uninstall profile targets that are older than the current default
+ -profiles-db=$JIRI_ROOT/.jiri_root/profile_db
+ specify the profiles database directory or file.
+ -profiles-dir=.jiri_root/profiles
+ the directory, relative to JIRI_ROOT, that profiles are installed in
+ -rewrite-profiles-db=false
+ rewrite the profiles database to use the latest schema version
+ -rm-all=false
+ remove profiles database and all profile generated output files.
+ -v=false
+ print more detailed information
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+
+Jiri profile available - List the available profiles
+
+List the available profiles.
+
+Usage:
+ jiri profile available [flags]
+
+The jiri profile available flags are:
+ -v=false
+ print more detailed information
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+
+Jiri project - Manage the jiri projects
+
+Manage the jiri projects.
+
+Usage:
+ jiri project [flags] <command>
+
+The jiri project commands are:
+ clean Restore jiri projects to their pristine state
+ list List existing jiri projects and branches
+ shell-prompt Print a succinct status of projects suitable for shell prompts
+ poll Poll existing jiri projects
+
+The jiri project flags are:
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri project clean - Restore jiri projects to their pristine state
+
+Restore jiri projects back to their master branches and get rid of all the local
+branches and changes.
+
+Usage:
+ jiri project clean [flags] <project ...>
+
+<project ...> is a list of projects to clean up.
+
+The jiri project clean flags are:
+ -branches=false
+ Delete all non-master branches.
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri project list - List existing jiri projects and branches
+
+Inspect the local filesystem and list the existing projects and branches.
+
+Usage:
+ jiri project list [flags]
+
+The jiri project list flags are:
+ -branches=false
+ Show project branches.
+ -nopristine=false
+ If true, omit pristine projects, i.e. projects with a clean master branch and
+ no other branches.
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri project shell-prompt - Print a succinct status of projects suitable for shell prompts
+
+Reports current branches of jiri projects (repositories) as well as an
+indication of each project's status:
+ * indicates that a repository contains uncommitted changes
+ % indicates that a repository contains untracked files
+
+Usage:
+ jiri project shell-prompt [flags]
+
+The jiri project shell-prompt flags are:
+ -check-dirty=true
+ If false, don't check for uncommitted changes or untracked files. Setting
+ this option to false is dangerous: dirty master branches will not appear in
+ the output.
+ -show-name=false
+ Show the name of the current repo.
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri project poll - Poll existing jiri projects
+
+Poll jiri projects that can affect the outcome of the given tests and report
+whether any new changes in these projects exist. If no tests are specified, all
+projects are polled by default.
+
+Usage:
+ jiri project poll [flags] <test ...>
+
+<test ...> is a list of tests that determine what projects to poll.
+
+The jiri project poll flags are:
+ -manifest=
+ Name of the project manifest.
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri rebuild - Rebuild all jiri tools
+
+Rebuilds all jiri tools and installs the resulting binaries into
+$JIRI_ROOT/.jiri_root/bin. This is similar to "jiri update", but does not update
+any projects before building the tools. The set of tools to rebuild is described
+in the manifest.
+
+Run "jiri help manifest" for details on manifests.
+
+Usage:
+ jiri rebuild [flags]
+
+The jiri rebuild flags are:
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri snapshot - Manage project snapshots
+
+The "jiri snapshot" command can be used to manage project snapshots. In
+particular, it can be used to create new snapshots and to list existing
+snapshots.
+
+Usage:
+ jiri snapshot [flags] <command>
+
+The jiri snapshot commands are:
+ checkout Checkout a project snapshot
+ create Create a new project snapshot
+ list List existing project snapshots
+
+The jiri snapshot flags are:
+ -dir=
+ Directory where snapshot are stored. Defaults to $JIRI_ROOT/.snapshot.
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri snapshot checkout - Checkout a project snapshot
+
+The "jiri snapshot checkout <snapshot>" command restores local project state to
+the state in the given snapshot manifest.
+
+Usage:
+ jiri snapshot checkout [flags] <snapshot>
+
+<snapshot> is the snapshot manifest file.
+
+The jiri snapshot checkout flags are:
+ -gc=false
+ Garbage collect obsolete repositories.
+
+ -color=true
+ Use color to format output.
+ -dir=
+ Directory where snapshot are stored. Defaults to $JIRI_ROOT/.snapshot.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri snapshot create - Create a new project snapshot
+
+The "jiri snapshot create <label>" command captures the current project state in
+a manifest. If the -push-remote flag is provided, the snapshot is committed and
+pushed upstream.
+
+Internally, snapshots are organized as follows:
+
+ <snapshot-dir>/
+ labels/
+ <label1>/
+ <label1-snapshot1>
+ <label1-snapshot2>
+ ...
+ <label2>/
+ <label2-snapshot1>
+ <label2-snapshot2>
+ ...
+ <label3>/
+ ...
+ <label1> # a symlink to the latest <label1-snapshot*>
+ <label2> # a symlink to the latest <label2-snapshot*>
+ ...
+
+NOTE: Unlike the jiri tool commands, the above internal organization is not an
+API. It is an implementation and can change without notice.
+
+Usage:
+ jiri snapshot create [flags] <label>
+
+<label> is the snapshot label.
+
+The jiri snapshot create flags are:
+ -push-remote=false
+ Commit and push snapshot upstream.
+ -time-format=2006-01-02T15:04:05Z07:00
+ Time format for snapshot file name.
+
+ -color=true
+ Use color to format output.
+ -dir=
+ Directory where snapshot are stored. Defaults to $JIRI_ROOT/.snapshot.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri snapshot list - List existing project snapshots
+
+The "snapshot list" command lists existing snapshots of the labels specified as
+command-line arguments. If no arguments are provided, the command lists
+snapshots for all known labels.
+
+Usage:
+ jiri snapshot list [flags] <label ...>
+
+<label ...> is a list of snapshot labels.
+
+The jiri snapshot list flags are:
+ -color=true
+ Use color to format output.
+ -dir=
+ Directory where snapshot are stored. Defaults to $JIRI_ROOT/.snapshot.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri update - Update all jiri tools and projects
+
+Updates all projects, builds the latest version of all tools, and installs the
+resulting binaries into $JIRI_ROOT/.jiri_root/bin. The sequence in which the
+individual updates happen guarantees that we end up with a consistent set of
+tools and source code. The set of projects and tools to update is described in
+the manifest.
+
+Run "jiri help manifest" for details on manifests.
+
+Usage:
+ jiri update [flags]
+
+The jiri update flags are:
+ -attempts=1
+ Number of attempts before failing.
+ -gc=false
+ Garbage collect obsolete repositories.
+ -manifest=
+ Name of the project manifest.
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri upgrade - Upgrade jiri to new-style manifests
+
+Upgrades jiri to use new-style manifests.
+
+The old (deprecated) behavior only allowed a single manifest repository, located
+in $JIRI_ROOT/.manifest. The initial manifest file is located as follows:
+ 1) Use -manifest flag, if non-empty. If it's empty...
+ 2) Use $JIRI_ROOT/.local_manifest file. If it doesn't exist...
+ 3) Use $JIRI_ROOT/.manifest/v2/default.
+
+The new behavior allows multiple manifest repositories, by allowing imports to
+specify project attributes describing the remote repository. The -manifest flag
+is no longer allowed to be set; the initial manifest file is always located in
+$JIRI_ROOT/.jiri_manifest. The .local_manifest file is ignored.
+
+During the transition phase, both old and new behaviors are supported. The jiri
+tool uses the existence of the $JIRI_ROOT/.jiri_manifest file as the signal; if
+it exists we run the new behavior, otherwise we run the old behavior.
+
+The new behavior includes a "jiri import" command, which writes or updates the
+.jiri_manifest file. The new bootstrap procedure runs "jiri import", and it is
+intended as a regular command to add imports to your jiri environment.
+
+This upgrade command eases the transition by writing an initial .jiri_manifest
+file for you. If you have an existing .local_manifest file, its contents will
+be incorporated into the new .jiri_manifest file, and it will be renamed to
+.local_manifest.BACKUP. The -revert flag deletes the .jiri_manifest file, and
+restores the .local_manifest file.
+
+Usage:
+ jiri upgrade [flags] <kind>
+
+<kind> specifies the kind of upgrade, one of "v23" or "fuchsia".
+
+The jiri upgrade flags are:
+ -revert=false
+ Revert the upgrade by deleting the $JIRI_ROOT/.jiri_manifest file.
+
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri which - Show path to the jiri tool
+
+Which behaves similarly to the unix commandline tool. It is useful in
+determining whether the jiri binary is being run directly, or run via the jiri
+shim script.
+
+If the binary is being run directly, the output looks like this:
+
+ # binary
+ /path/to/binary/jiri
+
+If the script is being run, the output looks like this:
+
+ # script
+ /path/to/script/jiri
+
+Usage:
+ jiri which [flags]
+
+The jiri which flags are:
+ -color=true
+ Use color to format output.
+ -n=false
+ Show what commands will run but do not execute them.
+ -v=false
+ Print verbose output.
+
+Jiri help - Display help for commands or topics
+
+Help with no args displays the usage of the parent command.
+
+Help with args displays the usage of the specified sub-command or help topic.
+
+"help ..." recursively displays help for all commands and topics.
+
+Usage:
+ jiri help [flags] [command/topic ...]
+
+[command/topic ...] optionally identifies a specific sub-command or help topic.
+
+The jiri help flags are:
+ -style=compact
+ The formatting style for help output:
+ compact - Good for compact cmdline output.
+ full - Good for cmdline output, shows all global flags.
+ godoc - Good for godoc processing.
+ shortonly - Only output short description.
+ Override the default by setting the CMDLINE_STYLE environment variable.
+ -width=<terminal width>
+ Format output to this target width in runes, or unlimited if width < 0.
+ Defaults to the terminal width if available. Override the default by setting
+ the CMDLINE_WIDTH environment variable.
+
+Jiri filesystem - Description of jiri file system layout
+
+All data managed by the jiri tool is located in the file system under a root
+directory, colloquially called the jiri root directory. The file system layout
+looks like this:
+
+ [root] # root directory (name picked by user)
+ [root]/.jiri_root # root metadata directory
+ [root]/.jiri_root/bin # contains tool binaries (jiri, etc.)
+ [root]/.jiri_root/update_history # contains history of update snapshots
+ [root]/.manifest # contains jiri manifests
+ [root]/[project1] # project directory (name picked by user)
+ [root]/[project1]/.jiri # project metadata directory
+ [root]/[project1]/.jiri/metadata.v2 # project metadata file
+ [root]/[project1]/.jiri/<<cls>> # project per-cl metadata directories
+ [root]/[project1]/<<files>> # project files
+ [root]/[project2]...
+
+The [root] and [projectN] directory names are picked by the user. The <<cls>>
+are named via jiri cl new, and the <<files>> are named as the user adds files
+and directories to their project. All other names above have special meaning to
+the jiri tool, and cannot be changed; you must ensure your path names don't
+collide with these special names.
+
+There are two ways to run the jiri tool:
+
+1) Shim script (recommended approach). This is a shell script that looks for
+the [root] directory. If the JIRI_ROOT environment variable is set, that is
+assumed to be the [root] directory. Otherwise the script looks for the
+.jiri_root directory, starting in the current working directory and walking up
+the directory chain. The search is terminated successfully when the .jiri_root
+directory is found; it fails after it reaches the root of the file system. Thus
+the shim must be invoked from the [root] directory or one of its subdirectories.
+
+Once the [root] is found, the JIRI_ROOT environment variable is set to its
+location, and [root]/.jiri_root/bin/jiri is invoked. That file contains the
+actual jiri binary.
+
+The point of the shim script is to make it easy to use the jiri tool with
+multiple [root] directories on your file system. Keep in mind that when "jiri
+update" is run, the jiri tool itself is automatically updated along with all
+projects. By using the shim script, you only need to remember to invoke the
+jiri tool from within the appropriate [root] directory, and the projects and
+tools under that [root] directory will be updated.
+
+The shim script is located at [root]/release/go/src/v.io/jiri/scripts/jiri
+
+2) Direct binary. This is the jiri binary, containing all of the actual jiri
+tool logic. The binary requires the JIRI_ROOT environment variable to point to
+the [root] directory.
+
+Note that if you have multiple [root] directories on your file system, you must
+remember to run the jiri binary corresponding to the setting of your JIRI_ROOT
+environment variable. Things may fail if you mix things up, since the jiri
+binary is updated with each call to "jiri update", and you may encounter version
+mismatches between the jiri binary and the various metadata files or other
+logic. This is the reason the shim script is recommended over running the
+binary directly.
+
+The binary is located at [root]/.jiri_root/bin/jiri
+
+Jiri manifest - Description of manifest files
+
+Jiri manifests are revisioned and stored in a "manifest" repository, that is
+available locally in $JIRI_ROOT/.manifest. The manifest uses the following XML
+schema:
+
+ <manifest>
+ <imports>
+ <import name="default"/>
+ ...
+ </imports>
+ <projects>
+ <project name="release.go.jiri"
+ path="release/go/src/v.io/jiri"
+ protocol="git"
+ name="https://vanadium.googlesource.com/release.go.jiri"
+ revision="HEAD"/>
+ ...
+ </projects>
+ <tools>
+ <tool name="jiri" package="v.io/jiri"/>
+ ...
+ </tools>
+ </manifest>
+
+The <import> element can be used to share settings across multiple manifests.
+Import names are interpreted relative to the $JIRI_ROOT/.manifest/v2 directory.
+Import cycles are not allowed and if a project or a tool is specified multiple
+times, the last specification takes effect. In particular, the elements <project
+name="foo" exclude="true"/> and <tool name="bar" exclude="true"/> can be used to
+exclude previously included projects and tools.
+
+The tool identifies which manifest to use using the following algorithm. If the
+$JIRI_ROOT/.local_manifest file exists, then it is used. Otherwise, the
+$JIRI_ROOT/.manifest/v2/<manifest>.xml file is used, where <manifest> is the
+value of the -manifest command-line flag, which defaults to "default".
+*/
+package main
diff --git a/cmd/jiri/import.go b/cmd/jiri/import.go
new file mode 100644
index 0000000..7636f13
--- /dev/null
+++ b/cmd/jiri/import.go
@@ -0,0 +1,101 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "os"
+
+ "v.io/jiri/jiri"
+ "v.io/jiri/project"
+ "v.io/jiri/runutil"
+ "v.io/x/lib/cmdline"
+)
+
+var (
+ // Flags for configuring project attributes for remote imports.
+ flagImportName, flagImportProtocol, flagImportRemoteBranch, flagImportRoot string
+ // Flags for controlling the behavior of the command.
+ flagImportOverwrite bool
+ flagImportOut string
+)
+
+func init() {
+ cmdImport.Flags.StringVar(&flagImportName, "name", "", `The name of the remote manifest project, used to disambiguate manifest projects with the same remote. Typically empty.`)
+ cmdImport.Flags.StringVar(&flagImportProtocol, "protocol", "git", `The version control protocol used by the remote manifest project.`)
+ cmdImport.Flags.StringVar(&flagImportRemoteBranch, "remote-branch", "master", `The branch of the remote manifest project to track, without the leading "origin/".`)
+ cmdImport.Flags.StringVar(&flagImportRoot, "root", "", `Root to store the manifest project locally.`)
+
+ cmdImport.Flags.BoolVar(&flagImportOverwrite, "overwrite", false, `Write a new .jiri_manifest file with the given specification. If it already exists, the existing content will be ignored and the file will be overwritten.`)
+ cmdImport.Flags.StringVar(&flagImportOut, "out", "", `The output file. Uses $JIRI_ROOT/.jiri_manifest if unspecified. Uses stdout if set to "-".`)
+}
+
+var cmdImport = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runImport),
+ Name: "import",
+ Short: "Adds imports to .jiri_manifest file",
+ Long: `
+Command "import" adds imports to the $JIRI_ROOT/.jiri_manifest file, which
+specifies manifest information for the jiri tool. The file is created if it
+doesn't already exist, otherwise additional imports are added to the existing
+file.
+
+An <import> element is added to the manifest representing a remote manifest
+import. The manifest file path is relative to the root directory of the remote
+import repository.
+
+Example:
+ $ jiri import myfile https://foo.com/bar.git
+
+Run "jiri help manifest" for details on manifests.
+`,
+ ArgsName: "<manifest> <remote>",
+ ArgsLong: `
+<manifest> specifies the manifest file to use.
+
+<remote> specifies the remote manifest repository.
+`,
+}
+
+func runImport(jirix *jiri.X, args []string) error {
+ if len(args) != 2 {
+ return jirix.UsageErrorf("wrong number of arguments")
+ }
+ // Initialize manifest.
+ var manifest *project.Manifest
+ if !flagImportOverwrite {
+ m, err := project.ManifestFromFile(jirix, jirix.JiriManifestFile())
+ if err != nil && !runutil.IsNotExist(err) {
+ return err
+ }
+ manifest = m
+ }
+ if manifest == nil {
+ manifest = &project.Manifest{}
+ }
+ // There's not much error checking when writing the .jiri_manifest file;
+ // errors will be reported when "jiri update" is run.
+ manifest.Imports = append(manifest.Imports, project.Import{
+ Manifest: args[0],
+ Name: flagImportName,
+ Protocol: flagImportProtocol,
+ Remote: args[1],
+ RemoteBranch: flagImportRemoteBranch,
+ Root: flagImportRoot,
+ })
+ // Write output to stdout or file.
+ outFile := flagImportOut
+ if outFile == "" {
+ outFile = jirix.JiriManifestFile()
+ }
+ if outFile == "-" {
+ bytes, err := manifest.ToBytes()
+ if err != nil {
+ return err
+ }
+ _, err = os.Stdout.Write(bytes)
+ return err
+ }
+ return manifest.ToFile(jirix, outFile)
+}
diff --git a/cmd/jiri/import_test.go b/cmd/jiri/import_test.go
new file mode 100644
index 0000000..7b13b97
--- /dev/null
+++ b/cmd/jiri/import_test.go
@@ -0,0 +1,199 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "v.io/jiri/jiri"
+ "v.io/x/lib/gosh"
+)
+
+type importTestCase struct {
+ Args []string
+ Filename string
+ Exist, Want string
+ Stdout, Stderr string
+}
+
+func TestImport(t *testing.T) {
+ tests := []importTestCase{
+ {
+ Stderr: `wrong number of arguments`,
+ },
+ {
+ Args: []string{"a"},
+ Stderr: `wrong number of arguments`,
+ },
+ {
+ Args: []string{"a", "b", "c"},
+ Stderr: `wrong number of arguments`,
+ },
+ // Remote imports, default append behavior
+ {
+ Args: []string{"-name=name", "-remote-branch=remotebranch", "-root=root", "foo", "https://github.com/new.git"},
+ Want: `<manifest>
+ <imports>
+ <import manifest="foo" name="name" remote="https://github.com/new.git" remotebranch="remotebranch" root="root"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"foo", "https://github.com/new.git"},
+ Want: `<manifest>
+ <imports>
+ <import manifest="foo" remote="https://github.com/new.git"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"-out=file", "foo", "https://github.com/new.git"},
+ Filename: `file`,
+ Want: `<manifest>
+ <imports>
+ <import manifest="foo" remote="https://github.com/new.git"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"-out=-", "foo", "https://github.com/new.git"},
+ Stdout: `<manifest>
+ <imports>
+ <import manifest="foo" remote="https://github.com/new.git"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"foo", "https://github.com/new.git"},
+ Exist: `<manifest>
+ <imports>
+ <import manifest="bar" remote="https://github.com/orig.git"/>
+ </imports>
+</manifest>
+`,
+ Want: `<manifest>
+ <imports>
+ <import manifest="bar" remote="https://github.com/orig.git"/>
+ <import manifest="foo" remote="https://github.com/new.git"/>
+ </imports>
+</manifest>
+`,
+ },
+ // Remote imports, explicit overwrite behavior
+ {
+ Args: []string{"-overwrite", "foo", "https://github.com/new.git"},
+ Want: `<manifest>
+ <imports>
+ <import manifest="foo" remote="https://github.com/new.git"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"-overwrite", "-out=file", "foo", "https://github.com/new.git"},
+ Filename: `file`,
+ Want: `<manifest>
+ <imports>
+ <import manifest="foo" remote="https://github.com/new.git"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"-overwrite", "-out=-", "foo", "https://github.com/new.git"},
+ Stdout: `<manifest>
+ <imports>
+ <import manifest="foo" remote="https://github.com/new.git"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"-overwrite", "foo", "https://github.com/new.git"},
+ Exist: `<manifest>
+ <imports>
+ <import manifest="bar" remote="https://github.com/orig.git"/>
+ </imports>
+</manifest>
+`,
+ Want: `<manifest>
+ <imports>
+ <import manifest="foo" remote="https://github.com/new.git"/>
+ </imports>
+</manifest>
+`,
+ },
+ }
+ opts := gosh.Opts{Fatalf: t.Fatalf, Logf: t.Logf}
+ sh := gosh.NewShell(opts)
+ defer sh.Cleanup()
+ jiriTool := sh.BuildGoPkg("v.io/jiri")
+ for _, test := range tests {
+ if err := testImport(opts, jiriTool, test); err != nil {
+ t.Errorf("%v: %v", test.Args, err)
+ }
+ }
+}
+
+func testImport(opts gosh.Opts, jiriTool string, test importTestCase) error {
+ sh := gosh.NewShell(opts)
+ defer sh.Cleanup()
+ tmpDir := sh.MakeTempDir()
+ jiriRoot := filepath.Join(tmpDir, "root")
+ if err := os.Mkdir(jiriRoot, 0755); err != nil {
+ return err
+ }
+ sh.Pushd(jiriRoot)
+ filename := test.Filename
+ if filename == "" {
+ filename = ".jiri_manifest"
+ }
+ // Set up manfile for the local file import tests. It should exist in both
+ // the tmpDir (for ../manfile tests) and jiriRoot.
+ for _, dir := range []string{tmpDir, jiriRoot} {
+ if err := ioutil.WriteFile(filepath.Join(dir, "manfile"), nil, 0644); err != nil {
+ return err
+ }
+ }
+ // Set up an existing file if it was specified.
+ if test.Exist != "" {
+ if err := ioutil.WriteFile(filename, []byte(test.Exist), 0644); err != nil {
+ return err
+ }
+ }
+ // Run import and check the error.
+ sh.Vars[jiri.RootEnv] = jiriRoot
+ cmd := sh.Cmd(jiriTool, append([]string{"import"}, test.Args...)...)
+ if test.Stderr != "" {
+ cmd.ExitErrorIsOk = true
+ }
+ stdout, stderr := cmd.StdoutStderr()
+ if got, want := stdout, test.Stdout; !strings.Contains(got, want) || (got != "" && want == "") {
+ return fmt.Errorf("stdout got %q, want substr %q", got, want)
+ }
+ if got, want := stderr, test.Stderr; !strings.Contains(got, want) || (got != "" && want == "") {
+ return fmt.Errorf("stderr got %q, want substr %q", got, want)
+ }
+ // Make sure the right file is generated.
+ if test.Want != "" {
+ data, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return err
+ }
+ if got, want := string(data), test.Want; got != want {
+ return fmt.Errorf("GOT\n%s\nWANT\n%s", got, want)
+ }
+ }
+ return nil
+}
diff --git a/cmd/jiri/profile.go b/cmd/jiri/profile.go
new file mode 100644
index 0000000..a472a09
--- /dev/null
+++ b/cmd/jiri/profile.go
@@ -0,0 +1,22 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "v.io/jiri/jiri"
+ "v.io/jiri/profiles/profilescmdline"
+ "v.io/x/lib/cmdline"
+)
+
+var cmdProfile = &cmdline.Command{
+ Name: "profile",
+ Short: "Display information about installed profiles",
+ Long: "Display information about installed profiles and their configuration.",
+}
+
+func init() {
+ profilescmdline.RegisterReaderCommands(cmdProfile, jiri.ProfilesDBDir)
+ profilescmdline.RegisterManagementCommands(cmdProfile, "", jiri.ProfilesDBDir, jiri.ProfilesRootDir)
+}
diff --git a/cmd/jiri/project.go b/cmd/jiri/project.go
new file mode 100644
index 0000000..ea94798
--- /dev/null
+++ b/cmd/jiri/project.go
@@ -0,0 +1,252 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "path/filepath"
+ "sort"
+ "strings"
+
+ "v.io/jiri/jiri"
+ "v.io/jiri/project"
+ "v.io/jiri/tool"
+ "v.io/jiri/util"
+ "v.io/x/lib/cmdline"
+ "v.io/x/lib/set"
+)
+
+var (
+ branchesFlag bool
+ cleanupBranchesFlag bool
+ noPristineFlag bool
+ checkDirtyFlag bool
+ showNameFlag bool
+)
+
+func init() {
+ cmdProjectClean.Flags.BoolVar(&cleanupBranchesFlag, "branches", false, "Delete all non-master branches.")
+ cmdProjectList.Flags.BoolVar(&branchesFlag, "branches", false, "Show project branches.")
+ cmdProjectList.Flags.BoolVar(&noPristineFlag, "nopristine", false, "If true, omit pristine projects, i.e. projects with a clean master branch and no other branches.")
+ cmdProjectShellPrompt.Flags.BoolVar(&checkDirtyFlag, "check-dirty", true, "If false, don't check for uncommitted changes or untracked files. Setting this option to false is dangerous: dirty master branches will not appear in the output.")
+ cmdProjectShellPrompt.Flags.BoolVar(&showNameFlag, "show-name", false, "Show the name of the current repo.")
+
+ tool.InitializeProjectFlags(&cmdProjectPoll.Flags)
+
+}
+
+// cmdProject represents the "jiri project" command.
+var cmdProject = &cmdline.Command{
+ Name: "project",
+ Short: "Manage the jiri projects",
+ Long: "Manage the jiri projects.",
+ Children: []*cmdline.Command{cmdProjectClean, cmdProjectList, cmdProjectShellPrompt, cmdProjectPoll},
+}
+
+// cmdProjectClean represents the "jiri project clean" command.
+var cmdProjectClean = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runProjectClean),
+ Name: "clean",
+ Short: "Restore jiri projects to their pristine state",
+ Long: "Restore jiri projects back to their master branches and get rid of all the local branches and changes.",
+ ArgsName: "<project ...>",
+ ArgsLong: "<project ...> is a list of projects to clean up.",
+}
+
+func runProjectClean(jirix *jiri.X, args []string) (e error) {
+ localProjects, err := project.LocalProjects(jirix, project.FullScan)
+ if err != nil {
+ return err
+ }
+ var projects project.Projects
+ if len(args) > 0 {
+ for _, arg := range args {
+ p, err := localProjects.FindUnique(arg)
+ if err != nil {
+ fmt.Fprintf(jirix.Stderr(), "Error finding local project %q: %v.\n", p.Name, err)
+ } else {
+ projects[p.Key()] = p
+ }
+ }
+ } else {
+ projects = localProjects
+ }
+ if err := project.CleanupProjects(jirix, projects, cleanupBranchesFlag); err != nil {
+ return err
+ }
+ return nil
+}
+
+// cmdProjectList represents the "jiri project list" command.
+var cmdProjectList = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runProjectList),
+ Name: "list",
+ Short: "List existing jiri projects and branches",
+ Long: "Inspect the local filesystem and list the existing projects and branches.",
+}
+
+// runProjectList generates a listing of local projects.
+func runProjectList(jirix *jiri.X, _ []string) error {
+ states, err := project.GetProjectStates(jirix, noPristineFlag)
+ if err != nil {
+ return err
+ }
+ var keys project.ProjectKeys
+ for key := range states {
+ keys = append(keys, key)
+ }
+ sort.Sort(keys)
+
+ for _, key := range keys {
+ state := states[key]
+ if noPristineFlag {
+ pristine := len(state.Branches) == 1 && state.CurrentBranch == "master" && !state.HasUncommitted && !state.HasUntracked
+ if pristine {
+ continue
+ }
+ }
+ fmt.Fprintf(jirix.Stdout(), "name=%q remote=%q path=%q\n", state.Project.Name, state.Project.Remote, state.Project.Path)
+ if branchesFlag {
+ for _, branch := range state.Branches {
+ s := " "
+ if branch.Name == state.CurrentBranch {
+ s += "* "
+ }
+ s += branch.Name
+ if branch.HasGerritMessage {
+ s += " (exported to gerrit)"
+ }
+ fmt.Fprintf(jirix.Stdout(), "%v\n", s)
+ }
+ }
+ }
+ return nil
+}
+
+// cmdProjectShellPrompt represents the "jiri project shell-prompt" command.
+var cmdProjectShellPrompt = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runProjectShellPrompt),
+ Name: "shell-prompt",
+ Short: "Print a succinct status of projects suitable for shell prompts",
+ Long: `
+Reports current branches of jiri projects (repositories) as well as an
+indication of each project's status:
+ * indicates that a repository contains uncommitted changes
+ % indicates that a repository contains untracked files
+`,
+}
+
+func runProjectShellPrompt(jirix *jiri.X, args []string) error {
+ states, err := project.GetProjectStates(jirix, checkDirtyFlag)
+ if err != nil {
+ return err
+ }
+ var keys project.ProjectKeys
+ for key := range states {
+ keys = append(keys, key)
+ }
+ sort.Sort(keys)
+
+ // Get the key of the current project.
+ currentProjectKey, err := project.CurrentProjectKey(jirix)
+ if err != nil {
+ return err
+ }
+ var statuses []string
+ for _, key := range keys {
+ state := states[key]
+ status := ""
+ if checkDirtyFlag {
+ if state.HasUncommitted {
+ status += "*"
+ }
+ if state.HasUntracked {
+ status += "%"
+ }
+ }
+ short := state.CurrentBranch + status
+ long := filepath.Base(states[key].Project.Name) + ":" + short
+ if key == currentProjectKey {
+ if showNameFlag {
+ statuses = append([]string{long}, statuses...)
+ } else {
+ statuses = append([]string{short}, statuses...)
+ }
+ } else {
+ pristine := state.CurrentBranch == "master"
+ if checkDirtyFlag {
+ pristine = pristine && !state.HasUncommitted && !state.HasUntracked
+ }
+ if !pristine {
+ statuses = append(statuses, long)
+ }
+ }
+ }
+ fmt.Println(strings.Join(statuses, ","))
+ return nil
+}
+
+// cmdProjectPoll represents the "jiri project poll" command.
+var cmdProjectPoll = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runProjectPoll),
+ Name: "poll",
+ Short: "Poll existing jiri projects",
+ Long: `
+Poll jiri projects that can affect the outcome of the given tests
+and report whether any new changes in these projects exist. If no
+tests are specified, all projects are polled by default.
+`,
+ ArgsName: "<test ...>",
+ ArgsLong: "<test ...> is a list of tests that determine what projects to poll.",
+}
+
+// runProjectPoll generates a description of changes that exist
+// remotely but do not exist locally.
+func runProjectPoll(jirix *jiri.X, args []string) error {
+ projectSet := map[string]struct{}{}
+ if len(args) > 0 {
+ config, err := util.LoadConfig(jirix)
+ if err != nil {
+ return err
+ }
+ // Compute a map from tests to projects that can change the
+ // outcome of the test.
+ testProjects := map[string][]string{}
+ for _, project := range config.Projects() {
+ for _, test := range config.ProjectTests([]string{project}) {
+ testProjects[test] = append(testProjects[test], project)
+ }
+ }
+ for _, arg := range args {
+ projects, ok := testProjects[arg]
+ if !ok {
+ return fmt.Errorf("failed to find any projects for test %q", arg)
+ }
+ set.String.Union(projectSet, set.String.FromSlice(projects))
+ }
+ }
+ update, err := project.PollProjects(jirix, projectSet)
+ if err != nil {
+ return err
+ }
+
+ // Remove projects with empty changes.
+ for project := range update {
+ if changes := update[project]; len(changes) == 0 {
+ delete(update, project)
+ }
+ }
+
+ // Print update if it is not empty.
+ if len(update) > 0 {
+ bytes, err := json.MarshalIndent(update, "", " ")
+ if err != nil {
+ return fmt.Errorf("MarshalIndent() failed: %v", err)
+ }
+ fmt.Fprintf(jirix.Stdout(), "%s\n", bytes)
+ }
+ return nil
+}
diff --git a/cmd/jiri/rebuild.go b/cmd/jiri/rebuild.go
new file mode 100644
index 0000000..dbf30bc
--- /dev/null
+++ b/cmd/jiri/rebuild.go
@@ -0,0 +1,56 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+
+ "v.io/jiri/collect"
+ "v.io/jiri/jiri"
+ "v.io/jiri/project"
+ "v.io/x/lib/cmdline"
+)
+
+// cmdRebuild represents the "jiri rebuild" command.
+var cmdRebuild = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runRebuild),
+ Name: "rebuild",
+ Short: "Rebuild all jiri tools",
+ Long: `
+Rebuilds all jiri tools and installs the resulting binaries into
+$JIRI_ROOT/.jiri_root/bin. This is similar to "jiri update", but does not update
+any projects before building the tools. The set of tools to rebuild is described
+in the manifest.
+
+Run "jiri help manifest" for details on manifests.
+`,
+}
+
+func runRebuild(jirix *jiri.X, args []string) (e error) {
+ projects, tools, err := project.LoadManifest(jirix)
+ if err != nil {
+ return err
+ }
+
+ // Create a temporary directory in which tools will be built.
+ tmpDir, err := jirix.NewSeq().TempDir("", "tmp-jiri-rebuild")
+ if err != nil {
+ return fmt.Errorf("TempDir() failed: %v", err)
+ }
+
+ // Make sure we cleanup the temp directory.
+ defer collect.Error(func() error { return jirix.NewSeq().RemoveAll(tmpDir).Done() }, &e)
+
+ // Paranoid sanity checking.
+ if _, ok := tools[project.JiriName]; !ok {
+ return fmt.Errorf("tool %q not found", project.JiriName)
+ }
+
+ // Build and install tools.
+ if err := project.BuildTools(jirix, projects, tools, tmpDir); err != nil {
+ return err
+ }
+ return project.InstallTools(jirix, tmpDir)
+}
diff --git a/cmd/jiri/scripts/bootstrap_jiri b/cmd/jiri/scripts/bootstrap_jiri
new file mode 100755
index 0000000..8f00a2f
--- /dev/null
+++ b/cmd/jiri/scripts/bootstrap_jiri
@@ -0,0 +1,76 @@
+#!/bin/bash
+# Copyright 2015 The Vanadium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# bootstrap_jiri initializes a root directory for jiri. The following
+# directories and files will be created:
+# <root_dir> - root directory (picked by user)
+# <root_dir>/.jiri_root - root metadata directory
+# <root_dir>/.jiri_root/bin/jiri - jiri binary
+# <root_dir>/.jiri_root/scripts/jiri - jiri script
+#
+# The jiri sources are downloaded and built into a temp directory, which is
+# always deleted when this script finishes. The <root_dir> is deleted on any
+# failure.
+
+set -euf -o pipefail
+
+# fatal prints an error message, followed by the usage string, and then exits.
+fatal() {
+ usage='
+
+Usage:
+ bootstrap_jiri <root_dir>
+
+A typical bootstrap workflow looks like this:
+
+$ curl -s https://raw.githubusercontent.com/vanadium/go.jiri/master/scripts/bootstrap_jiri | bash -s myroot
+$ export PATH=myroot/.jiri_root/scripts:$PATH
+$ cd myroot
+$ jiri import public https://vanadium.googlesource.com/manifest
+$ jiri update'
+ echo "ERROR: $@${usage}" 1>&2
+ exit 1
+}
+
+# toabs converts the possibly relative argument into an absolute path. Run in a
+# subshell to avoid changing the caller's working directory.
+toabs() (
+ cd $(dirname $1)
+ echo ${PWD}/$(basename $1)
+)
+
+# Check the <root_dir> argument is supplied and doesn't already exist.
+if [[ $# -ne 1 ]]; then
+ fatal "need <root_dir> argument"
+fi
+mkdir -p $(dirname $1)
+root_dir=$(toabs $1)
+if [[ -e ${root_dir} ]]; then
+ fatal "${root_dir} already exists"
+fi
+# Check that go is on the PATH.
+if ! go version >& /dev/null ; then
+ fatal 'ERROR: "go" tool not found, see https://golang.org/doc/install'
+fi
+
+trap "rm -rf ${root_dir}" INT TERM EXIT
+
+# Make the output directories.
+tmp_dir="${root_dir}/.jiri_root/tmp"
+bin_dir="${root_dir}/.jiri_root/bin"
+scripts_dir="${root_dir}/.jiri_root/scripts"
+mkdir -p "${tmp_dir}" "${bin_dir}" "${scripts_dir}"
+
+# Go get the jiri source files, build the jiri binary, and copy the jiri shim
+# script from the sources.
+GOPATH="${tmp_dir}" go get -d v.io/jiri
+GOPATH="${tmp_dir}" go build -o "${bin_dir}/jiri" v.io/jiri
+cp "${tmp_dir}/src/v.io/jiri/scripts/jiri" "${scripts_dir}/jiri"
+
+# Clean up the tmp_dir.
+rm -rf "${tmp_dir}"
+
+echo "Please add ${scripts_dir} to your PATH."
+trap - EXIT
diff --git a/cmd/jiri/scripts/jiri b/cmd/jiri/scripts/jiri
new file mode 100755
index 0000000..6684669
--- /dev/null
+++ b/cmd/jiri/scripts/jiri
@@ -0,0 +1,61 @@
+#!/bin/bash
+# Copyright 2015 The Vanadium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# jiri is a shim script that determines the jiri root directory and invokes
+# $JIRI_ROOT/.jiri_root/bin/jiri with the given arguments.
+#
+# If the JIRI_ROOT environment variable is set, that is assumed to be the jiri
+# root directory.
+#
+# Otherwise the script looks for the .jiri_root directory, starting in the
+# current working directory and walking up the directory chain. The search is
+# terminated successfully when the .jiri_root directory is found; it fails after
+# it reaches the root of the file system.
+#
+# This script should be invoked from the jiri root directory or one of its
+# subdirectories, unless the JIRI_ROOT environment variable is set.
+
+set -euf -o pipefail
+
+# fatal prints an error message and exits.
+fatal() {
+ echo "ERROR: $@" 1>&2
+ exit 1
+}
+
+# Handle "jiri which" without any arguments. This is handy to determine whether
+# the PATH is set up pointing at this shim script, or a regular binary. If
+# there are any arguments, we pass the command through to the binary.
+if [[ $# -eq 1 ]]; then
+ if [[ "$1" == "which" ]]; then
+ echo "# script"
+ type -p $0
+ exit 0
+ fi
+fi
+
+# If $JIRI_ROOT is set we always use it, otherwise look for a .jiri_root
+# directory starting with the current working directory, and walking up.
+if [[ -z ${JIRI_ROOT+x} ]]; then
+ CWD="$(pwd)"
+ while [[ ! -d "$(pwd)/.jiri_root" ]]; do
+ if [[ "$(pwd)" == "/" ]]; then
+ fatal "could not find .jiri_root directory"
+ fi
+ cd ..
+ done
+ export JIRI_ROOT="$(pwd)"
+ cd "${CWD}"
+fi
+
+# Make sure the jiri binary exists and is executable.
+if [[ ! -e "${JIRI_ROOT}/.jiri_root/bin/jiri" ]]; then
+ fatal "${JIRI_ROOT}/.jiri_root/bin/jiri does not exist"
+elif [[ ! -x "${JIRI_ROOT}/.jiri_root/bin/jiri" ]]; then
+ fatal "${JIRI_ROOT}/.jiri_root/bin/jiri is not executable"
+fi
+
+# Execute the jiri binary.
+exec "${JIRI_ROOT}/.jiri_root/bin/jiri" "$@"
diff --git a/cmd/jiri/scripts/jiri-bash-completion.sh b/cmd/jiri/scripts/jiri-bash-completion.sh
new file mode 100644
index 0000000..92e7e27
--- /dev/null
+++ b/cmd/jiri/scripts/jiri-bash-completion.sh
@@ -0,0 +1,107 @@
+#!/bin/bash
+# Copyright 2015 The Vanadium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+# Check for bash
+[[ -z "$BASH_VERSION" ]] && return
+
+# Extracts subcommands from the help output of a given command ($1).
+# An example help output from "jiri help":
+# ...
+# The jiri commands are:
+# build Tool for managing vanadium builds
+# contributors List vanadium project contributors
+# ...
+# Run "jiri help [command]" for command usage.
+# ...
+_jiri_extract_subcommands() {
+ local -r CMD="$1"
+ local -r START_LINE="The .* commands are"
+ local -r END_LINE="Run \"jiri"
+ ${CMD} -h > /dev/null 2>&1
+ if [[ "$?" = 0 ]]; then
+ ${CMD} -h \
+ | sed -n "/${START_LINE}/,/${END_LINE}/p" | grep '^ '| awk '{print $1}' | sort
+ fi
+}
+
+# Extracts flags from the help output of a given command ($1).
+# Note that we ignore all "global" flags.
+# An example help output from "jiri help update":
+# ...
+# The jiri update flags are:
+# -gc=false
+# Garbage collect obsolete repositories.
+# -n=false
+# ...
+_jiri_extract_flags() {
+ local -r CMD="$1"
+ local -r START_LINE="The .* flags are"
+ local -r END_LINE="^$"
+ ${CMD} -h > /dev/null 2>&1
+ if [[ "$?" = 0 ]]; then
+ ${CMD} -h | sed -n "/${START_LINE}/,/${END_LINE}/p" | grep '^ -'| cut -d= -f1 | tr -d " " | sort
+ fi
+}
+
+# Gets the command line before the word where the current cursor is.
+_jiri_get_cmdline_before_cursor() {
+ local i
+ local CMD=""
+ for(( i=0; i<COMP_CWORD; i++ )); do
+ CMD="${CMD} ${COMP_WORDS[i]}"
+ done
+ echo "${CMD}"
+}
+
+# Gets the names of available projects.
+_jiri_get_project_names() {
+ jiri project list | sed "s/^.*project=\"\(.*\)\" path.*/\1/"
+}
+
+
+# Main bash completion function for the "jiri" command.
+#
+# Completion-related internal Bash vars:
+# - COMP_WORDS: An array variable consisting of the individual words in the
+# current command line
+# - COMP_CWORD: An index into ${COMP_WORDS} of the word containing the current cursor
+# position.
+# - COMPREPLY An array variable from which bash reads the possible completions
+# generated by a completion function.
+_jiri_complete() {
+ local -r CUR="${COMP_WORDS[COMP_CWORD]}"
+ local -r CMD="$(_jiri_get_cmdline_before_cursor)"
+
+ case "${CUR}" in
+ -*)
+ # Complete flags.
+ COMPREPLY=($(compgen -W "$(_jiri_extract_flags "${CMD}")" -- ${CUR}))
+ ;;
+ *)
+ # Complete commands.
+ if [[ "${COMP_LINE}" =~ .*test(\ )+project.* ]]; then
+ # Special handling for completing "jiri test project".
+ COMPREPLY=($(compgen -W "$(_jiri_get_project_names)" -- ${CUR}))
+ elif [[ "${COMP_LINE}" =~ .*test(\ )+run.* ]]; then
+ # Special handling for completing "jiri test run".
+ COMPREPLY=($(compgen -W "$(jiri test list)" -- ${CUR}))
+ else
+ COMPREPLY=($(compgen -W "$(_jiri_extract_subcommands "${CMD}")" -- ${CUR}))
+ fi
+ ;;
+ esac
+
+ return 0
+}
+
+# Main bash completion function for the "vcd" command.
+_jiri_vcd_complete() {
+ local -r CUR="${COMP_WORDS[COMP_CWORD]}"
+
+ COMPREPLY=($(compgen -W "$(_jiri_get_project_names)" -- ${CUR}))
+}
+
+complete -F _jiri_complete jiri
+complete -F _jiri_vcd_complete vcd
diff --git a/cmd/jiri/snapshot.go b/cmd/jiri/snapshot.go
new file mode 100644
index 0000000..de89012
--- /dev/null
+++ b/cmd/jiri/snapshot.go
@@ -0,0 +1,300 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "v.io/jiri/collect"
+ "v.io/jiri/gitutil"
+ "v.io/jiri/jiri"
+ "v.io/jiri/project"
+ "v.io/jiri/runutil"
+ "v.io/x/lib/cmdline"
+)
+
+const (
+ defaultSnapshotDir = ".snapshot"
+)
+
+var (
+ pushRemoteFlag bool
+ snapshotDirFlag string
+ snapshotGcFlag bool
+ timeFormatFlag string
+)
+
+func init() {
+ cmdSnapshot.Flags.StringVar(&snapshotDirFlag, "dir", "", "Directory where snapshot are stored. Defaults to $JIRI_ROOT/.snapshot.")
+ cmdSnapshotCheckout.Flags.BoolVar(&snapshotGcFlag, "gc", false, "Garbage collect obsolete repositories.")
+ cmdSnapshotCreate.Flags.BoolVar(&pushRemoteFlag, "push-remote", false, "Commit and push snapshot upstream.")
+ cmdSnapshotCreate.Flags.StringVar(&timeFormatFlag, "time-format", time.RFC3339, "Time format for snapshot file name.")
+}
+
+var cmdSnapshot = &cmdline.Command{
+ Name: "snapshot",
+ Short: "Manage project snapshots",
+ Long: `
+The "jiri snapshot" command can be used to manage project snapshots.
+In particular, it can be used to create new snapshots and to list
+existing snapshots.
+`,
+ Children: []*cmdline.Command{cmdSnapshotCheckout, cmdSnapshotCreate, cmdSnapshotList},
+}
+
+// cmdSnapshotCreate represents the "jiri snapshot create" command.
+var cmdSnapshotCreate = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runSnapshotCreate),
+ Name: "create",
+ Short: "Create a new project snapshot",
+ Long: `
+The "jiri snapshot create <label>" command captures the current project state
+in a manifest. If the -push-remote flag is provided, the snapshot is committed
+and pushed upstream.
+
+Internally, snapshots are organized as follows:
+
+ <snapshot-dir>/
+ labels/
+ <label1>/
+ <label1-snapshot1>
+ <label1-snapshot2>
+ ...
+ <label2>/
+ <label2-snapshot1>
+ <label2-snapshot2>
+ ...
+ <label3>/
+ ...
+ <label1> # a symlink to the latest <label1-snapshot*>
+ <label2> # a symlink to the latest <label2-snapshot*>
+ ...
+
+NOTE: Unlike the jiri tool commands, the above internal organization
+is not an API. It is an implementation and can change without notice.
+`,
+ ArgsName: "<label>",
+ ArgsLong: "<label> is the snapshot label.",
+}
+
+func runSnapshotCreate(jirix *jiri.X, args []string) error {
+ if len(args) != 1 {
+ return jirix.UsageErrorf("unexpected number of arguments")
+ }
+ label := args[0]
+ snapshotDir, err := getSnapshotDir(jirix)
+ if err != nil {
+ return err
+ }
+ snapshotFile := filepath.Join(snapshotDir, "labels", label, time.Now().Format(timeFormatFlag))
+
+ if !pushRemoteFlag {
+ // No git operations necessary. Just create the snapshot file.
+ return createSnapshot(jirix, snapshotDir, snapshotFile, label)
+ }
+
+ // Attempt to create a snapshot on a clean master branch. If snapshot
+ // creation fails, return to the state we were in before.
+ createFn := func() error {
+ git := gitutil.New(jirix.NewSeq())
+ revision, err := git.CurrentRevision()
+ if err != nil {
+ return err
+ }
+ if err := createSnapshot(jirix, snapshotDir, snapshotFile, label); err != nil {
+ git.Reset(revision)
+ git.RemoveUntrackedFiles()
+ return err
+ }
+ return commitAndPushChanges(jirix, snapshotDir, snapshotFile, label)
+ }
+
+ // Execute the above function in the snapshot directory on a clean master branch.
+ p := project.Project{
+ Path: snapshotDir,
+ Protocol: "git",
+ RemoteBranch: "master",
+ Revision: "HEAD",
+ }
+ return project.ApplyToLocalMaster(jirix, project.Projects{p.Key(): p}, createFn)
+}
+
+// getSnapshotDir returns the path to the snapshot directory, creating it if
+// necessary.
+func getSnapshotDir(jirix *jiri.X) (string, error) {
+ dir := snapshotDirFlag
+ if dir == "" {
+ dir = filepath.Join(jirix.Root, defaultSnapshotDir)
+ }
+
+ if !filepath.IsAbs(dir) {
+ cwd, err := os.Getwd()
+ if err != nil {
+ return "", err
+ }
+ dir = filepath.Join(cwd, dir)
+ }
+
+ // Make sure directory exists.
+ if err := jirix.NewSeq().MkdirAll(dir, 0755).Done(); err != nil {
+ return "", err
+ }
+ return dir, nil
+}
+
+func createSnapshot(jirix *jiri.X, snapshotDir, snapshotFile, label string) error {
+ // Create a snapshot that encodes the current state of master
+ // branches for all local projects.
+ if err := project.CreateSnapshot(jirix, snapshotFile); err != nil {
+ return err
+ }
+
+ s := jirix.NewSeq()
+ // Update the symlink for this snapshot label to point to the
+ // latest snapshot.
+ symlink := filepath.Join(snapshotDir, label)
+ newSymlink := symlink + ".new"
+ relativeSnapshotPath := strings.TrimPrefix(snapshotFile, snapshotDir+string(os.PathSeparator))
+ return s.RemoveAll(newSymlink).
+ Symlink(relativeSnapshotPath, newSymlink).
+ Rename(newSymlink, symlink).Done()
+}
+
+// commitAndPushChanges commits changes identified by the given manifest file
+// and label to the containing repository and pushes these changes to the
+// remote repository.
+func commitAndPushChanges(jirix *jiri.X, snapshotDir, snapshotFile, label string) (e error) {
+ cwd, err := os.Getwd()
+ if err != nil {
+ return err
+ }
+ defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e)
+ if err := jirix.NewSeq().Chdir(snapshotDir).Done(); err != nil {
+ return err
+ }
+ relativeSnapshotPath := strings.TrimPrefix(snapshotFile, snapshotDir+string(os.PathSeparator))
+ git := gitutil.New(jirix.NewSeq())
+ // Pull from master so we are up-to-date.
+ if err := git.Pull("origin", "master"); err != nil {
+ return err
+ }
+ if err := git.Add(relativeSnapshotPath); err != nil {
+ return err
+ }
+ if err := git.Add(label); err != nil {
+ return err
+ }
+ name := strings.TrimPrefix(snapshotFile, snapshotDir)
+ if err := git.CommitNoVerify(fmt.Sprintf("adding snapshot %q for label %q", name, label)); err != nil {
+ return err
+ }
+ if err := git.Push("origin", "master", gitutil.VerifyOpt(false)); err != nil {
+ return err
+ }
+ return nil
+}
+
+// cmdSnapshotCheckout represents the "jiri snapshot checkout" command.
+var cmdSnapshotCheckout = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runSnapshotCheckout),
+ Name: "checkout",
+ Short: "Checkout a project snapshot",
+ Long: `
+The "jiri snapshot checkout <snapshot>" command restores local project state to
+the state in the given snapshot manifest.
+`,
+ ArgsName: "<snapshot>",
+ ArgsLong: "<snapshot> is the snapshot manifest file.",
+}
+
+func runSnapshotCheckout(jirix *jiri.X, args []string) error {
+ if len(args) != 1 {
+ return jirix.UsageErrorf("unexpected number of arguments")
+ }
+ return project.CheckoutSnapshot(jirix, args[0], snapshotGcFlag)
+}
+
+// cmdSnapshotList represents the "jiri snapshot list" command.
+var cmdSnapshotList = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runSnapshotList),
+ Name: "list",
+ Short: "List existing project snapshots",
+ Long: `
+The "snapshot list" command lists existing snapshots of the labels
+specified as command-line arguments. If no arguments are provided, the
+command lists snapshots for all known labels.
+`,
+ ArgsName: "<label ...>",
+ ArgsLong: "<label ...> is a list of snapshot labels.",
+}
+
+func runSnapshotList(jirix *jiri.X, args []string) error {
+ snapshotDir, err := getSnapshotDir(jirix)
+ if err != nil {
+ return err
+ }
+ if len(args) == 0 {
+ // Identify all known snapshot labels, using a
+ // heuristic that looks for all symbolic links <foo>
+ // in the snapshot directory that point to a file in
+ // the "labels/<foo>" subdirectory of the snapshot
+ // directory.
+ fileInfoList, err := ioutil.ReadDir(snapshotDir)
+ if err != nil {
+ return fmt.Errorf("ReadDir(%v) failed: %v", snapshotDir, err)
+ }
+ for _, fileInfo := range fileInfoList {
+ if fileInfo.Mode()&os.ModeSymlink != 0 {
+ path := filepath.Join(snapshotDir, fileInfo.Name())
+ dst, err := filepath.EvalSymlinks(path)
+ if err != nil {
+ return fmt.Errorf("EvalSymlinks(%v) failed: %v", path, err)
+ }
+ if strings.HasSuffix(filepath.Dir(dst), filepath.Join("labels", fileInfo.Name())) {
+ args = append(args, fileInfo.Name())
+ }
+ }
+ }
+ }
+
+ // Check that all labels exist.
+ failed := false
+ for _, label := range args {
+ labelDir := filepath.Join(snapshotDir, "labels", label)
+ if _, err := jirix.NewSeq().Stat(labelDir); err != nil {
+ if !runutil.IsNotExist(err) {
+ return err
+ }
+ failed = true
+ fmt.Fprintf(jirix.Stderr(), "snapshot label %q not found", label)
+ }
+ }
+ if failed {
+ return cmdline.ErrExitCode(2)
+ }
+
+ // Print snapshots for all labels.
+ sort.Strings(args)
+ for _, label := range args {
+ // Scan the snapshot directory "labels/<label>" printing
+ // all snapshots.
+ labelDir := filepath.Join(snapshotDir, "labels", label)
+ fileInfoList, err := ioutil.ReadDir(labelDir)
+ if err != nil {
+ return fmt.Errorf("ReadDir(%v) failed: %v", labelDir, err)
+ }
+ fmt.Fprintf(jirix.Stdout(), "snapshots of label %q:\n", label)
+ for _, fileInfo := range fileInfoList {
+ fmt.Fprintf(jirix.Stdout(), " %v\n", fileInfo.Name())
+ }
+ }
+ return nil
+}
diff --git a/cmd/jiri/snapshot_test.go b/cmd/jiri/snapshot_test.go
new file mode 100644
index 0000000..d24b5a6
--- /dev/null
+++ b/cmd/jiri/snapshot_test.go
@@ -0,0 +1,341 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "v.io/jiri/gitutil"
+ "v.io/jiri/jiri"
+ "v.io/jiri/jiritest"
+ "v.io/jiri/project"
+ "v.io/jiri/tool"
+)
+
+func createLabelDir(t *testing.T, jirix *jiri.X, snapshotDir, name string, snapshots []string) {
+ if snapshotDir == "" {
+ snapshotDir = filepath.Join(jirix.Root, defaultSnapshotDir)
+ }
+ s := jirix.NewSeq()
+ labelDir, perm := filepath.Join(snapshotDir, "labels", name), os.FileMode(0700)
+ if err := s.MkdirAll(labelDir, perm).Done(); err != nil {
+ t.Fatalf("MkdirAll(%v, %v) failed: %v", labelDir, perm, err)
+ }
+ for i, snapshot := range snapshots {
+ path := filepath.Join(labelDir, snapshot)
+ _, err := os.Create(path)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if i == 0 {
+ symlinkPath := filepath.Join(snapshotDir, name)
+ if err := s.Symlink(path, symlinkPath).Done(); err != nil {
+ t.Fatalf("Symlink(%v, %v) failed: %v", path, symlinkPath, err)
+ }
+ }
+ }
+}
+
+func generateOutput(labels []label) string {
+ output := ""
+ for _, label := range labels {
+ output += fmt.Sprintf("snapshots of label %q:\n", label.name)
+ for _, snapshot := range label.snapshots {
+ output += fmt.Sprintf(" %v\n", snapshot)
+ }
+ }
+ return output
+}
+
+type config struct {
+ remote bool
+ dir string
+}
+
+type label struct {
+ name string
+ snapshots []string
+}
+
+func TestList(t *testing.T) {
+ resetFlags()
+ fake, cleanup := jiritest.NewFakeJiriRoot(t)
+ defer cleanup()
+
+ snapshotDir1 := "" // Should use default dir.
+ snapshotDir2 := filepath.Join(fake.X.Root, "some/other/dir")
+
+ // Create a test suite.
+ tests := []config{
+ config{
+ dir: snapshotDir1,
+ },
+ config{
+ dir: snapshotDir2,
+ },
+ }
+ labels := []label{
+ label{
+ name: "beta",
+ snapshots: []string{"beta-1", "beta-2", "beta-3"},
+ },
+ label{
+ name: "stable",
+ snapshots: []string{"stable-1", "stable-2", "stable-3"},
+ },
+ }
+
+ for _, test := range tests {
+ snapshotDirFlag = test.dir
+ // Create the snapshots directory and populate it with the
+ // data specified by the test suite.
+ for _, label := range labels {
+ createLabelDir(t, fake.X, test.dir, label.name, label.snapshots)
+ }
+
+ // Check that running "jiri snapshot list" with no arguments
+ // returns the expected output.
+ var stdout bytes.Buffer
+ fake.X.Context = tool.NewContext(tool.ContextOpts{Stdout: &stdout})
+ if err := runSnapshotList(fake.X, nil); err != nil {
+ t.Fatalf("%v", err)
+ }
+ got, want := stdout.String(), generateOutput(labels)
+ if got != want {
+ t.Fatalf("unexpected output:\ngot\n%v\nwant\n%v\n", got, want)
+ }
+
+ // Check that running "jiri snapshot list" with one argument
+ // returns the expected output.
+ stdout.Reset()
+ if err := runSnapshotList(fake.X, []string{"stable"}); err != nil {
+ t.Fatalf("%v", err)
+ }
+ got, want = stdout.String(), generateOutput(labels[1:])
+ if got != want {
+ t.Fatalf("unexpected output:\ngot\n%v\nwant\n%v\n", got, want)
+ }
+
+ // Check that running "jiri snapshot list" with
+ // multiple arguments returns the expected output.
+ stdout.Reset()
+ if err := runSnapshotList(fake.X, []string{"beta", "stable"}); err != nil {
+ t.Fatalf("%v", err)
+ }
+ got, want = stdout.String(), generateOutput(labels)
+ if got != want {
+ t.Fatalf("unexpected output:\ngot\n%v\nwant\n%v\n", got, want)
+ }
+ }
+}
+
+func checkReadme(t *testing.T, jirix *jiri.X, project, message string) {
+ s := jirix.NewSeq()
+ if _, err := s.Stat(project); err != nil {
+ t.Fatalf("%v", err)
+ }
+ readmeFile := filepath.Join(project, "README")
+ data, err := s.ReadFile(readmeFile)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ if got, want := data, []byte(message); bytes.Compare(got, want) != 0 {
+ t.Fatalf("unexpected content %v:\ngot\n%s\nwant\n%s\n", project, got, want)
+ }
+}
+
+func localProjectName(i int) string {
+ return "test-local-project-" + fmt.Sprintf("%d", i+1)
+}
+
+func remoteProjectName(i int) string {
+ return "test-remote-project-" + fmt.Sprintf("%d", i+1)
+}
+
+func writeReadme(t *testing.T, jirix *jiri.X, projectDir, message string) {
+ s := jirix.NewSeq()
+ path, perm := filepath.Join(projectDir, "README"), os.FileMode(0644)
+ if err := s.WriteFile(path, []byte(message), perm).Done(); err != nil {
+ t.Fatalf("%v", err)
+ }
+ cwd, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+ defer jirix.NewSeq().Chdir(cwd)
+ if err := s.Chdir(projectDir).Done(); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := gitutil.New(jirix.NewSeq()).CommitFile(path, "creating README"); err != nil {
+ t.Fatalf("%v", err)
+ }
+}
+
+func resetFlags() {
+ snapshotDirFlag = ""
+ pushRemoteFlag = false
+}
+
+func TestGetSnapshotDir(t *testing.T) {
+ resetFlags()
+ defer resetFlags()
+ fake, cleanup := jiritest.NewFakeJiriRoot(t)
+ defer cleanup()
+
+ // With all flags at default values, snapshot dir should be default.
+ resetFlags()
+ got, err := getSnapshotDir(fake.X)
+ if err != nil {
+ t.Fatalf("getSnapshotDir() failed: %v\n", err)
+ }
+ if want := filepath.Join(fake.X.Root, defaultSnapshotDir); got != want {
+ t.Errorf("unexpected snapshot dir: got %v want %v", got, want)
+ }
+
+ // With dir flag set to absolute path, snapshot dir should be value of dir
+ // flag.
+ resetFlags()
+ tempDir, err := fake.X.NewSeq().TempDir("", "")
+ if err != nil {
+ t.Fatalf("TempDir() failed: %v", err)
+ }
+ defer fake.X.NewSeq().RemoveAll(tempDir).Done()
+ snapshotDirFlag = tempDir
+ got, err = getSnapshotDir(fake.X)
+ if err != nil {
+ t.Fatalf("getSnapshotDir() failed: %v\n", err)
+ }
+ if want := snapshotDirFlag; got != want {
+ t.Errorf("unexpected snapshot dir: got %v want %v", got, want)
+ }
+
+ // With dir flag set to relative path, snapshot dir should absolute path
+ // rooted at current working dir.
+ resetFlags()
+ snapshotDirFlag = "some/relative/path"
+ cwd, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("os.Getwd() failed: %v", err)
+ }
+ got, err = getSnapshotDir(fake.X)
+ if err != nil {
+ t.Fatalf("getSnapshotDir() failed: %v\n", err)
+ }
+ if want := filepath.Join(cwd, snapshotDirFlag); got != want {
+ t.Errorf("unexpected snapshot dir: got %v want %v", got, want)
+ }
+}
+
+// TestCreate tests creating and checking out a snapshot.
+func TestCreate(t *testing.T) {
+ resetFlags()
+ defer resetFlags()
+ fake, cleanup := jiritest.NewFakeJiriRoot(t)
+ defer cleanup()
+ s := fake.X.NewSeq()
+
+ // Setup the initial remote and local projects.
+ numProjects, remoteProjects := 2, []string{}
+ for i := 0; i < numProjects; i++ {
+ if err := fake.CreateRemoteProject(remoteProjectName(i)); err != nil {
+ t.Fatalf("%v", err)
+ }
+ if err := fake.AddProject(project.Project{
+ Name: remoteProjectName(i),
+ Path: localProjectName(i),
+ Remote: fake.Projects[remoteProjectName(i)],
+ }); err != nil {
+ t.Fatalf("%v", err)
+ }
+ }
+
+ // Create initial commits in the remote projects and use UpdateUniverse()
+ // to mirror them locally.
+ for i := 0; i < numProjects; i++ {
+ writeReadme(t, fake.X, fake.Projects[remoteProjectName(i)], "revision 1")
+ }
+ if err := project.UpdateUniverse(fake.X, true); err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ // Create a snapshot.
+ var stdout bytes.Buffer
+ fake.X.Context = tool.NewContext(tool.ContextOpts{Stdout: &stdout})
+ if err := runSnapshotCreate(fake.X, []string{"test-local"}); err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ // Remove the local project repositories.
+ for i, _ := range remoteProjects {
+ localProject := filepath.Join(fake.X.Root, localProjectName(i))
+ if err := s.RemoveAll(localProject).Done(); err != nil {
+ t.Fatalf("%v", err)
+ }
+ }
+
+ // Check that invoking the UpdateUniverse() with the snapshot restores the
+ // local repositories.
+ snapshotDir := filepath.Join(fake.X.Root, defaultSnapshotDir)
+ snapshotFile := filepath.Join(snapshotDir, "test-local")
+ localX := fake.X.Clone(tool.ContextOpts{
+ Manifest: &snapshotFile,
+ })
+ if err := project.UpdateUniverse(localX, true); err != nil {
+ t.Fatalf("%v", err)
+ }
+ for i, _ := range remoteProjects {
+ localProject := filepath.Join(fake.X.Root, localProjectName(i))
+ checkReadme(t, fake.X, localProject, "revision 1")
+ }
+}
+
+// TestCreatePushRemote checks that creating a snapshot with the -push-remote
+// flag causes the snapshot to be committed and pushed upstream.
+func TestCreatePushRemote(t *testing.T) {
+ resetFlags()
+ defer resetFlags()
+
+ fake, cleanup := jiritest.NewFakeJiriRoot(t)
+ defer cleanup()
+
+ fake.EnableRemoteManifestPush()
+ defer fake.DisableRemoteManifestPush()
+
+ manifestDir := filepath.Join(fake.X.Root, ".manifest")
+ snapshotDir := filepath.Join(manifestDir, "snapshot")
+ label := "test"
+
+ git := gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(manifestDir))
+ commitCount, err := git.CountCommits("master", "")
+ if err != nil {
+ t.Fatalf("git.CountCommits(\"master\", \"\") failed: %v", err)
+ }
+
+ // Create snapshot with -push-remote flag set to true.
+ snapshotDirFlag = snapshotDir
+ pushRemoteFlag = true
+ if err := runSnapshotCreate(fake.X, []string{label}); err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ // Check that repo has one new commit.
+ newCommitCount, err := git.CountCommits("master", "")
+ if err != nil {
+ t.Fatalf("git.CountCommits(\"master\", \"\") failed: %v", err)
+ }
+ if got, want := newCommitCount, commitCount+1; got != want {
+ t.Errorf("unexpected commit count: got %v want %v", got, want)
+ }
+
+ // Check that new label is commited.
+ labelFile := filepath.Join(snapshotDir, "labels", label)
+ if !git.IsFileCommitted(labelFile) {
+ t.Errorf("expected file %v to be committed but it was not", labelFile)
+ }
+}
diff --git a/cmd/jiri/update.go b/cmd/jiri/update.go
new file mode 100644
index 0000000..f87107d
--- /dev/null
+++ b/cmd/jiri/update.go
@@ -0,0 +1,67 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "v.io/jiri/jiri"
+ "v.io/jiri/project"
+ "v.io/jiri/retry"
+ "v.io/jiri/tool"
+ "v.io/x/lib/cmdline"
+)
+
+var (
+ gcFlag bool
+ attemptsFlag int
+)
+
+func init() {
+ tool.InitializeProjectFlags(&cmdUpdate.Flags)
+
+ cmdUpdate.Flags.BoolVar(&gcFlag, "gc", false, "Garbage collect obsolete repositories.")
+ cmdUpdate.Flags.IntVar(&attemptsFlag, "attempts", 1, "Number of attempts before failing.")
+}
+
+// cmdUpdate represents the "jiri update" command.
+var cmdUpdate = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runUpdate),
+ Name: "update",
+ Short: "Update all jiri tools and projects",
+ Long: `
+Updates all projects, builds the latest version of all tools, and installs the
+resulting binaries into $JIRI_ROOT/.jiri_root/bin. The sequence in which the
+individual updates happen guarantees that we end up with a consistent set of
+tools and source code. The set of projects and tools to update is described in
+the manifest.
+
+Run "jiri help manifest" for details on manifests.
+`,
+}
+
+func runUpdate(jirix *jiri.X, _ []string) error {
+ seq := jirix.NewSeq()
+ // Create the $JIRI_ROOT/.jiri_root directory if it doesn't already exist.
+ //
+ // TODO(toddw): Remove this logic after the transition to .jiri_root is done.
+ // The bootstrapping logic should create this directory, and jiri should fail
+ // if the directory doesn't exist.
+ if err := seq.MkdirAll(jirix.RootMetaDir(), 0755).Done(); err != nil {
+ return err
+ }
+
+ // Update all projects to their latest version.
+ // Attempt <attemptsFlag> times before failing.
+ updateFn := func() error { return project.UpdateUniverse(jirix, gcFlag) }
+ if err := retry.Function(jirix.Context, updateFn, retry.AttemptsOpt(attemptsFlag)); err != nil {
+ return err
+ }
+ if err := project.WriteUpdateHistorySnapshot(jirix); err != nil {
+ return err
+ }
+
+ // Only attempt the bin dir transition after the update has succeeded, to
+ // avoid messy partial states.
+ return project.TransitionBinDir(jirix)
+}
diff --git a/cmd/jiri/upgrade.go b/cmd/jiri/upgrade.go
new file mode 100644
index 0000000..da6947f
--- /dev/null
+++ b/cmd/jiri/upgrade.go
@@ -0,0 +1,150 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "v.io/jiri/jiri"
+ "v.io/jiri/project"
+ "v.io/jiri/runutil"
+ "v.io/x/lib/cmdline"
+)
+
+// TODO(toddw): Remove the upgrade command after the transition to new-style
+// manifests is complete.
+
+var flagUpgradeRevert bool
+
+func init() {
+ cmdUpgrade.Flags.BoolVar(&flagUpgradeRevert, "revert", false, `Revert the upgrade by deleting the $JIRI_ROOT/.jiri_manifest file.`)
+}
+
+var cmdUpgrade = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runUpgrade),
+ Name: "upgrade",
+ Short: "Upgrade jiri to new-style manifests",
+ Long: `
+Upgrades jiri to use new-style manifests.
+
+The old (deprecated) behavior only allowed a single manifest repository, located
+in $JIRI_ROOT/.manifest. The initial manifest file is located as follows:
+ 1) Use -manifest flag, if non-empty. If it's empty...
+ 2) Use $JIRI_ROOT/.local_manifest file. If it doesn't exist...
+ 3) Use $JIRI_ROOT/.manifest/v2/default.
+
+The new behavior allows multiple manifest repositories, by allowing imports to
+specify project attributes describing the remote repository. The -manifest flag
+is no longer allowed to be set; the initial manifest file is always located in
+$JIRI_ROOT/.jiri_manifest. The .local_manifest file is ignored.
+
+During the transition phase, both old and new behaviors are supported. The jiri
+tool uses the existence of the $JIRI_ROOT/.jiri_manifest file as the signal; if
+it exists we run the new behavior, otherwise we run the old behavior.
+
+The new behavior includes a "jiri import" command, which writes or updates the
+.jiri_manifest file. The new bootstrap procedure runs "jiri import", and it is
+intended as a regular command to add imports to your jiri environment.
+
+This upgrade command eases the transition by writing an initial .jiri_manifest
+file for you. If you have an existing .local_manifest file, its contents will
+be incorporated into the new .jiri_manifest file, and it will be renamed to
+.local_manifest.BACKUP. The -revert flag deletes the .jiri_manifest file, and
+restores the .local_manifest file.
+`,
+ ArgsName: "<kind>",
+ ArgsLong: `
+<kind> specifies the kind of upgrade, one of "v23" or "fuchsia".
+`,
+}
+
+func runUpgrade(jirix *jiri.X, args []string) error {
+ localFile := filepath.Join(jirix.Root, ".local_manifest")
+ backupFile := localFile + ".BACKUP"
+ if flagUpgradeRevert {
+ // Restore .local_manifest.BACKUP if it exists.
+ switch _, err := jirix.NewSeq().Stat(backupFile); {
+ case err != nil && !runutil.IsNotExist(err):
+ return err
+ case err == nil:
+ if err := jirix.NewSeq().Rename(backupFile, localFile).Done(); err != nil {
+ return fmt.Errorf("couldn't restore %v to %v: %v", backupFile, localFile, err)
+ }
+ }
+ // Deleting the .jiri_manifest file reverts to the old behavior.
+ return jirix.NewSeq().Remove(jirix.JiriManifestFile()).Done()
+ }
+ if len(args) != 1 {
+ return jirix.UsageErrorf("must specify upgrade kind")
+ }
+ kind := args[0]
+ var argRemote, argName, argManifest string
+ switch kind {
+ case "v23":
+ argRemote = "https://vanadium.googlesource.com/manifest"
+ argName, argManifest = "manifest", "public"
+ case "fuchsia":
+ argRemote = "https://fuchsia.googlesource.com/fnl-start"
+ argName, argManifest = "fnl-start", "manifest/fuchsia"
+ default:
+ return jirix.UsageErrorf("unknown upgrade kind %q", kind)
+ }
+ // Initialize manifest from .local_manifest.
+ hasLocalFile := true
+ manifest, err := project.ManifestFromFile(jirix, localFile)
+ if err != nil {
+ if !runutil.IsNotExist(err) {
+ return err
+ }
+ hasLocalFile = false
+ manifest = &project.Manifest{}
+ }
+ oldImports := manifest.Imports
+ manifest.Imports = nil
+ for _, oldImport := range oldImports {
+ if oldImport.Remote != "" {
+ // This is a new-style remote import, carry it over directly.
+ manifest.Imports = append(manifest.Imports, oldImport)
+ continue
+ }
+ // This is an old-style import, convert it to the new style.
+ oldName := oldImport.Name
+ switch {
+ case kind == "v23" && oldName == "default":
+ oldName = "public"
+ case kind == "fuchsia" && oldName == "default":
+ oldName = "manifest/fuchsia"
+ }
+ manifest.Imports = append(manifest.Imports, project.Import{
+ Manifest: oldName,
+ Name: argName,
+ Remote: argRemote,
+ })
+ }
+ if len(manifest.Imports) == 0 {
+ manifest.Imports = append(manifest.Imports, project.Import{
+ Manifest: argManifest,
+ Name: argName,
+ Remote: argRemote,
+ })
+ }
+ // Write output to .jiri_manifest file.
+ outFile := jirix.JiriManifestFile()
+ if _, err := os.Stat(outFile); err == nil {
+ return fmt.Errorf("%v already exists", outFile)
+ }
+ if err := manifest.ToFile(jirix, outFile); err != nil {
+ return err
+ }
+ // Backup .local_manifest file, if it exists.
+ if hasLocalFile {
+ if err := jirix.NewSeq().Rename(localFile, backupFile).Done(); err != nil {
+ return fmt.Errorf("couldn't backup %v to %v: %v", localFile, backupFile, err)
+ }
+ }
+ return nil
+}
diff --git a/cmd/jiri/upgrade_test.go b/cmd/jiri/upgrade_test.go
new file mode 100644
index 0000000..a7c5eab
--- /dev/null
+++ b/cmd/jiri/upgrade_test.go
@@ -0,0 +1,311 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "strings"
+ "testing"
+
+ "v.io/jiri/jiri"
+ "v.io/x/lib/gosh"
+)
+
+type upgradeTestCase struct {
+ Args []string
+ Exist bool
+ Local, Want string
+ Stderr string
+}
+
+func TestUpgrade(t *testing.T) {
+ tests := []upgradeTestCase{
+ {
+ Stderr: `must specify upgrade kind`,
+ },
+ {
+ Args: []string{"foo"},
+ Stderr: `unknown upgrade kind "foo"`,
+ },
+ // Test v23 upgrades.
+ {
+ Args: []string{"v23"},
+ Exist: true,
+ Stderr: `.jiri_manifest already exists`,
+ },
+ {
+ Args: []string{"v23"},
+ Want: `<manifest>
+ <imports>
+ <import manifest="public" name="manifest" remote="https://vanadium.googlesource.com/manifest"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"v23"},
+ Local: `<manifest>
+ <imports>
+ <import name="default"/>
+ </imports>
+</manifest>
+`,
+ Want: `<manifest>
+ <imports>
+ <import manifest="public" name="manifest" remote="https://vanadium.googlesource.com/manifest"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"v23"},
+ Local: `<manifest>
+ <imports>
+ <import name="private"/>
+ </imports>
+</manifest>
+`,
+ Want: `<manifest>
+ <imports>
+ <import manifest="private" name="manifest" remote="https://vanadium.googlesource.com/manifest"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"v23"},
+ Local: `<manifest>
+ <imports>
+ <import name="private"/>
+ <import name="infrastructure"/>
+ <import name="default"/>
+ </imports>
+</manifest>
+`,
+ Want: `<manifest>
+ <imports>
+ <import manifest="private" name="manifest" remote="https://vanadium.googlesource.com/manifest"/>
+ <import manifest="infrastructure" name="manifest" remote="https://vanadium.googlesource.com/manifest"/>
+ <import manifest="public" name="manifest" remote="https://vanadium.googlesource.com/manifest"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"v23"},
+ Local: `<manifest>
+ <imports>
+ <import name="default"/>
+ <import name="infrastructure"/>
+ <import name="private"/>
+ </imports>
+</manifest>
+`,
+ Want: `<manifest>
+ <imports>
+ <import manifest="public" name="manifest" remote="https://vanadium.googlesource.com/manifest"/>
+ <import manifest="infrastructure" name="manifest" remote="https://vanadium.googlesource.com/manifest"/>
+ <import manifest="private" name="manifest" remote="https://vanadium.googlesource.com/manifest"/>
+ </imports>
+</manifest>
+`,
+ },
+ // Test fuchsia upgrades.
+ {
+ Args: []string{"fuchsia"},
+ Exist: true,
+ Stderr: `.jiri_manifest already exists`,
+ },
+ {
+ Args: []string{"fuchsia"},
+ Want: `<manifest>
+ <imports>
+ <import manifest="manifest/fuchsia" name="fnl-start" remote="https://fuchsia.googlesource.com/fnl-start"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"fuchsia"},
+ Local: `<manifest>
+ <imports>
+ <import name="default"/>
+ </imports>
+</manifest>
+`,
+ Want: `<manifest>
+ <imports>
+ <import manifest="manifest/fuchsia" name="fnl-start" remote="https://fuchsia.googlesource.com/fnl-start"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"fuchsia"},
+ Local: `<manifest>
+ <imports>
+ <import name="private"/>
+ </imports>
+</manifest>
+`,
+ Want: `<manifest>
+ <imports>
+ <import manifest="private" name="fnl-start" remote="https://fuchsia.googlesource.com/fnl-start"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"fuchsia"},
+ Local: `<manifest>
+ <imports>
+ <import name="private"/>
+ <import name="infrastructure"/>
+ <import name="default"/>
+ </imports>
+</manifest>
+`,
+ Want: `<manifest>
+ <imports>
+ <import manifest="private" name="fnl-start" remote="https://fuchsia.googlesource.com/fnl-start"/>
+ <import manifest="infrastructure" name="fnl-start" remote="https://fuchsia.googlesource.com/fnl-start"/>
+ <import manifest="manifest/fuchsia" name="fnl-start" remote="https://fuchsia.googlesource.com/fnl-start"/>
+ </imports>
+</manifest>
+`,
+ },
+ {
+ Args: []string{"fuchsia"},
+ Local: `<manifest>
+ <imports>
+ <import name="default"/>
+ <import name="infrastructure"/>
+ <import name="private"/>
+ </imports>
+</manifest>
+`,
+ Want: `<manifest>
+ <imports>
+ <import manifest="manifest/fuchsia" name="fnl-start" remote="https://fuchsia.googlesource.com/fnl-start"/>
+ <import manifest="infrastructure" name="fnl-start" remote="https://fuchsia.googlesource.com/fnl-start"/>
+ <import manifest="private" name="fnl-start" remote="https://fuchsia.googlesource.com/fnl-start"/>
+ </imports>
+</manifest>
+`,
+ },
+ }
+ opts := gosh.Opts{Fatalf: t.Fatalf, Logf: t.Logf}
+ sh := gosh.NewShell(opts)
+ defer sh.Cleanup()
+ jiriTool := sh.BuildGoPkg("v.io/jiri")
+ for _, test := range tests {
+ if err := testUpgrade(opts, jiriTool, test); err != nil {
+ t.Errorf("%v: %v", test.Args, err)
+ }
+ }
+}
+
+func testUpgrade(opts gosh.Opts, jiriTool string, test upgradeTestCase) error {
+ opts.PropagateChildOutput = true
+ sh := gosh.NewShell(opts)
+ defer sh.Cleanup()
+ jiriRoot := sh.MakeTempDir()
+ sh.Pushd(jiriRoot)
+ // Set up an existing file or local_manifest, if they were specified
+ if test.Exist {
+ if err := ioutil.WriteFile(".jiri_manifest", []byte("<manifest/>"), 0644); err != nil {
+ return err
+ }
+ }
+ if test.Local != "" {
+ if err := ioutil.WriteFile(".local_manifest", []byte(test.Local), 0644); err != nil {
+ return err
+ }
+ }
+ // Run upgrade and check the error.
+ sh.Vars[jiri.RootEnv] = jiriRoot
+ cmd := sh.Cmd(jiriTool, append([]string{"upgrade"}, test.Args...)...)
+ if test.Stderr != "" {
+ cmd.ExitErrorIsOk = true
+ }
+ _, stderr := cmd.StdoutStderr()
+ if got, want := stderr, test.Stderr; !strings.Contains(got, want) || (got != "" && want == "") {
+ return fmt.Errorf("stderr got %q, want substr %q", got, want)
+ }
+ // Make sure the right file is generated.
+ if test.Want != "" {
+ data, err := ioutil.ReadFile(".jiri_manifest")
+ if err != nil {
+ return err
+ }
+ if got, want := string(data), test.Want; got != want {
+ return fmt.Errorf("GOT\n%s\nWANT\n%s", got, want)
+ }
+ }
+ // Make sure the .local_manifest file is backed up.
+ if test.Local != "" && test.Stderr == "" {
+ data, err := ioutil.ReadFile(".local_manifest.BACKUP")
+ if err != nil {
+ return fmt.Errorf("local manifest backup got error: %v", err)
+ }
+ if got, want := string(data), test.Local; got != want {
+ return fmt.Errorf("local manifest backup GOT\n%s\nWANT\n%s", got, want)
+ }
+ }
+ return nil
+}
+
+func TestUpgradeRevert(t *testing.T) {
+ sh := gosh.NewShell(gosh.Opts{Fatalf: t.Fatalf, Logf: t.Logf})
+ defer sh.Cleanup()
+ jiriRoot := sh.MakeTempDir()
+ sh.Pushd(jiriRoot)
+ jiriTool := sh.BuildGoPkg("v.io/jiri")
+ localData := `<manifest/>`
+ jiriData := `<manifest>
+ <imports>
+ <import manifest="public" name="manifest" remote="https://vanadium.googlesource.com/manifest"/>
+ </imports>
+</manifest>
+`
+ // Set up an existing local_manifest.
+ if err := ioutil.WriteFile(".local_manifest", []byte(localData), 0644); err != nil {
+ t.Errorf("couldn't write local manifest: %v", err)
+ }
+ // Run a regular upgrade first, and make sure files are as expected.
+ sh.Vars[jiri.RootEnv] = jiriRoot
+ sh.Cmd(jiriTool, "upgrade", "v23").Run()
+ gotJiri, err := ioutil.ReadFile(".jiri_manifest")
+ if err != nil {
+ t.Errorf("couldn't read jiri manifest: %v", err)
+ }
+ if got, want := string(gotJiri), jiriData; got != want {
+ t.Errorf("jiri manifest GOT\n%s\nWANT\n%s", got, want)
+ }
+ gotBackup, err := ioutil.ReadFile(".local_manifest.BACKUP")
+ if err != nil {
+ t.Errorf("couldn't read local manifest backup: %v", err)
+ }
+ if got, want := string(gotBackup), localData; got != want {
+ t.Errorf("local manifest backup GOT\n%s\nWANT\n%s", got, want)
+ }
+ // Now run a revert, and make sure files are as expected.
+ sh.Cmd(jiriTool, "upgrade", "-revert").Run()
+ if _, err := os.Stat(".jiri_manifest"); !os.IsNotExist(err) {
+ t.Errorf(".jiri_manifest still exists after revert: %v", err)
+ }
+ if _, err := os.Stat(".local_manifest.BACKUP"); !os.IsNotExist(err) {
+ t.Errorf(".local_manifest.BACKUP still exists after revert: %v", err)
+ }
+ gotLocal, err := ioutil.ReadFile(".local_manifest")
+ if err != nil {
+ t.Errorf("couldn't read local manifest: %v", err)
+ }
+ if got, want := string(gotLocal), localData; got != want {
+ t.Errorf("local manifest GOT\n%s\nWANT\n%s", got, want)
+ }
+}
diff --git a/cmd/jiri/which.go b/cmd/jiri/which.go
new file mode 100644
index 0000000..9dee05c
--- /dev/null
+++ b/cmd/jiri/which.go
@@ -0,0 +1,54 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+
+ "v.io/x/lib/cmdline"
+)
+
+var cmdWhich = &cmdline.Command{
+ Runner: cmdline.RunnerFunc(runWhich),
+ Name: "which",
+ Short: "Show path to the jiri tool",
+ Long: `
+Which behaves similarly to the unix commandline tool. It is useful in
+determining whether the jiri binary is being run directly, or run via the jiri
+shim script.
+
+If the binary is being run directly, the output looks like this:
+
+ # binary
+ /path/to/binary/jiri
+
+If the script is being run, the output looks like this:
+
+ # script
+ /path/to/script/jiri
+`,
+}
+
+func runWhich(env *cmdline.Env, args []string) error {
+ if len(args) == 0 {
+ fmt.Fprintln(env.Stdout, "# binary")
+ path, err := exec.LookPath(os.Args[0])
+ if err != nil {
+ return err
+ }
+ abs, err := filepath.Abs(path)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintln(env.Stdout, abs)
+ return nil
+ }
+ // TODO(toddw): Look up the path to each argument. This will only be helpful
+ // after the profiles are moved back into the main jiri tool.
+ return fmt.Errorf("unexpected arguments")
+}
diff --git a/cmd/jiri/which_test.go b/cmd/jiri/which_test.go
new file mode 100644
index 0000000..dbe46f1
--- /dev/null
+++ b/cmd/jiri/which_test.go
@@ -0,0 +1,45 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "path/filepath"
+ "testing"
+
+ "v.io/x/lib/gosh"
+)
+
+func TestWhich(t *testing.T) {
+ sh := gosh.NewShell(gosh.Opts{Fatalf: t.Fatalf, Logf: t.Logf, PropagateChildOutput: true})
+ defer sh.Cleanup()
+
+ jiriBinary := sh.BuildGoPkg("v.io/jiri")
+ stdout, stderr := sh.Cmd(jiriBinary, []string{"which"}...).StdoutStderr()
+ if got, want := stdout, fmt.Sprintf("# binary\n%s\n", jiriBinary); got != want {
+ t.Errorf("stdout got %q, want %q", got, want)
+ }
+ if got, want := stderr, ""; got != want {
+ t.Errorf("stderr got %q, want %q", got, want)
+ }
+}
+
+// TestWhichScript tests the behavior of "jiri which" for the shim script.
+func TestWhichScript(t *testing.T) {
+ sh := gosh.NewShell(gosh.Opts{Fatalf: t.Fatalf, Logf: t.Logf, PropagateChildOutput: true})
+ defer sh.Cleanup()
+
+ jiriScript, err := filepath.Abs("./scripts/jiri")
+ if err != nil {
+ t.Fatalf("couldn't determine absolute path to jiri script")
+ }
+ stdout, stderr := sh.Cmd(jiriScript, "which").StdoutStderr()
+ if got, want := stdout, fmt.Sprintf("# script\n%s\n", jiriScript); got != want {
+ t.Errorf("stdout got %q, want %q", got, want)
+ }
+ if got, want := stderr, ""; got != want {
+ t.Errorf("stderr got %q, want %q", got, want)
+ }
+}