v.io/jiri: add project runp and project info commands.
jiri runp will run commands in parallel in each of a specified
set of jiri projects. jiri project info provides structured
access to project information.
MultiPart: 1/2
Change-Id: I173b8f1d9f41b36b6f67fde3df0772501d8d3273
diff --git a/cmd/jiri/cl.go b/cmd/jiri/cl.go
index 1950a78..2ddc965 100644
--- a/cmd/jiri/cl.go
+++ b/cmd/jiri/cl.go
@@ -335,7 +335,7 @@
// 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) {
+func currentProject(jirix *jiri.X, cmd string) (project.Project, error) {
dir, err := os.Getwd()
if err != nil {
return project.Project{}, fmt.Errorf("os.Getwd() failed: %v", err)
@@ -343,7 +343,7 @@
// 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")
+ return project.Project{}, fmt.Errorf("'%s' must be run from within a project in JIRI_ROOT", cmd)
}
// Walk up the path until we find a project at that path, or hit the jirix.Root.
@@ -385,7 +385,7 @@
strings.Join(gerrit.PresubmitTestTypes(), ","))
}
- p, err := currentProject(jirix)
+ p, err := currentProject(jirix, "jiri cl mail")
if err != nil {
return err
}
diff --git a/cmd/jiri/doc.go b/cmd/jiri/doc.go
index 61a3e10..5215073 100644
--- a/cmd/jiri/doc.go
+++ b/cmd/jiri/doc.go
@@ -21,6 +21,7 @@
snapshot Manage project snapshots
update Update all jiri tools and projects
which Show path to the jiri tool
+ runp Run a command in parallel across jiri projects
help Display help for commands or topics
The jiri additional help topics are:
@@ -469,6 +470,8 @@
The jiri project commands are:
clean Restore jiri projects to their pristine state
+ info Provided structured input for existing jiri projects and
+ branches
list List existing jiri projects and branches
shell-prompt Print a succinct status of projects suitable for shell prompts
poll Poll existing jiri projects
@@ -498,6 +501,35 @@
-v=false
Print verbose output.
+Jiri project info - Provided structured input for existing jiri projects and branches
+
+Inspect the local filesystem and provide structured info on the existing
+projects and branches. Projects are specified using regular expressions that are
+matched against project keys. If no command line arguments are provided the
+project that the contains the current directory is used. The information to be
+displayed is specified using a go template, supplied via the -f flag, that is
+executed against the v.io/jiri/project.ProjectState structure. This structure
+currently has the following fields:
+project.ProjectState{Branches:[]project.BranchState(nil), CurrentBranch:"",
+HasUncommitted:false, HasUntracked:false, Project:project.Project{Name:"",
+Path:"", Protocol:"", Remote:"", RemoteBranch:"", Revision:"", GerritHost:"",
+GitHooks:"", RunHook:"", XMLName:struct {}{}}}
+
+Usage:
+ jiri project info [flags] <project-keys>...
+
+<project-keys>... a list of project keys, as regexps, to apply the specified
+format to
+
+The jiri project info flags are:
+ -f={{.Project.Name}}
+ The go template for the fields to display.
+
+ -color=true
+ Use color to format output.
+ -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.
@@ -737,6 +769,73 @@
-v=false
Print verbose output.
+Jiri runp - Run a command in parallel across jiri projects
+
+Run a command in parallel across one or more jiri projects using the specified
+profile target's environment. Commands are run using the shell specified by the
+users $SHELL environment variable, or "sh" if that's not set. Thus commands are
+run as $SHELL -c "args..."
+
+Usage:
+ jiri runp [flags] <command line>
+
+A command line to be run in each project specified by the supplied command line
+flags. Any environment variables intended to be evaluated when the command line
+is run must be quoted to avoid expansion before being passed to runp by the
+shell.
+
+The jiri runp flags are:
+ -collate-stdout=true
+ Collate all stdout output from each parallel invocation and display it as if
+ had been generated sequentially. This flag cannot be used with
+ -show-name-prefix, -show-key-prefix or -interactive.
+ -env=
+ specify an environment variable in the form: <var>=[<val>],...
+ -exit-on-error=false
+ If set, all commands will killed as soon as one reports an error, otherwise,
+ each will run to completion.
+ -has-gerrit-message=false
+ If specified, match branches that have, or have no, gerrit message
+ -has-uncommitted=false
+ If specified, match projects that have, or have no, uncommitted changes
+ -has-untracked=false
+ if specified, match projects that have, or have no, untracked files
+ -interactive=true
+ If set, the command to be run is interactive and should not have its
+ stdout/stderr manipulated. This flag cannot be used with -show-name-prefix,
+ -show-key-prefix or -collate-stdout.
+ -merge-policies=+CCFLAGS,+CGO_CFLAGS,+CGO_CXXFLAGS,+CGO_LDFLAGS,+CXXFLAGS,GOARCH,GOOS,GOPATH:,^GOROOT*,+LDFLAGS,:PATH,VDLPATH:
+ specify policies for merging environment variables
+ -profiles=v23:base,jiri
+ a comma separated list of profiles to use
+ -profiles-db=$JIRI_ROOT/.jiri_root/profile_db
+ the path, relative to JIRI_ROOT, that contains the profiles database.
+ -projects=
+ A Regular expression specifying project keys to run commands in. By default,
+ runp will use projects that have the same branch checked as the current
+ project.
+ -show-key-prefix=false
+ If set, each line of output from each project will begin with the key of the
+ project followed by a colon. This is intended for use with long running
+ commands where the output needs to be streamed. Stdout and stderr are spliced
+ apart. This flag cannot be used with -interactive, -show-name-prefix or
+ -collate-stdout
+ -show-name-prefix=false
+ If set, each line of output from each project will begin with the name of the
+ project followed by a colon. This is intended for use with long running
+ commands where the output needs to be streamed. Stdout and stderr are spliced
+ apart. This flag cannot be used with -interactive, -show-key-prefix or
+ -collate-stdout.
+ -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 verbose logging information
+
+ -color=true
+ Use color to format output.
+
Jiri help - Display help for commands or topics
Help with no args displays the usage of the parent command.
diff --git a/cmd/jiri/project.go b/cmd/jiri/project.go
index dc6e7f6..86ced20 100644
--- a/cmd/jiri/project.go
+++ b/cmd/jiri/project.go
@@ -5,11 +5,14 @@
package main
import (
+ "bytes"
"encoding/json"
"fmt"
"path/filepath"
+ "regexp"
"sort"
"strings"
+ "text/template"
"v.io/jiri"
"v.io/jiri/project"
@@ -25,6 +28,7 @@
noPristineFlag bool
checkDirtyFlag bool
showNameFlag bool
+ formatFlag string
)
func init() {
@@ -33,7 +37,7 @@
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.")
-
+ cmdProjectInfo.Flags.StringVar(&formatFlag, "f", "{{.Project.Name}}", "The go template for the fields to display.")
tool.InitializeProjectFlags(&cmdProjectPoll.Flags)
}
@@ -43,7 +47,7 @@
Name: "project",
Short: "Manage the jiri projects",
Long: "Manage the jiri projects.",
- Children: []*cmdline.Command{cmdProjectClean, cmdProjectList, cmdProjectShellPrompt, cmdProjectPoll},
+ Children: []*cmdline.Command{cmdProjectClean, cmdProjectInfo, cmdProjectList, cmdProjectShellPrompt, cmdProjectPoll},
}
// cmdProjectClean represents the "jiri project clean" command.
@@ -126,6 +130,93 @@
return nil
}
+// cmdProjectInfo represents the "jiri project info" command.
+var cmdProjectInfo = &cmdline.Command{
+ Runner: jiri.RunnerFunc(runProjectInfo),
+ Name: "info",
+ Short: "Provided structured input for existing jiri projects and branches",
+ Long: `
+Inspect the local filesystem and provide structured info on the existing projects
+and branches. Projects are specified using regular expressions that are matched
+against project keys. If no command line arguments are provided the project
+that the contains the current directory is used. The information to be
+displayed is specified using a go template, supplied via the -f flag, that is
+executed against the v.io/jiri/project.ProjectState structure. This structure
+currently has the following fields: ` + fmt.Sprintf("%#v", project.ProjectState{}),
+ ArgsName: "<project-keys>...",
+ ArgsLong: "<project-keys>... a list of project keys, as regexps, to apply the specified format to",
+}
+
+// runProjectInfo provides structured info on local projects.
+func runProjectInfo(jirix *jiri.X, args []string) error {
+ tmpl, err := template.New("info").Parse(formatFlag)
+ if err != nil {
+ return fmt.Errorf("failed to parse template %q: %v", formatFlag, err)
+ }
+ regexps := []*regexp.Regexp{}
+
+ if len(args) > 0 {
+ regexps = make([]*regexp.Regexp, len(args), len(args))
+ for i, a := range args {
+ re, err := regexp.Compile(a)
+ if err != nil {
+ return fmt.Errorf("failed to compile regexp %v: %v", a, err)
+ }
+ regexps[i] = re
+ }
+ }
+
+ dirty := false
+ for _, slow := range []string{"HasUncommitted", "HasUntracked"} {
+ if strings.Contains(formatFlag, slow) {
+ dirty = true
+ break
+ }
+ }
+
+ var states map[project.ProjectKey]*project.ProjectState
+ var keys project.ProjectKeys
+ if len(args) == 0 {
+ currentProjectKey, err := project.CurrentProjectKey(jirix)
+ if err != nil {
+ return err
+ }
+ state, err := project.GetProjectState(jirix, currentProjectKey, true)
+ if err != nil {
+ return err
+ }
+ states = map[project.ProjectKey]*project.ProjectState{
+ currentProjectKey: state,
+ }
+ keys = append(keys, currentProjectKey)
+ } else {
+ var err error
+ states, err = project.GetProjectStates(jirix, dirty)
+ if err != nil {
+ return err
+ }
+ for key := range states {
+ for _, re := range regexps {
+ if re.MatchString(string(key)) {
+ keys = append(keys, key)
+ break
+ }
+ }
+ }
+ }
+ sort.Sort(keys)
+
+ for _, key := range keys {
+ state := states[key]
+ out := &bytes.Buffer{}
+ if err = tmpl.Execute(out, state); err != nil {
+ return jirix.UsageErrorf("invalid format")
+ }
+ fmt.Fprintln(jirix.Stdout(), out.String())
+ }
+ return nil
+}
+
// cmdProjectShellPrompt represents the "jiri project shell-prompt" command.
var cmdProjectShellPrompt = &cmdline.Command{
Runner: jiri.RunnerFunc(runProjectShellPrompt),
diff --git a/cmd/jiri/runp.go b/cmd/jiri/runp.go
new file mode 100644
index 0000000..381a18f
--- /dev/null
+++ b/cmd/jiri/runp.go
@@ -0,0 +1,418 @@
+// 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 (
+ "bufio"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "os/signal"
+ "regexp"
+ "sort"
+ "strings"
+ "sync"
+
+ "v.io/jiri"
+ "v.io/jiri/gitutil"
+ "v.io/jiri/profiles/profilescmdline"
+ "v.io/jiri/profiles/profilesreader"
+ "v.io/jiri/project"
+ "v.io/jiri/tool"
+ "v.io/x/lib/cmdline"
+ "v.io/x/lib/envvar"
+ "v.io/x/lib/simplemr"
+)
+
+var (
+ cmdRunP *cmdline.Command
+ runpFlags runpFlagValues
+)
+
+func newRunP() *cmdline.Command {
+ return &cmdline.Command{
+ Runner: jiri.RunnerFunc(runRunp),
+ Name: "runp",
+ Short: "Run a command in parallel across jiri projects",
+ Long: `
+Run a command in parallel across one or more jiri projects using the specified
+profile target's environment. Commands are run using the shell specified by the
+users $SHELL environment variable, or "sh" if that's not set. Thus commands
+are run as $SHELL -c "args..."
+ `,
+ ArgsName: "<command line>",
+ ArgsLong: `
+ A command line to be run in each project specified by the supplied command
+line flags. Any environment variables intended to be evaluated when the
+command line is run must be quoted to avoid expansion before being passed to
+runp by the shell.
+`,
+ }
+}
+
+type runpFlagValues struct {
+ profilescmdline.ReaderFlagValues
+ projectKeys string
+ verbose bool
+ interactive bool
+ hasUncommitted bool
+ hasUntracked bool
+ hasGerritMessage bool
+ showNamePrefix bool
+ showKeyPrefix bool
+ exitOnError bool
+ collateOutput bool
+ editMessage bool
+}
+
+func registerCommonFlags(flags *flag.FlagSet, values *runpFlagValues) {
+ profilescmdline.RegisterReaderFlags(flags, &values.ReaderFlagValues, jiri.ProfilesDBDir)
+ flags.BoolVar(&values.verbose, "v", false, "Print verbose logging information")
+ flags.StringVar(&values.projectKeys, "projects", "", "A Regular expression specifying project keys to run commands in. By default, runp will use projects that have the same branch checked as the current project.")
+ flags.BoolVar(&values.hasUncommitted, "has-uncommitted", false, "If specified, match projects that have, or have no, uncommitted changes")
+ flags.BoolVar(&values.hasUntracked, "has-untracked", false, "if specified, match projects that have, or have no, untracked files")
+ flags.BoolVar(&values.hasGerritMessage, "has-gerrit-message", false, "If specified, match branches that have, or have no, gerrit message")
+ flags.BoolVar(&values.interactive, "interactive", true, "If set, the command to be run is interactive and should not have its stdout/stderr manipulated. This flag cannot be used with -show-name-prefix, -show-key-prefix or -collate-stdout.")
+ flags.BoolVar(&values.showNamePrefix, "show-name-prefix", false, "If set, each line of output from each project will begin with the name of the project followed by a colon. This is intended for use with long running commands where the output needs to be streamed. Stdout and stderr are spliced apart. This flag cannot be used with -interactive, -show-key-prefix or -collate-stdout.")
+ flags.BoolVar(&values.showKeyPrefix, "show-key-prefix", false, "If set, each line of output from each project will begin with the key of the project followed by a colon. This is intended for use with long running commands where the output needs to be streamed. Stdout and stderr are spliced apart. This flag cannot be used with -interactive, -show-name-prefix or -collate-stdout")
+ flags.BoolVar(&values.collateOutput, "collate-stdout", true, "Collate all stdout output from each parallel invocation and display it as if had been generated sequentially. This flag cannot be used with -show-name-prefix, -show-key-prefix or -interactive.")
+ flags.BoolVar(&values.exitOnError, "exit-on-error", false, "If set, all commands will killed as soon as one reports an error, otherwise, each will run to completion.")
+}
+
+func init() {
+ // Avoid an intialization loop between cmdline.Command.Runner which
+ // refers to cmdRunP and runRunp referring back to cmdRunP.ParsedFlags.
+ cmdRunP = newRunP()
+ cmdRoot.Children = append(cmdRoot.Children, cmdRunP)
+ registerCommonFlags(&cmdRunP.Flags, &runpFlags)
+}
+
+type mapInput struct {
+ *project.ProjectState
+ key project.ProjectKey
+ jirix *jiri.X
+ index, total int
+ result error
+}
+
+func newmapInput(jirix *jiri.X, state *project.ProjectState, key project.ProjectKey, index, total int) *mapInput {
+ return &mapInput{
+ ProjectState: state,
+ key: key,
+ jirix: jirix.Clone(tool.ContextOpts{}),
+ index: index,
+ total: total,
+ }
+}
+
+func stateNames(states map[project.ProjectKey]*mapInput) []string {
+ n := []string{}
+ for _, state := range states {
+ n = append(n, state.Project.Name)
+ }
+ sort.Strings(n)
+ return n
+}
+
+func stateKeys(states map[project.ProjectKey]*mapInput) []string {
+ n := []string{}
+ for key := range states {
+ n = append(n, string(key))
+ }
+ sort.Strings(n)
+ return n
+}
+
+type runner struct {
+ args []string
+ reader *profilesreader.Reader
+ serializedWriterLock sync.Mutex
+ collatedOutputLock sync.Mutex
+}
+
+func (r *runner) serializedWriter(w io.Writer) io.Writer {
+ return &sharedLockWriter{&r.serializedWriterLock, w}
+}
+
+type sharedLockWriter struct {
+ mu *sync.Mutex
+ f io.Writer
+}
+
+func (lw *sharedLockWriter) Write(d []byte) (int, error) {
+ lw.mu.Lock()
+ defer lw.mu.Unlock()
+ return lw.f.Write(d)
+}
+
+func copyWithPrefix(prefix string, w io.Writer, r io.Reader) {
+ reader := bufio.NewReader(r)
+ for {
+ line, err := reader.ReadString('\n')
+ if err != nil {
+ if line != "" {
+ fmt.Fprintf(w, "%v: %v\n", prefix, line)
+ }
+ break
+ }
+ fmt.Fprintf(w, "%v: %v", prefix, line)
+ }
+}
+
+type mapOutput struct {
+ mi *mapInput
+ outputFilename string
+ key string
+ err error
+}
+
+func (r *runner) Map(mr *simplemr.MR, key string, val interface{}) error {
+ mi := val.(*mapInput)
+ output := &mapOutput{
+ key: key,
+ mi: mi}
+ jirix := mi.jirix
+ path := os.Getenv("SHELL")
+ if path == "" {
+ path = "sh"
+ }
+ var wg sync.WaitGroup
+ cmd := exec.Command(path, "-c", strings.Join(r.args, " "))
+ cmd.Env = envvar.MapToSlice(jirix.Env())
+ cmd.Dir = mi.ProjectState.Project.Path
+ cmd.Stdin = mi.jirix.Stdin()
+ var stdoutCloser, stderrCloser io.Closer
+ if runpFlags.interactive {
+ cmd.Stdout = jirix.Stdout()
+ cmd.Stderr = jirix.Stderr()
+ } else {
+ var stdout io.Writer
+ stderr := r.serializedWriter(jirix.Stderr())
+ var cleanup func()
+ if runpFlags.collateOutput {
+ // Write standard output to a file, stderr
+ // is not collated.
+ f, err := ioutil.TempFile("", mi.ProjectState.Project.Name+"-")
+ if err != nil {
+ return err
+ }
+ stdout = f
+ output.outputFilename = f.Name()
+ cleanup = func() {
+ os.Remove(output.outputFilename)
+ }
+ // The child process will have exited by the
+ // time this method returns so it's safe to close the file
+ // here.
+ defer f.Close()
+ } else {
+ stdout = r.serializedWriter(jirix.Stdout())
+ cleanup = func() {}
+ }
+ if !runpFlags.showNamePrefix && !runpFlags.showKeyPrefix {
+ // write directly to stdout, stderr if there's no prefix
+ cmd.Stdout = stdout
+ cmd.Stderr = stderr
+ } else {
+ stdoutReader, stdoutWriter, err := os.Pipe()
+ if err != nil {
+ cleanup()
+ return err
+ }
+ stderrReader, stderrWriter, err := os.Pipe()
+ if err != nil {
+ cleanup()
+ stdoutReader.Close()
+ stdoutWriter.Close()
+ return err
+ }
+ cmd.Stdout = stdoutWriter
+ cmd.Stderr = stderrWriter
+ // Record the write end of the pipe so that it can be closed
+ // after the child has exited, this ensures that all goroutines
+ // will finish.
+ stdoutCloser = stdoutWriter
+ stderrCloser = stderrWriter
+ prefix := key
+ if runpFlags.showNamePrefix {
+ prefix = mi.ProjectState.Project.Name
+ }
+ wg.Add(2)
+ go func() { copyWithPrefix(prefix, stdout, stdoutReader); wg.Done() }()
+ go func() { copyWithPrefix(prefix, stderr, stderrReader); wg.Done() }()
+
+ }
+ }
+ if err := cmd.Start(); err != nil {
+ mi.result = err
+ }
+ done := make(chan error)
+ go func() {
+ done <- cmd.Wait()
+ }()
+ select {
+ case output.err = <-done:
+ if output.err != nil && runpFlags.exitOnError {
+ mr.Cancel()
+ }
+ case <-mr.CancelCh():
+ output.err = cmd.Process.Kill()
+ }
+ for _, closer := range []io.Closer{stdoutCloser, stderrCloser} {
+ if closer != nil {
+ closer.Close()
+ }
+ }
+ wg.Wait()
+ mr.MapOut(key, output)
+ return nil
+}
+
+func (r *runner) Reduce(mr *simplemr.MR, key string, values []interface{}) error {
+ for _, v := range values {
+ mo := v.(*mapOutput)
+ jirix := mo.mi.jirix
+ if mo.err != nil {
+ fmt.Fprintf(jirix.Stdout(), "FAILED: %v: %s %v\n", mo.key, strings.Join(r.args, " "), mo.err)
+ return mo.err
+ } else {
+ if runpFlags.collateOutput {
+ r.collatedOutputLock.Lock()
+ defer r.collatedOutputLock.Unlock()
+ defer os.Remove(mo.outputFilename)
+ if fi, err := os.Open(mo.outputFilename); err == nil {
+ io.Copy(jirix.Stdout(), fi)
+ fi.Close()
+ } else {
+ return err
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func runp(jirix *jiri.X, cmd *cmdline.Command, args []string) error {
+ hasUntrackedSet := profilescmdline.IsFlagSet(cmd.ParsedFlags, "has-untracked")
+ hasUncommitedSet := profilescmdline.IsFlagSet(cmd.ParsedFlags, "has-uncommitted")
+ hasGerritSet := profilescmdline.IsFlagSet(cmd.ParsedFlags, "has-gerrit-message")
+
+ if runpFlags.interactive {
+ runpFlags.collateOutput = false
+ }
+
+ var keysRE *regexp.Regexp
+ var err error
+
+ if profilescmdline.IsFlagSet(cmd.ParsedFlags, "projects") {
+ keysRE, err = regexp.Compile(runpFlags.projectKeys)
+ if err != nil {
+ return fmt.Errorf("failed to compile projects regexp: %q: %v", runpFlags.projectKeys, err)
+ }
+ }
+
+ for _, f := range []string{"show-key-prefix", "show-name-prefix"} {
+ if profilescmdline.IsFlagSet(cmd.ParsedFlags, f) {
+ runpFlags.interactive = false
+ runpFlags.collateOutput = true
+ break
+ }
+ }
+
+ git := gitutil.New(jirix.NewSeq())
+ homeBranch, err := git.CurrentBranchName()
+ if err != nil {
+ return fmt.Errorf("failed to determine name of current git branch: %s", err)
+ }
+
+ dirty := false
+ if hasUntrackedSet || hasUncommitedSet {
+ dirty = true
+ }
+ states, err := project.GetProjectStates(jirix, dirty)
+ if err != nil {
+ return err
+ }
+ mapInputs := map[project.ProjectKey]*mapInput{}
+ var keys project.ProjectKeys
+ for key, state := range states {
+ if keysRE != nil {
+ if !keysRE.MatchString(string(key)) {
+ continue
+ }
+ } else {
+ if state.CurrentBranch != homeBranch {
+ continue
+ }
+ }
+ if hasUntrackedSet && (state.HasUntracked != runpFlags.hasUntracked) {
+ continue
+ }
+ if hasUncommitedSet && (state.HasUncommitted != runpFlags.hasUncommitted) {
+ continue
+ }
+ if hasGerritSet {
+ hasMsg := false
+ for _, br := range state.Branches {
+ if (state.CurrentBranch == br.Name) && br.HasGerritMessage {
+ hasMsg = true
+ break
+ }
+ }
+ if hasMsg != runpFlags.hasGerritMessage {
+ continue
+ }
+ }
+ mapInputs[key] = &mapInput{
+ ProjectState: state,
+ jirix: jirix,
+ key: key,
+ }
+ keys = append(keys, key)
+ }
+
+ total := len(mapInputs)
+ index := 1
+ for _, mi := range mapInputs {
+ mi.index = index
+ mi.total = total
+ index++
+ }
+
+ if runpFlags.verbose {
+ fmt.Fprintf(jirix.Stdout(), "Project Names: %s\n", strings.Join(stateNames(mapInputs), " "))
+ fmt.Fprintf(jirix.Stdout(), "Project Keys: %s\n", strings.Join(stateKeys(mapInputs), " "))
+ }
+
+ reader, err := profilesreader.NewReader(jirix, runpFlags.ProfilesMode, runpFlags.DBFilename)
+ runner := &runner{
+ reader: reader,
+ args: args,
+ }
+ mr := simplemr.MR{}
+ if runpFlags.interactive {
+ // Run one mapper at a time.
+ mr.NumMappers = 1
+ sort.Sort(keys)
+ }
+ in, out := make(chan *simplemr.Record, len(mapInputs)), make(chan *simplemr.Record, len(mapInputs))
+ sigch := make(chan os.Signal)
+ signal.Notify(sigch, os.Interrupt)
+ go func() { <-sigch; mr.Cancel() }()
+ go mr.Run(in, out, runner, runner)
+ for _, key := range keys {
+ in <- &simplemr.Record{string(key), []interface{}{mapInputs[key]}}
+ }
+ close(in)
+ <-out
+ return mr.Error()
+}
+
+func runRunp(jirix *jiri.X, args []string) error {
+ return runp(jirix, cmdRunP, args)
+}
diff --git a/cmd/jiri/runp_test.go b/cmd/jiri/runp_test.go
new file mode 100644
index 0000000..325bbc8
--- /dev/null
+++ b/cmd/jiri/runp_test.go
@@ -0,0 +1,249 @@
+// 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 (
+ "flag"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "sync"
+ "testing"
+
+ "v.io/jiri/gitutil"
+ "v.io/jiri/jiritest"
+ "v.io/jiri/project"
+ "v.io/x/lib/gosh"
+)
+
+var (
+ buildJiriOnce sync.Once
+ buildJiriBinDir = ""
+)
+
+func buildJiri(t *testing.T) string {
+ buildJiriOnce.Do(func() {
+ binDir, err := ioutil.TempDir("", "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ sh := gosh.NewShell(t)
+ defer sh.Cleanup()
+ gosh.BuildGoPkg(sh, binDir, "v.io/jiri/cmd/jiri", "-o", "jiri")
+ buildJiriBinDir = binDir
+ })
+ return buildJiriBinDir
+}
+
+func addProjects(t *testing.T, fake *jiritest.FakeJiriRoot) []*project.Project {
+ projects := []*project.Project{}
+ for _, name := range []string{"a", "b", "c", "t1", "t2"} {
+ projectPath := "r." + name
+ if err := fake.CreateRemoteProject(projectPath); err != nil {
+ t.Fatalf("%v", err)
+ }
+ p := project.Project{
+ Name: projectPath,
+ Path: projectPath,
+ Remote: fake.Projects[projectPath],
+ RemoteBranch: "master",
+ }
+ if err := fake.AddProject(p); err != nil {
+ t.Fatalf("%v", err)
+ }
+ projects = append(projects, &p)
+ }
+ if err := fake.UpdateUniverse(false); err != nil {
+ t.Fatalf("%v", err)
+ }
+ return projects
+}
+
+func run(sh *gosh.Shell, dir, bin string, args ...string) string {
+ cmd := sh.Cmd(filepath.Join(dir, bin), args...)
+ if testing.Verbose() {
+ cmd.PropagateOutput = true
+ }
+ return strings.TrimSpace(cmd.CombinedOutput())
+}
+
+func TestMain(m *testing.M) {
+ flag.Parse()
+ r := m.Run()
+ if buildJiriBinDir != "" {
+ os.RemoveAll(buildJiriBinDir)
+ }
+ os.Exit(r)
+}
+
+func TestRunP(t *testing.T) {
+ fake, cleanup := jiritest.NewFakeJiriRoot(t)
+ defer cleanup()
+ projects := addProjects(t, fake)
+ dir, sh := buildJiri(t), gosh.NewShell(t)
+
+ if got, want := len(projects), 5; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ cwd, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Chdir(cwd)
+
+ chdir := func(dir string) {
+ if err := os.Chdir(filepath.Join(fake.X.Root, dir)); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ manifestKey := strings.Replace(string(projects[0].Key()), "r.a", "manifest", -1)
+ keys := []string{manifestKey}
+ for _, p := range projects {
+ keys = append(keys, string(p.Key()))
+ }
+
+ chdir(projects[0].Path)
+
+ got := run(sh, dir, "jiri", "runp", "--show-name-prefix", "-v", "echo")
+ hdr := "Project Names: manifest r.a r.b r.c r.t1 r.t2\n"
+ hdr += "Project Keys: " + strings.Join(keys, " ") + "\n"
+
+ if want := hdr + "manifest: \nr.a: \nr.b: \nr.c: \nr.t1: \nr.t2:"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ got = run(sh, dir, "jiri", "runp", "-v", "--interactive=false", "basename", "$(", "jiri", "project", "info", "-f", "{{.Project.Path}}", ")")
+ if want := hdr + "manifest\nr.a\nr.b\nr.c\nr.t1\nr.t2"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ got = run(sh, dir, "jiri", "runp", "--interactive=false", "git", "rev-parse", "--abbrev-ref", "HEAD")
+ if want := "master\nmaster\nmaster\nmaster\nmaster\nmaster"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ got = run(sh, dir, "jiri", "runp", "-interactive=false", "--show-name-prefix=true", "git", "rev-parse", "--abbrev-ref", "HEAD")
+ if want := "manifest: master\nr.a: master\nr.b: master\nr.c: master\nr.t1: master\nr.t2: master"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ got = run(sh, dir, "jiri", "runp", "--interactive=false", "--show-key-prefix=true", "git", "rev-parse", "--abbrev-ref", "HEAD")
+ if want := strings.Join(keys, ": master\n") + ": master"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ uncollated := run(sh, dir, "jiri", "runp", "--interactive=false", "--collate-stdout=false", "--show-name-prefix=true", "git", "rev-parse", "--abbrev-ref", "HEAD")
+ split := strings.Split(uncollated, "\n")
+ sort.Strings(split)
+ got = strings.TrimSpace(strings.Join(split, "\n"))
+ if want := "manifest: master\nr.a: master\nr.b: master\nr.c: master\nr.t1: master\nr.t2: master"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ got = run(sh, dir, "jiri", "runp", "--show-name-prefix", "--projects=r.t[12]", "echo")
+ if want := "r.t1: \nr.t2:"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ rb := projects[1].Path
+ rc := projects[2].Path
+ t1 := projects[3].Path
+
+ s := fake.X.NewSeq()
+ newfile := func(dir, file string) {
+ testfile := filepath.Join(fake.X.Root, dir, file)
+ _, err := s.Create(testfile)
+ if err != nil {
+ t.Errorf("failed to create %s: %v", testfile, err)
+ }
+ }
+
+ git := func(root, dir string) *gitutil.Git {
+ return gitutil.New(fake.X.NewSeq(), gitutil.RootDirOpt(filepath.Join(fake.X.Root, dir)))
+ }
+
+ newfile(rb, "untracked.go")
+
+ got = run(sh, dir, "jiri", "runp", "--has-untracked", "--show-name-prefix", "echo")
+ if want := "r.b:"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ got = run(sh, dir, "jiri", "runp", "--has-untracked=false", "--show-name-prefix", "echo")
+ if want := "manifest: \nr.a: \nr.c: \nr.t1: \nr.t2:"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ newfile(rc, "uncommitted.go")
+
+ if err := git(fake.X.Root, rc).Add("uncommitted.go"); err != nil {
+ t.Error(err)
+ }
+
+ got = run(sh, dir, "jiri", "runp", "--has-uncommitted", "--show-name-prefix", "echo")
+ if want := "r.c:"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ got = run(sh, dir, "jiri", "runp", "--has-uncommitted=false", "--show-name-prefix", "echo")
+ if want := "manifest: \nr.a: \nr.b: \nr.t1: \nr.t2:"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ // test ordering of has-<x> flags
+ newfile(rc, "untracked.go")
+ got = run(sh, dir, "jiri", "runp", "--has-untracked", "--has-uncommitted", "--show-name-prefix", "echo")
+ if want := "r.c:"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ got = run(sh, dir, "jiri", "runp", "--has-uncommitted", "--has-untracked", "--show-name-prefix", "echo")
+ if want := "r.c:"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ git(fake.X.Root, rb).CreateAndCheckoutBranch("a1")
+ git(fake.X.Root, rb).CreateAndCheckoutBranch("b2")
+ git(fake.X.Root, rc).CreateAndCheckoutBranch("b2")
+ git(fake.X.Root, t1).CreateAndCheckoutBranch("a1")
+
+ chdir(rc)
+
+ // Just the projects with branch b2.
+ got = run(sh, dir, "jiri", "runp", "--show-name-prefix", "echo")
+ if want := "r.b: \nr.c:"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ // All projects since --projects takes precendence over branches.
+ got = run(sh, dir, "jiri", "runp", "--projects=.*", "--show-name-prefix", "echo")
+ if want := "manifest: \nr.a: \nr.b: \nr.c: \nr.t1: \nr.t2:"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ if err := s.MkdirAll(filepath.Join(fake.X.Root, rb, ".jiri", "a1"), os.FileMode(0755)).Done(); err != nil {
+ t.Fatal(err)
+ }
+ newfile(rb, filepath.Join(".jiri", "a1", ".gerrit_commit_message"))
+
+ git(fake.X.Root, rb).CheckoutBranch("a1")
+ git(fake.X.Root, t1).CheckoutBranch("a1")
+ chdir(t1)
+
+ got = run(sh, dir, "jiri", "runp", "--has-gerrit-message", "--show-name-prefix", "echo")
+ if want := "r.b:"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+ got = run(sh, dir, "jiri", "runp", "--has-gerrit-message=false", "--show-name-prefix", "echo")
+ if want := "r.t1:"; got != want {
+ t.Errorf("got %v, want %v", got, want)
+ }
+
+}
diff --git a/project/.api b/project/.api
index 325cd3e..3fb025f 100644
--- a/project/.api
+++ b/project/.api
@@ -7,6 +7,7 @@
pkg project, func CreateSnapshot(*jiri.X, string, string) error
pkg project, func CurrentProjectKey(*jiri.X) (ProjectKey, error)
pkg project, func DataDirPath(*jiri.X, string) (string, error)
+pkg project, func GetProjectState(*jiri.X, ProjectKey, bool) (*ProjectState, error)
pkg project, func GetProjectStates(*jiri.X, bool) (map[ProjectKey]*ProjectState, error)
pkg project, func InstallTools(*jiri.X, string) error
pkg project, func LoadManifest(*jiri.X) (Projects, Tools, error)
diff --git a/project/state.go b/project/state.go
index deaf766..ea6a23a 100644
--- a/project/state.go
+++ b/project/state.go
@@ -5,6 +5,7 @@
package project
import (
+ "fmt"
"path/filepath"
"v.io/jiri"
@@ -94,3 +95,21 @@
}
return states, nil
}
+
+func GetProjectState(jirix *jiri.X, key ProjectKey, checkDirty bool) (*ProjectState, error) {
+ projects, err := LocalProjects(jirix, FastScan)
+ if err != nil {
+ return nil, err
+ }
+ sem := make(chan error, 1)
+ for k, project := range projects {
+ if k == key {
+ state := &ProjectState{
+ Project: project,
+ }
+ setProjectState(jirix, state, checkDirty, sem)
+ return state, <-sem
+ }
+ }
+ return nil, fmt.Errorf("failed to find project key %v", key)
+}