go.fuchsia.dev/jiri@v0.0.0-20240502161911-b66513b29486/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  	"encoding/json"
    10  	"fmt"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"sort"
    15  	"text/template"
    16  
    17  	"go.fuchsia.dev/jiri"
    18  	"go.fuchsia.dev/jiri/cmdline"
    19  	"go.fuchsia.dev/jiri/project"
    20  )
    21  
    22  var (
    23  	cleanAllFlag      bool
    24  	cleanupFlag       bool
    25  	useRemoteProjects bool
    26  	jsonOutputFlag    string
    27  	regexpFlag        bool
    28  	templateFlag      string
    29  	useLocalManifest  bool
    30  )
    31  
    32  func init() {
    33  	cmdProject.Flags.BoolVar(&cleanAllFlag, "clean-all", false, "Restore jiri projects to their pristine state and delete all branches.")
    34  	cmdProject.Flags.BoolVar(&cleanupFlag, "clean", false, "Restore jiri projects to their pristine state.")
    35  	cmdProject.Flags.StringVar(&jsonOutputFlag, "json-output", "", "Path to write operation results to.")
    36  	cmdProject.Flags.BoolVar(&regexpFlag, "regexp", false, "Use argument as regular expression.")
    37  	cmdProject.Flags.StringVar(&templateFlag, "template", "", "The template for the fields to display.")
    38  	cmdProject.Flags.BoolVar(&useLocalManifest, "local-manifest", false, "List project status  based on local manifest.")
    39  	cmdProject.Flags.BoolVar(&useRemoteProjects, "list-remote-projects", false, "List remote projects instead of local projects.")
    40  }
    41  
    42  // cmdProject represents the "jiri project" command.
    43  var cmdProject = &cmdline.Command{
    44  	Runner: jiri.RunnerFunc(runProject),
    45  	Name:   "project",
    46  	Short:  "Manage the jiri projects",
    47  	Long: `Cleans all projects if -clean flag is provided else inspect
    48  	the local filesystem and provide structured info on the existing
    49  	projects and branches. Projects are specified using either names or
    50  	regular expressions that are matched against project names. If no
    51  	command line arguments are provided the project that the contains the
    52  	current directory is used, or if run from outside of a given project,
    53  	all projects will be used. The information to be displayed can be
    54  	specified using a Go template, supplied via
    55  the -template flag.`,
    56  	ArgsName: "<project ...>",
    57  	ArgsLong: "<project ...> is a list of projects to clean up or give info about.",
    58  }
    59  
    60  func runProject(jirix *jiri.X, args []string) (e error) {
    61  	if cleanupFlag || cleanAllFlag {
    62  		return runProjectClean(jirix, args)
    63  	} else {
    64  		return runProjectInfo(jirix, args)
    65  	}
    66  }
    67  func runProjectClean(jirix *jiri.X, args []string) (e error) {
    68  	localProjects, err := project.LocalProjects(jirix, project.FullScan)
    69  	if err != nil {
    70  		return err
    71  	}
    72  	projects := make(project.Projects)
    73  	if len(args) > 0 {
    74  		if regexpFlag {
    75  			for _, a := range args {
    76  				re, err := regexp.Compile(a)
    77  				if err != nil {
    78  					return fmt.Errorf("failed to compile regexp %v: %v", a, err)
    79  				}
    80  				for _, p := range localProjects {
    81  					if re.MatchString(p.Name) {
    82  						projects[p.Key()] = p
    83  					}
    84  				}
    85  			}
    86  		} else {
    87  			for _, arg := range args {
    88  				p, err := localProjects.FindUnique(arg)
    89  				if err != nil {
    90  					fmt.Fprintf(jirix.Stderr(), "Error finding local project %q: %v.\n", p.Name, err)
    91  				} else {
    92  					projects[p.Key()] = p
    93  				}
    94  			}
    95  		}
    96  	} else {
    97  		projects = localProjects
    98  	}
    99  	if err := project.CleanupProjects(jirix, projects, cleanAllFlag); err != nil {
   100  		return err
   101  	}
   102  	return nil
   103  }
   104  
   105  // projectInfoOutput defines JSON format for 'project info' output.
   106  type projectInfoOutput struct {
   107  	Name string `json:"name"`
   108  	Path string `json:"path"`
   109  
   110  	// Relative path w.r.t to root
   111  	RelativePath   string   `json:"relativePath"`
   112  	Remote         string   `json:"remote"`
   113  	Revision       string   `json:"revision"`
   114  	CurrentBranch  string   `json:"current_branch,omitempty"`
   115  	Branches       []string `json:"branches,omitempty"`
   116  	Manifest       string   `json:"manifest,omitempty"`
   117  	GerritHost     string   `json:"gerrithost,omitempty"`
   118  	GitSubmoduleOf string   `json:"gitsubmoduleof,omitempty"`
   119  }
   120  
   121  // runProjectInfo provides structured info on local projects.
   122  func runProjectInfo(jirix *jiri.X, args []string) error {
   123  	var tmpl *template.Template
   124  	var err error
   125  	if templateFlag != "" {
   126  		tmpl, err = template.New("info").Parse(templateFlag)
   127  		if err != nil {
   128  			return fmt.Errorf("failed to parse template %q: %v", templateFlag, err)
   129  		}
   130  	}
   131  
   132  	regexps := []*regexp.Regexp{}
   133  	if len(args) > 0 && regexpFlag {
   134  		regexps = make([]*regexp.Regexp, len(args), len(args))
   135  		for i, a := range args {
   136  			re, err := regexp.Compile(a)
   137  			if err != nil {
   138  				return fmt.Errorf("failed to compile regexp %v: %v", a, err)
   139  			}
   140  			regexps[i] = re
   141  		}
   142  	}
   143  
   144  	var states map[project.ProjectKey]*project.ProjectState
   145  	var keys project.ProjectKeys
   146  	projects, err := project.LocalProjects(jirix, project.FastScan)
   147  	if err != nil {
   148  		return err
   149  	}
   150  	if useLocalManifest {
   151  		projects, _, _, err = project.LoadUpdatedManifest(jirix, projects, useLocalManifest)
   152  		if err := project.FilterOptionalProjectsPackages(jirix, jirix.FetchingAttrs, projects, nil); err != nil {
   153  			return err
   154  		}
   155  	}
   156  	if useRemoteProjects {
   157  		projects, _, _, err = project.LoadManifestFile(jirix, jirix.JiriManifestFile(), projects, false)
   158  		if err != nil {
   159  			return err
   160  		}
   161  		// filter optional projects
   162  		// we only need to filter in the remote projects path, otherwise it is only using
   163  		// projects that already exist on disk.
   164  		if err := project.FilterOptionalProjectsPackages(jirix, jirix.FetchingAttrs, projects, nil); err != nil {
   165  			return err
   166  		}
   167  	}
   168  	if len(args) == 0 {
   169  		currentProject, err := project.CurrentProject(jirix)
   170  		if err != nil {
   171  			return err
   172  		}
   173  		// Due to fuchsia.git is checked out at root.
   174  		// set currentProject to nil if current working
   175  		// dir is JIRI_ROOT to allow list all projects.
   176  		cwd, err := os.Getwd()
   177  		if cwd == jirix.Root {
   178  			currentProject = nil
   179  		}
   180  		if currentProject == nil {
   181  			// jiri was run from outside of a project so let's
   182  			// use all available projects.
   183  			states, err = project.GetProjectStates(jirix, projects, false)
   184  			if err != nil {
   185  				return err
   186  			}
   187  			for key := range states {
   188  				keys = append(keys, key)
   189  			}
   190  		} else {
   191  			state, err := project.GetProjectState(jirix, *currentProject, true)
   192  			if err != nil {
   193  				return err
   194  			}
   195  			states = map[project.ProjectKey]*project.ProjectState{
   196  				currentProject.Key(): state,
   197  			}
   198  			keys = append(keys, currentProject.Key())
   199  		}
   200  	} else {
   201  		var err error
   202  		states, err = project.GetProjectStates(jirix, projects, false)
   203  		if err != nil {
   204  			return err
   205  		}
   206  		for key, state := range states {
   207  			if regexpFlag {
   208  				for _, re := range regexps {
   209  					if re.MatchString(state.Project.Name) {
   210  						keys = append(keys, key)
   211  						break
   212  					}
   213  				}
   214  			} else {
   215  				for _, arg := range args {
   216  					if arg == state.Project.Name {
   217  						keys = append(keys, key)
   218  						break
   219  					}
   220  				}
   221  			}
   222  		}
   223  	}
   224  	sort.Sort(keys)
   225  
   226  	info := make([]projectInfoOutput, len(keys))
   227  	for i, key := range keys {
   228  		state := states[key]
   229  		rp, err := filepath.Rel(jirix.Root, state.Project.Path)
   230  		if err != nil {
   231  			// should not happen
   232  			panic(err)
   233  		}
   234  		info[i] = projectInfoOutput{
   235  			Name:           state.Project.Name,
   236  			Path:           state.Project.Path,
   237  			RelativePath:   rp,
   238  			Remote:         state.Project.Remote,
   239  			Revision:       state.Project.Revision,
   240  			CurrentBranch:  state.CurrentBranch.Name,
   241  			Manifest:       state.Project.ManifestPath,
   242  			GerritHost:     state.Project.GerritHost,
   243  			GitSubmoduleOf: state.Project.GitSubmoduleOf,
   244  		}
   245  		for _, b := range state.Branches {
   246  			info[i].Branches = append(info[i].Branches, b.Name)
   247  		}
   248  	}
   249  
   250  	for _, i := range info {
   251  		if templateFlag != "" {
   252  			out := &bytes.Buffer{}
   253  			if err := tmpl.Execute(out, i); err != nil {
   254  				return jirix.UsageErrorf("invalid format")
   255  			}
   256  			fmt.Fprintln(os.Stdout, out.String())
   257  		} else {
   258  			fmt.Printf("* project %s\n", i.Name)
   259  			fmt.Printf("  Path:     %s\n", i.Path)
   260  			fmt.Printf("  Remote:   %s\n", i.Remote)
   261  			fmt.Printf("  Revision: %s\n", i.Revision)
   262  			fmt.Printf("  GitSubmoduleOf: %s\n", i.GitSubmoduleOf)
   263  			if useRemoteProjects {
   264  				fmt.Printf("  Manifest: %s\n", i.Manifest)
   265  			}
   266  			if len(i.Branches) != 0 {
   267  				fmt.Printf("  Branches:\n")
   268  				width := 0
   269  				for _, b := range i.Branches {
   270  					if len(b) > width {
   271  						width = len(b)
   272  					}
   273  				}
   274  				for _, b := range i.Branches {
   275  					fmt.Printf("    %-*s", width, b)
   276  					if i.CurrentBranch == b {
   277  						fmt.Printf(" current")
   278  					}
   279  					fmt.Println()
   280  				}
   281  			} else {
   282  				fmt.Printf("  Branches: none\n")
   283  			}
   284  		}
   285  	}
   286  
   287  	if jsonOutputFlag != "" {
   288  		if err := writeJSONOutput(info); err != nil {
   289  			return err
   290  		}
   291  	}
   292  
   293  	return nil
   294  }
   295  
   296  func writeJSONOutput(result interface{}) error {
   297  	out, err := json.MarshalIndent(&result, "", "  ")
   298  	if err != nil {
   299  		return fmt.Errorf("failed to serialize JSON output: %s", err)
   300  	}
   301  
   302  	err = os.WriteFile(jsonOutputFlag, out, 0600)
   303  	if err != nil {
   304  		return fmt.Errorf("failed write JSON output to %s: %s", jsonOutputFlag, err)
   305  	}
   306  
   307  	return nil
   308  }