109 lines
2.8 KiB
Go
109 lines
2.8 KiB
Go
package main
|
|
|
|
// overview.go — the `overview` command (also reachable as `status -a`): a
|
|
// one-line-per-project summary of every git project under BASE, showing the
|
|
// dirty state and how far each branch is ahead/behind its upstream.
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// projStatus is the collected state of one project for the overview.
|
|
type projStatus struct {
|
|
name string
|
|
branch string
|
|
dirty bool
|
|
ahead, behind int
|
|
hasUpstream bool
|
|
}
|
|
|
|
// overviewAll prints a status summary for all git projects under BASE.
|
|
func overviewAll() {
|
|
entries, err := os.ReadDir(BASE)
|
|
if err != nil {
|
|
errorln("cannot read " + BASE + ": " + err.Error())
|
|
return
|
|
}
|
|
|
|
var rows []projStatus
|
|
width := 0
|
|
for _, e := range entries {
|
|
if !e.IsDir() || strings.HasPrefix(e.Name(), ".") {
|
|
continue
|
|
}
|
|
dir := BASE + "/" + e.Name()
|
|
if !isDir(dir + "/.git") {
|
|
continue
|
|
}
|
|
rows = append(rows, projectStatus(e.Name(), dir))
|
|
if len(e.Name()) > width {
|
|
width = len(e.Name())
|
|
}
|
|
}
|
|
|
|
if len(rows) == 0 {
|
|
fmt.Println(col(cGray, "no git projects under "+BASE))
|
|
return
|
|
}
|
|
|
|
dirtyN, syncN := 0, 0
|
|
for _, r := range rows {
|
|
fmt.Println(formatProjStatus(r, width))
|
|
if r.dirty {
|
|
dirtyN++
|
|
}
|
|
if r.hasUpstream && r.ahead == 0 && r.behind == 0 && !r.dirty {
|
|
syncN++
|
|
}
|
|
}
|
|
fmt.Printf("%s\n", col(cGray, fmt.Sprintf("%d projects · %d dirty · %d in sync", len(rows), dirtyN, syncN)))
|
|
}
|
|
|
|
// projectStatus gathers the git state of a single project directory.
|
|
func projectStatus(name, dir string) projStatus {
|
|
s := projStatus{name: name, branch: "-"}
|
|
if out, err := gitCapture(dir, "rev-parse", "--abbrev-ref", "HEAD"); err == nil {
|
|
s.branch = strings.TrimSpace(out)
|
|
}
|
|
if out, err := gitCapture(dir, "status", "--porcelain"); err == nil && strings.TrimSpace(out) != "" {
|
|
s.dirty = true
|
|
}
|
|
// left/right counts against the upstream: "<behind>\t<ahead>"
|
|
if out, err := gitCapture(dir, "rev-list", "--left-right", "--count", "@{upstream}...HEAD"); err == nil {
|
|
if _, e := fmt.Sscanf(strings.TrimSpace(out), "%d\t%d", &s.behind, &s.ahead); e == nil {
|
|
s.hasUpstream = true
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// formatProjStatus renders one aligned overview row.
|
|
func formatProjStatus(s projStatus, width int) string {
|
|
var marks []string
|
|
if s.dirty {
|
|
marks = append(marks, col(cYellow, "*"))
|
|
}
|
|
if s.ahead > 0 {
|
|
marks = append(marks, col(cGreen, fmt.Sprintf("↑%d", s.ahead)))
|
|
}
|
|
if s.behind > 0 {
|
|
marks = append(marks, col(cRed, fmt.Sprintf("↓%d", s.behind)))
|
|
}
|
|
state := strings.Join(marks, " ")
|
|
if state == "" {
|
|
if s.hasUpstream {
|
|
state = col(cGreen, "✓")
|
|
} else {
|
|
state = col(cGray, "✓ (no upstream)")
|
|
}
|
|
}
|
|
|
|
line := " " + col(cGreen, padRight(s.name, width+2)) + state
|
|
if s.branch != "master" && s.branch != "main" && s.branch != "-" {
|
|
line += col(cGray, " ("+s.branch+")")
|
|
}
|
|
return line
|
|
}
|