blob: 66adb4016d2b8f75921e7fc24e35e6f2140a35e0 [file] [log] [blame]
// Copyright 2017 The Fuchsia 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"
"sort"
"strings"
"go.fuchsia.dev/jiri"
"go.fuchsia.dev/jiri/cmdline"
"go.fuchsia.dev/jiri/gitutil"
"go.fuchsia.dev/jiri/project"
)
var statusFlags struct {
changes bool
checkHead bool
branch string
commits bool
deleted bool
}
var cmdStatus = &cmdline.Command{
Runner: jiri.RunnerFunc(runStatus),
Name: "status",
Short: "Prints status of all the projects",
Long: `
Prints status for the the projects. It runs git status -s across all the projects
and prints it if there are some changes. It also shows status if the project is on
a rev other then the one according to manifest(Named as JIRI_HEAD in git)
`,
}
func init() {
flags := &cmdStatus.Flags
flags.BoolVar(&statusFlags.changes, "changes", true, "Display projects with tracked or un-tracked changes.")
flags.BoolVar(&statusFlags.checkHead, "check-head", true, "Display projects that are not on HEAD/pinned revisions.")
flags.BoolVar(&statusFlags.commits, "commits", true, "Display commits not merged with remote. This only works when project is on a local branch.")
flags.StringVar(&statusFlags.branch, "branch", "", "Display all projects only on this branch along with their status.")
flags.BoolVar(&statusFlags.deleted, "deleted", false, "List all deleted projects. Other flags would be ignored.")
flags.BoolVar(&statusFlags.deleted, "d", false, "Same as -deleted.")
}
func colorFormatGitLog(jirix *jiri.X, log string) string {
strs := strings.SplitN(log, " ", 2)
strs[0] = jirix.Color.Green(strs[0])
return strings.Join(strs, " ")
}
func colorFormatGitiStatusLog(jirix *jiri.X, log string) string {
strs := strings.SplitN(log, " ", 2)
strs[0] = jirix.Color.Red(strs[0])
return strings.Join(strs, " ")
}
func runStatus(jirix *jiri.X, args []string) error {
localProjects, err := project.LocalProjects(jirix, project.FastScan)
if err != nil {
return err
}
remoteProjects, _, _, err := project.LoadManifestFile(jirix, jirix.JiriManifestFile(), localProjects, false /*localManifest*/)
if err != nil {
return err
}
cDir, err := os.Getwd()
if err != nil {
return err
}
if statusFlags.deleted {
for key, localProject := range localProjects {
if _, remoteOk := remoteProjects[key]; !remoteOk {
relativePath, err := filepath.Rel(cDir, localProject.Path)
if err != nil {
return err
}
fmt.Printf("Name: '%s', Path: '%s'\n", jirix.Color.Red(localProject.Name), jirix.Color.Red(relativePath))
}
continue
}
return nil
}
states, err := project.GetProjectStates(jirix, localProjects, false)
if err != nil {
return err
}
var keys project.ProjectKeys
for key := range localProjects {
keys = append(keys, key)
}
sort.Sort(keys)
deletedProjects := 0
for _, key := range keys {
localProject := localProjects[key]
remoteProject, foundRemote := remoteProjects[key]
if !foundRemote {
deletedProjects++
continue
}
state, ok := states[key]
if !ok {
// this should not happen
panic(fmt.Sprintf("State not found for project %q", localProject.Name))
}
if statusFlags.branch != "" && (statusFlags.branch != state.CurrentBranch.Name) {
continue
}
relativePath, err := filepath.Rel(cDir, localProject.Path)
if err != nil {
return err
}
errorMsg := fmt.Sprintf("getting status for project %s(%s)", localProject.Name, relativePath)
changes, headRev, extraCommits, err := getStatus(jirix, localProject, remoteProject, state.CurrentBranch)
if err != nil {
jirix.Logger.Errorf("%s :%s\n\n", errorMsg, err)
jirix.IncrementFailures()
continue
}
revisionMessage := ""
git := gitutil.New(jirix, gitutil.RootDirOpt(state.Project.Path))
currentLog, err := git.OneLineLog(state.CurrentBranch.Revision)
if err != nil {
jirix.Logger.Errorf("%s :%s\n\n", errorMsg, err)
jirix.IncrementFailures()
continue
}
currentLog = colorFormatGitLog(jirix, currentLog)
if statusFlags.checkHead {
if headRev != state.CurrentBranch.Revision {
headLog, err := git.OneLineLog(headRev)
if err != nil {
jirix.Logger.Errorf("%s :%s\n\n", errorMsg, err)
jirix.IncrementFailures()
continue
}
headLog = colorFormatGitLog(jirix, headLog)
revisionMessage = fmt.Sprintf("\n%s: %s", jirix.Color.Yellow("JIRI_HEAD"), headLog)
revisionMessage = fmt.Sprintf("%s\n%s: %s", revisionMessage, jirix.Color.Yellow("Current Revision"), currentLog)
}
}
if statusFlags.branch != "" || changes != "" || revisionMessage != "" ||
len(extraCommits) != 0 {
fmt.Printf("%s: %s", jirix.Color.Yellow(relativePath), revisionMessage)
fmt.Println()
branch := state.CurrentBranch.Name
if branch == "" {
branch = fmt.Sprintf("DETACHED-HEAD(%s)", currentLog)
}
fmt.Printf("%s: %s\n", jirix.Color.Yellow("Branch"), branch)
if len(extraCommits) != 0 {
fmt.Printf("%s: %d commit(s) not merged to remote\n", jirix.Color.Yellow("Commits"), len(extraCommits))
for _, commitLog := range extraCommits {
fmt.Println(colorFormatGitLog(jirix, commitLog))
}
}
if changes != "" {
changesArr := strings.Split(changes, "\n")
for _, change := range changesArr {
fmt.Println(colorFormatGitiStatusLog(jirix, change))
}
}
fmt.Println()
}
}
if deletedProjects != 0 {
jirix.Logger.Warningf("Found %d deleted project(s), run with -d flag to list them.\n\n", deletedProjects)
}
if jirix.Failures() != 0 {
return fmt.Errorf("completed with non-fatal errors")
}
return nil
}
func getStatus(jirix *jiri.X, local project.Project, remote project.Project, currentBranch project.BranchState) (string, string, []string, error) {
var extraCommits []string
headRev := ""
changes := ""
scm := gitutil.New(jirix, gitutil.RootDirOpt(local.Path))
var err error
if statusFlags.changes {
changes, err = scm.ShortStatus()
if err != nil {
return "", "", nil, err
}
}
if statusFlags.checkHead && remote.Name != "" {
// try getting JIRI_HEAD first
if r, err := scm.CurrentRevisionForRef("JIRI_HEAD"); err == nil {
headRev = r
} else {
headRev, err = project.GetHeadRevision(jirix, remote)
if err != nil {
return "", "", nil, err
}
if r, err := scm.CurrentRevisionForRef(headRev); err != nil {
return "", "", nil, fmt.Errorf("Cannot find revision for ref %q for project %q: %s", headRev, local.Name, err)
} else {
headRev = r
}
}
}
if currentBranch.Name != "" && statusFlags.commits {
remoteBranch := "remotes/origin/" + remote.RemoteBranch
if currentBranch.Tracking != nil {
remoteBranch = currentBranch.Tracking.Name
}
commits, err := scm.ExtraCommits(currentBranch.Name, remoteBranch)
if err != nil {
return "", "", nil, err
}
for _, commit := range commits {
log, err := scm.OneLineLog(commit)
if err != nil {
return "", "", nil, err
}
extraCommits = append(extraCommits, log)
}
}
return changes, headRev, extraCommits, nil
}