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

     1  // Copyright 2015 The Vanadium Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"path/filepath"
    11  	"regexp"
    12  	"sort"
    13  	"strings"
    14  	"text/template"
    15  
    16  	"v.io/jiri"
    17  	"v.io/jiri/project"
    18  	"v.io/x/lib/cmdline"
    19  )
    20  
    21  var (
    22  	branchesFlag        bool
    23  	cleanupBranchesFlag bool
    24  	noPristineFlag      bool
    25  	checkDirtyFlag      bool
    26  	showNameFlag        bool
    27  	formatFlag          string
    28  )
    29  
    30  func init() {
    31  	cmdProjectClean.Flags.BoolVar(&cleanupBranchesFlag, "branches", false, "Delete all non-master branches.")
    32  	cmdProjectList.Flags.BoolVar(&branchesFlag, "branches", false, "Show project branches.")
    33  	cmdProjectList.Flags.BoolVar(&noPristineFlag, "nopristine", false, "If true, omit pristine projects, i.e. projects with a clean master branch and no other branches.")
    34  	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.")
    35  	cmdProjectShellPrompt.Flags.BoolVar(&showNameFlag, "show-name", false, "Show the name of the current repo.")
    36  	cmdProjectInfo.Flags.StringVar(&formatFlag, "f", "{{.Project.Name}}", "The go template for the fields to display.")
    37  }
    38  
    39  // cmdProject represents the "jiri project" command.
    40  var cmdProject = &cmdline.Command{
    41  	Name:     "project",
    42  	Short:    "Manage the jiri projects",
    43  	Long:     "Manage the jiri projects.",
    44  	Children: []*cmdline.Command{cmdProjectClean, cmdProjectInfo, cmdProjectList, cmdProjectShellPrompt},
    45  }
    46  
    47  // cmdProjectClean represents the "jiri project clean" command.
    48  var cmdProjectClean = &cmdline.Command{
    49  	Runner:   jiri.RunnerFunc(runProjectClean),
    50  	Name:     "clean",
    51  	Short:    "Restore jiri projects to their pristine state",
    52  	Long:     "Restore jiri projects back to their master branches and get rid of all the local branches and changes.",
    53  	ArgsName: "<project ...>",
    54  	ArgsLong: "<project ...> is a list of projects to clean up.",
    55  }
    56  
    57  func runProjectClean(jirix *jiri.X, args []string) (e error) {
    58  	localProjects, err := project.LocalProjects(jirix, project.FullScan)
    59  	if err != nil {
    60  		return err
    61  	}
    62  	var projects project.Projects
    63  	if len(args) > 0 {
    64  		for _, arg := range args {
    65  			p, err := localProjects.FindUnique(arg)
    66  			if err != nil {
    67  				fmt.Fprintf(jirix.Stderr(), "Error finding local project %q: %v.\n", p.Name, err)
    68  			} else {
    69  				projects[p.Key()] = p
    70  			}
    71  		}
    72  	} else {
    73  		projects = localProjects
    74  	}
    75  	if err := project.CleanupProjects(jirix, projects, cleanupBranchesFlag); err != nil {
    76  		return err
    77  	}
    78  	return nil
    79  }
    80  
    81  // cmdProjectList represents the "jiri project list" command.
    82  var cmdProjectList = &cmdline.Command{
    83  	Runner: jiri.RunnerFunc(runProjectList),
    84  	Name:   "list",
    85  	Short:  "List existing jiri projects and branches",
    86  	Long:   "Inspect the local filesystem and list the existing projects and branches.",
    87  }
    88  
    89  // runProjectList generates a listing of local projects.
    90  func runProjectList(jirix *jiri.X, _ []string) error {
    91  	states, err := project.GetProjectStates(jirix, noPristineFlag)
    92  	if err != nil {
    93  		return err
    94  	}
    95  	var keys project.ProjectKeys
    96  	for key := range states {
    97  		keys = append(keys, key)
    98  	}
    99  	sort.Sort(keys)
   100  
   101  	for _, key := range keys {
   102  		state := states[key]
   103  		if noPristineFlag {
   104  			pristine := len(state.Branches) == 1 && state.CurrentBranch == "master" && !state.HasUncommitted && !state.HasUntracked
   105  			if pristine {
   106  				continue
   107  			}
   108  		}
   109  		fmt.Fprintf(jirix.Stdout(), "name=%q remote=%q path=%q\n", state.Project.Name, state.Project.Remote, state.Project.Path)
   110  		if branchesFlag {
   111  			for _, branch := range state.Branches {
   112  				s := "  "
   113  				if branch.Name == state.CurrentBranch {
   114  					s += "* "
   115  				}
   116  				s += branch.Name
   117  				if branch.HasGerritMessage {
   118  					s += " (exported to gerrit)"
   119  				}
   120  				fmt.Fprintf(jirix.Stdout(), "%v\n", s)
   121  			}
   122  		}
   123  	}
   124  	return nil
   125  }
   126  
   127  // cmdProjectInfo represents the "jiri project info" command.
   128  var cmdProjectInfo = &cmdline.Command{
   129  	Runner: jiri.RunnerFunc(runProjectInfo),
   130  	Name:   "info",
   131  	Short:  "Provided structured input for existing jiri projects and branches",
   132  	Long: `
   133  Inspect the local filesystem and provide structured info on the existing projects
   134  and branches. Projects are specified using regular expressions that are matched
   135  against project keys. If no command line arguments are provided the project
   136  that the contains the current directory is used, or if run from outside
   137  of a given project, all projects will be used. The information to be
   138  displayed is specified using a go template, supplied via the -f flag, that is
   139  executed against the v.io/jiri/project.ProjectState structure. This structure
   140  currently has the following fields: ` + fmt.Sprintf("%#v", project.ProjectState{}),
   141  	ArgsName: "<project-keys>...",
   142  	ArgsLong: "<project-keys>... a list of project keys, as regexps, to apply the specified format to",
   143  }
   144  
   145  // runProjectInfo provides structured info on local projects.
   146  func runProjectInfo(jirix *jiri.X, args []string) error {
   147  	tmpl, err := template.New("info").Parse(formatFlag)
   148  	if err != nil {
   149  		return fmt.Errorf("failed to parse template %q: %v", formatFlag, err)
   150  	}
   151  	regexps := []*regexp.Regexp{}
   152  
   153  	if len(args) > 0 {
   154  		regexps = make([]*regexp.Regexp, len(args), len(args))
   155  		for i, a := range args {
   156  			re, err := regexp.Compile(a)
   157  			if err != nil {
   158  				return fmt.Errorf("failed to compile regexp %v: %v", a, err)
   159  			}
   160  			regexps[i] = re
   161  		}
   162  	}
   163  
   164  	dirty := false
   165  	for _, slow := range []string{"HasUncommitted", "HasUntracked"} {
   166  		if strings.Contains(formatFlag, slow) {
   167  			dirty = true
   168  			break
   169  		}
   170  	}
   171  
   172  	var states map[project.ProjectKey]*project.ProjectState
   173  	var keys project.ProjectKeys
   174  	if len(args) == 0 {
   175  		currentProjectKey, err := project.CurrentProjectKey(jirix)
   176  		if err != nil {
   177  			return err
   178  		}
   179  		state, err := project.GetProjectState(jirix, currentProjectKey, true)
   180  		if err != nil {
   181  			// jiri was run from outside of a project so let's
   182  			// use all available projects.
   183  			states, err = project.GetProjectStates(jirix, dirty)
   184  			if err != nil {
   185  				return err
   186  			}
   187  			for key := range states {
   188  				keys = append(keys, key)
   189  			}
   190  		} else {
   191  			states = map[project.ProjectKey]*project.ProjectState{
   192  				currentProjectKey: state,
   193  			}
   194  			keys = append(keys, currentProjectKey)
   195  		}
   196  	} else {
   197  		var err error
   198  		states, err = project.GetProjectStates(jirix, dirty)
   199  		if err != nil {
   200  			return err
   201  		}
   202  		for key := range states {
   203  			for _, re := range regexps {
   204  				if re.MatchString(string(key)) {
   205  					keys = append(keys, key)
   206  					break
   207  				}
   208  			}
   209  		}
   210  	}
   211  	sort.Sort(keys)
   212  
   213  	for _, key := range keys {
   214  		state := states[key]
   215  		out := &bytes.Buffer{}
   216  		if err = tmpl.Execute(out, state); err != nil {
   217  			return jirix.UsageErrorf("invalid format")
   218  		}
   219  		fmt.Fprintln(jirix.Stdout(), out.String())
   220  	}
   221  	return nil
   222  }
   223  
   224  // cmdProjectShellPrompt represents the "jiri project shell-prompt" command.
   225  var cmdProjectShellPrompt = &cmdline.Command{
   226  	Runner: jiri.RunnerFunc(runProjectShellPrompt),
   227  	Name:   "shell-prompt",
   228  	Short:  "Print a succinct status of projects suitable for shell prompts",
   229  	Long: `
   230  Reports current branches of jiri projects (repositories) as well as an
   231  indication of each project's status:
   232    *  indicates that a repository contains uncommitted changes
   233    %  indicates that a repository contains untracked files
   234  `,
   235  }
   236  
   237  func runProjectShellPrompt(jirix *jiri.X, args []string) error {
   238  	states, err := project.GetProjectStates(jirix, checkDirtyFlag)
   239  	if err != nil {
   240  		return err
   241  	}
   242  	var keys project.ProjectKeys
   243  	for key := range states {
   244  		keys = append(keys, key)
   245  	}
   246  	sort.Sort(keys)
   247  
   248  	// Get the key of the current project.
   249  	currentProjectKey, err := project.CurrentProjectKey(jirix)
   250  	if err != nil {
   251  		return err
   252  	}
   253  	var statuses []string
   254  	for _, key := range keys {
   255  		state := states[key]
   256  		status := ""
   257  		if checkDirtyFlag {
   258  			if state.HasUncommitted {
   259  				status += "*"
   260  			}
   261  			if state.HasUntracked {
   262  				status += "%"
   263  			}
   264  		}
   265  		short := state.CurrentBranch + status
   266  		long := filepath.Base(states[key].Project.Name) + ":" + short
   267  		if key == currentProjectKey {
   268  			if showNameFlag {
   269  				statuses = append([]string{long}, statuses...)
   270  			} else {
   271  				statuses = append([]string{short}, statuses...)
   272  			}
   273  		} else {
   274  			pristine := state.CurrentBranch == "master"
   275  			if checkDirtyFlag {
   276  				pristine = pristine && !state.HasUncommitted && !state.HasUntracked
   277  			}
   278  			if !pristine {
   279  				statuses = append(statuses, long)
   280  			}
   281  		}
   282  	}
   283  	fmt.Println(strings.Join(statuses, ","))
   284  	return nil
   285  }