WIP: feat(cmd/monorelease): show table

This commit is contained in:
AJ ONeal 2026-02-28 18:54:15 -07:00
parent 6438ef8064
commit c8c2b63e93
No known key found for this signature in database

View File

@ -12,6 +12,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/csv"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
@ -24,8 +25,12 @@ import (
"strings" "strings"
) )
var verbose = flag.Bool("verbose", false, "") var (
var ignoreDirty = flag.String("ignore-dirty", "", "ignore dirty states [u n m d]") verbose = flag.Bool("verbose", false, "")
ignoreDirty = flag.String("ignore-dirty", "", "ignore dirty states [u n m d]")
csvOut = flag.Bool("csv", false, "output real CSV")
comma = flag.String("comma", ",", "CSV field delimiter")
)
func runGit(args ...string) (string, error) { func runGit(args ...string) (string, error) {
cmd := exec.Command("git", args...) cmd := exec.Command("git", args...)
@ -40,6 +45,15 @@ func isVersion(s string) bool {
return re.MatchString(s) return re.MatchString(s)
} }
type Row struct {
Status string
Type string
Name string
Version string
CurrentTag string
Path string
}
func main() { func main() {
flag.Parse() flag.Parse()
@ -48,25 +62,21 @@ func main() {
ignoreSet[c] = true ignoreSet[c] = true
} }
// prefixB, _ := exec.Command("git", "rev-parse", "--show-prefix").Output() repoDir, _ := runGit("rev-parse", "--show-toplevel")
// prefix := strings.TrimSuffix(strings.TrimSpace(string(prefixB)), "/") _ = os.Chdir(strings.TrimSpace(repoDir))
rootB, _ := exec.Command("git", "rev-parse", "--show-toplevel").Output() repoName := filepath.Base(repoDir)
root := strings.TrimSpace(string(rootB))
_ = os.Chdir(root)
repoName := filepath.Base(root)
ls, _ := runGit("ls-files") gitFiles, _ := runGit("ls-files")
committed := strings.Split(ls, "\n") committed := strings.Split(gitFiles, "\n")
type Module struct { modTypes := map[string]string{"go.mod": "go", "package.json": "node"}
modMap := make(map[string]struct {
Path string Path string
Type string Type string
Untracked bool Untracked bool
Bins map[string]string Bins map[string]string
} })
modMap := make(map[string]Module)
modTypes := map[string]string{"go.mod": "go", "package.json": "node"}
for _, f := range committed { for _, f := range committed {
for suf, typ := range modTypes { for suf, typ := range modTypes {
@ -75,13 +85,19 @@ func main() {
if p == "." { if p == "." {
p = "" p = ""
} }
modMap[p] = Module{Path: p, Type: typ, Untracked: false} modMap[p] = struct {
Path string
Type string
Untracked bool
Bins map[string]string
}{p, typ, false, make(map[string]string)}
break break
} }
} }
} }
untrackedS, _ := runGit("ls-files", "--others", "--exclude-standard")
for _, f := range strings.Split(untrackedS, "\n") { moreFiles, _ := runGit("ls-files", "--others", "--exclude-standard")
for _, f := range strings.Split(moreFiles, "\n") {
if f == "" { if f == "" {
continue continue
} }
@ -92,301 +108,333 @@ func main() {
p = "" p = ""
} }
if _, ok := modMap[p]; !ok { if _, ok := modMap[p]; !ok {
modMap[p] = Module{Path: p, Type: typ, Untracked: true} modMap[p] = struct {
Path string
Type string
Untracked bool
Bins map[string]string
}{p, typ, true, make(map[string]string)}
} }
break break
} }
} }
} }
goCommS, _ := runGit("ls-files", "--", "*.go") goFiles, _ := runGit("ls-files", "*.go")
goUntrS, _ := runGit("ls-files", "--others", "--exclude-standard", "--", "*.go") moreGoFiles, _ := runGit("ls-files", "--others", "--exclude-standard", "*.go")
allGoFiles := []string{} allGoFiles := append(
for _, s := range []string{goCommS, goUntrS} { strings.Split(goFiles, "\n"),
for _, line := range strings.Split(s, "\n") { strings.Split(moreGoFiles, "\n")...,
if line != "" { )
allGoFiles = append(allGoFiles, line)
} // go module prefix matching
goPrefixes := []string{}
for p := range modMap {
if modMap[p].Type == "go" {
goPrefixes = append(goPrefixes, p+"/")
} }
} }
if _, ok := modMap[""]; ok && modMap[""].Type == "go" {
modules := make([]Module, 0, len(modMap)) goPrefixes = append(goPrefixes, "")
for _, m := range modMap {
modules = append(modules, m)
} }
sort.Slice(modules, func(i, j int) bool { return modules[i].Path < modules[j].Path }) sort.Slice(goPrefixes, func(i, j int) bool { return len(goPrefixes[i]) > len(goPrefixes[j]) })
goModulePrefixes := []string{} goModuleOf := make(map[string]string)
for _, m := range modules { for _, f := range allGoFiles {
if m.Type == "go" { if f == "" {
if m.Path == "" { continue
goModulePrefixes = append(goModulePrefixes, "")
} else {
goModulePrefixes = append(goModulePrefixes, m.Path+"/")
}
} }
} dir := filepath.Dir(f)
sort.Slice(goModulePrefixes, func(i, j int) bool {
return len(goModulePrefixes[i]) > len(goModulePrefixes[j])
})
goToModule := make(map[string]string)
for _, gf := range allGoFiles {
dir := filepath.Dir(gf)
if dir == "." { if dir == "." {
dir = "" dir = ""
} }
matchP := "" for _, pre := range goPrefixes {
matched := false if pre == "" || strings.HasPrefix(dir+"/", pre) {
for _, pre := range goModulePrefixes { goModuleOf[f] = strings.TrimSuffix(pre, "/")
if pre != "" && strings.HasPrefix(dir+"/", pre) {
matchP = strings.TrimSuffix(pre, "/")
matched = true
break break
} else if pre == "" && !matched {
matchP = ""
matched = true
} }
} }
if matched {
goToModule[gf] = matchP
}
} }
for i := range modules { // populate bins
m := &modules[i] for p, m := range modMap {
m.Bins = make(map[string]string)
if m.Type == "node" { if m.Type == "node" {
pkgFile := "package.json" pkgPath := filepath.Join(p, "package.json")
if m.Path != "" { if p == "" {
pkgFile = filepath.Join(m.Path, "package.json") pkgPath = "package.json"
} }
data, err := os.ReadFile(pkgFile) data, _ := os.ReadFile(pkgPath)
if err != nil { var pkg struct {
Name string `json:"name"`
Bin map[string]any `json:"bin"`
}
json.Unmarshal(data, &pkg)
if pkg.Bin == nil {
continue continue
} }
var pi struct { for name, v := range pkg.Bin {
Name string `json:"name"` if s, ok := v.(string); ok && s != "" {
Bin any `json:"bin"` rel := filepath.Clean(filepath.Join(p, s))
}
if json.Unmarshal(data, &pi) != nil {
continue
}
switch v := pi.Bin.(type) {
case string:
if v != "" {
rel := filepath.Clean(filepath.Join(m.Path, v))
name := pi.Name
if name == "" {
name = strings.TrimSuffix(filepath.Base(v), filepath.Ext(v))
}
if name == "" || name == "." {
name = "bin"
}
m.Bins[name] = rel m.Bins[name] = rel
} }
case map[string]any:
for k, vv := range v {
if s, ok := vv.(string); ok && s != "" {
rel := filepath.Clean(filepath.Join(m.Path, s))
m.Bins[k] = rel
}
}
} }
} else if m.Type == "go" { } else if m.Type == "go" {
mainDirs := make(map[string]bool) mainDirs := make(map[string]struct{})
for _, gf := range allGoFiles { for _, f := range allGoFiles {
if goToModule[gf] != m.Path { if f == "" || goModuleOf[f] != p || strings.HasSuffix(f, "_test.go") {
continue continue
} }
if strings.HasSuffix(gf, "_test.go") { data, _ := os.ReadFile(f)
continue if strings.Contains(string(data), "\npackage main\n") || strings.HasPrefix(string(data), "package main\n") {
} d := filepath.Dir(f)
data, err := os.ReadFile(gf) if d == "." {
if err != nil { d = ""
continue
}
hasMain := false
for _, line := range strings.Split(string(data), "\n") {
if line == "package main" {
hasMain = true
break
} }
} mainDirs[d] = struct{}{}
if hasMain {
dir := filepath.Dir(gf)
if dir == "." {
dir = ""
}
mainDirs[dir] = true
} }
} }
for dir := range mainDirs { for d := range mainDirs {
var name, fullP string name := repoName
if dir == "" || dir == "." { if d != "" {
name = repoName name = filepath.Base(d)
fullP = "."
} else {
name = filepath.Base(dir)
fullP = dir
} }
m.Bins[name] = fullP m.Bins[name] = d
} }
} }
modMap[p] = m
} }
por, _ := runGit("status", "--porcelain", ".") // dirty
porLines := strings.Split(por, "\n") statusLines, _ := runGit("status", "--porcelain", ".")
dirtyMod := make(map[string]bool) porLines := strings.Split(statusLines, "\n")
dirty := make(map[string]bool)
modPrefixes := make([]string, 0, len(modules)) modPrefixes := make([]string, 0, len(modMap))
for _, m := range modules { for p := range modMap {
if m.Path != "" { if p != "" {
modPrefixes = append(modPrefixes, m.Path+"/") modPrefixes = append(modPrefixes, p+"/")
} }
} }
sort.Slice(modPrefixes, func(i, j int) bool { return len(modPrefixes[i]) > len(modPrefixes[j]) }) sort.Slice(modPrefixes, func(i, j int) bool { return len(modPrefixes[i]) > len(modPrefixes[j]) })
for _, line := range porLines { for _, line := range porLines {
if len(line) < 3 || line[2] != ' ' { if len(line) < 4 {
continue continue
} }
status := line[0:2] st := line[:2]
fileP := line[3:] file := line[3:]
if st == " " || st == "!!" {
types := []rune{} continue
if status == "??" {
types = append(types, 'u')
}
if strings.Contains(status, "A") {
types = append(types, 'n')
}
if strings.Contains(status, "M") || strings.Contains(status, "R") || strings.Contains(status, "C") {
types = append(types, 'm')
}
if strings.Contains(status, "D") {
types = append(types, 'd')
} }
thisDirty := false states := []rune{}
for _, t := range types { if st == "??" {
if !ignoreSet[t] { states = append(states, 'u')
thisDirty = true }
if strings.Contains(st, "A") || strings.Contains(st, "?") {
states = append(states, 'n')
}
if strings.Contains(st, "M") || strings.Contains(st, "R") || strings.Contains(st, "C") {
states = append(states, 'm')
}
if strings.Contains(st, "D") {
states = append(states, 'd')
}
isDirty := false
for _, r := range states {
if !ignoreSet[r] {
isDirty = true
break break
} }
} }
if !thisDirty { if !isDirty {
continue continue
} }
matched := false found := false
for _, pre := range modPrefixes { for _, pre := range modPrefixes {
if strings.HasPrefix(fileP, pre) { if strings.HasPrefix(file, pre) {
modP := strings.TrimSuffix(pre, "/") dirty[strings.TrimSuffix(pre, "/")] = true
dirtyMod[modP] = true found = true
matched = true
break break
} }
} }
if !matched { if !found {
dirtyMod[""] = true dirty[""] = true
} }
} }
tagsS, _ := runGit("tag", "--list", "--sort=-version:refname") // tags
tags := strings.Split(tagsS, "\n") tagLines, _ := runGit("tag", "--list", "--sort=-version:refname")
modLatest := make(map[string]string) tags := strings.Split(tagLines, "\n")
latestTag := make(map[string]string)
for _, t := range tags { for _, t := range tags {
if t == "" { if t == "" {
continue continue
} }
matched := false for p := range modMap {
for _, m := range modules { if p == "" {
match := false
if m.Path == "" {
if !strings.Contains(t, "/") && isVersion(t) { if !strings.Contains(t, "/") && isVersion(t) {
match = true if _, ok := latestTag[p]; !ok {
latestTag[p] = t
}
break
} }
} else if strings.HasPrefix(t, m.Path+"/") { } else if strings.HasPrefix(t, p+"/") && isVersion(strings.TrimPrefix(t, p+"/")) {
suf := t[len(m.Path)+1:] if _, ok := latestTag[p]; !ok {
if isVersion(suf) { latestTag[p] = t
match = true
} }
}
if match {
if _, ok := modLatest[m.Path]; !ok {
modLatest[m.Path] = t
}
matched = true
break break
} }
} }
if !matched && *verbose {
fmt.Printf("unmatched tag: %s\n", t)
}
} }
for _, m := range modules { // rows
d := dirtyMod[m.Path] var rows []Row
latest := modLatest[m.Path] for p, m := range modMap {
ver := "v0.0.0-1" tag := latestTag[p]
commits := 0 suf := tag
if latest != "" { if p != "" {
suf := latest suf = strings.TrimPrefix(tag, p+"/")
if m.Path != "" { }
suf = strings.TrimPrefix(latest, m.Path+"/")
}
ver = suf
pArg := "." ver := "v0.0.0"
if m.Path != "" { if tag != "" {
pArg = "./" + m.Path ver = suf
}
commits := 0
if tag != "" {
scope := "."
if p != "" {
scope = "./" + p
} }
cS, _ := runGit("rev-list", "--count", latest+"..", "--", pArg) n, _ := runGit("rev-list", "--count", tag+"..", "--", scope)
commits, _ = strconv.Atoi(cS) c, _ := strconv.Atoi(n)
commits = c
if commits > 0 { if commits > 0 {
ver += fmt.Sprintf("-%d", commits) ver += fmt.Sprintf("-%d", commits)
} }
} else {
ver += "-1"
} }
if d { if dirty[p] {
ver += "-dirty" ver += "-dirty"
} }
pathShow := m.Path pathShow := "."
if pathShow == "" { if p != "" {
pathShow = "." pathShow = "./" + p
} }
unStr := ""
statusParts := []string{}
if m.Untracked { if m.Untracked {
unStr = ", untracked" statusParts = append(statusParts, "untracked")
} }
fmt.Printf("./%s (%s%s): %s\n", pathShow, m.Type, unStr, ver) if dirty[p] {
var ds []string
if len(m.Bins) > 0 { s, _ := runGit("status", "--porcelain", pathShow)
names := make([]string, 0, len(m.Bins)) if strings.Contains(s, " M") {
for n := range m.Bins { ds = append(ds, "m")
names = append(names, n)
} }
sort.Strings(names) s, _ = runGit("status", "--porcelain", pathShow)
for _, n := range names { if strings.Contains(s, "??") {
p := m.Bins[n] ds = append(ds, "u")
showP := p }
if showP == "" || showP == "." { // simplified; can expand later
showP = "." if len(ds) > 0 {
} statusParts = append(statusParts, fmt.Sprintf("dirty (%s)", strings.Join(ds, "")))
fmt.Printf(" %s -> ./%s\n", n, showP)
} }
} }
if commits > 0 {
statusParts = append(statusParts, "new commits")
}
if len(statusParts) == 0 {
statusParts = append(statusParts, "current")
}
status := strings.Join(statusParts, ", ")
if *verbose && commits > 0 && latest != "" { // module row
pArg := "." rows = append(rows, Row{
if m.Path != "" { Status: status,
pArg = "./" + m.Path Type: "mod",
Name: filepath.Base(pathShow),
Version: ver,
CurrentTag: tag,
Path: filepath.Join(pathShow, m.Type+".mod"),
})
// bin rows
binNames := make([]string, 0, len(m.Bins))
for n := range m.Bins {
binNames = append(binNames, n)
}
sort.Strings(binNames)
for _, name := range binNames {
binPath := m.Bins[name]
binShow := "."
if binPath != "" {
binShow = "./" + binPath
} }
logS, _ := runGit("log", latest+"..", "--pretty=format:- %h %s", "--", pArg) rows = append(rows, Row{
if logS != "" { Status: status,
fmt.Println(logS) Type: "bin",
Name: name,
Version: ver,
CurrentTag: tag,
Path: binShow + "/",
})
}
}
// output
if *csvOut {
w := csv.NewWriter(os.Stdout)
w.Comma = rune((*comma)[0])
w.Write([]string{"status", "type", "name", "version", "current tag", "path"})
for _, r := range rows {
w.Write([]string{r.Status, r.Type, r.Name, r.Version, r.CurrentTag, r.Path})
}
w.Flush()
return
}
// aligned table
var max [6]int
for _, r := range rows {
l := []string{r.Status, r.Type, r.Name, r.Version, r.CurrentTag, r.Path}
for i, s := range l {
if len(s) > max[i] {
max[i] = len(s)
} }
} }
} }
fmt.Printf("%-*s | %-*s | %-*s | %-*s | %-*s | %s\n",
max[0], "status",
max[1], "type",
max[2], "name",
max[3], "version",
max[4], "current tag",
"path",
)
fmt.Printf("%s-+-%s-+-%s-+-%s-+-%s-+-%s\n",
strings.Repeat("-", max[0]),
strings.Repeat("-", max[1]),
strings.Repeat("-", max[2]),
strings.Repeat("-", max[3]),
strings.Repeat("-", max[4]),
strings.Repeat("-", max[5]),
)
for _, r := range rows {
fmt.Printf("%-*s | %-*s | %-*s | %-*s | %-*s | %s\n",
max[0], r.Status,
max[1], r.Type,
max[2], r.Name,
max[3], r.Version,
max[4], r.CurrentTag,
r.Path,
)
}
} }