mirror of
https://github.com/therootcompany/golib.git
synced 2026-03-13 12:27:59 +00:00
WIP: feat: add cmd/monorelease for checking version info
This commit is contained in:
parent
3db8580d5a
commit
50cfc1fa32
246
cmd/monorelease/main.go
Normal file
246
cmd/monorelease/main.go
Normal file
@ -0,0 +1,246 @@
|
||||
// monorelease - Manages releases for code in monorepos.
|
||||
//
|
||||
// Authored in 2026 by AJ ONeal <aj@therootcompany.com>, assisted by Grok Ai.
|
||||
// To the extent possible under law, the author(s) have dedicated all copyright
|
||||
// and related and neighboring rights to this software to the public domain
|
||||
// worldwide. This software is distributed without any warranty.
|
||||
//
|
||||
// You should have received a copy of the CC0 Public Domain Dedication along with
|
||||
// this software. If not, see <https://creativecommons.org/publicdomain/zero/1.0/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var verbose = flag.Bool("verbose", false, "")
|
||||
var ignoreDirty = flag.String("ignore-dirty", "", "ignore dirty states [u n m d]")
|
||||
|
||||
func runGit(args ...string) (string, error) {
|
||||
cmd := exec.Command("git", args...)
|
||||
var b bytes.Buffer
|
||||
cmd.Stdout = &b
|
||||
err := cmd.Run()
|
||||
return strings.TrimSpace(b.String()), err
|
||||
}
|
||||
|
||||
func isVersion(s string) bool {
|
||||
re := regexp.MustCompile(`^v?\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$`)
|
||||
return re.MatchString(s)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
ignoreSet := make(map[rune]bool)
|
||||
for _, c := range *ignoreDirty {
|
||||
ignoreSet[c] = true
|
||||
}
|
||||
|
||||
// 1. prefix (relative to root)
|
||||
// prefixB, _ := exec.Command("git", "rev-parse", "--show-prefix").Output()
|
||||
// prefix := strings.TrimSuffix(strings.TrimSpace(string(prefixB)), "/")
|
||||
|
||||
// 2. cd root
|
||||
rootB, _ := exec.Command("git", "rev-parse", "--show-toplevel").Output()
|
||||
root := strings.TrimSpace(string(rootB))
|
||||
_ = os.Chdir(root)
|
||||
|
||||
// 3. committed files
|
||||
ls, _ := runGit("ls-files")
|
||||
committed := strings.Split(ls, "\n")
|
||||
|
||||
// 4+6. modules (go + node) + untracked
|
||||
type Module struct {
|
||||
Path string
|
||||
Type string
|
||||
Untracked bool
|
||||
}
|
||||
modMap := make(map[string]Module)
|
||||
modTypes := map[string]string{"go.mod": "go", "package.json": "node"}
|
||||
|
||||
for _, f := range committed {
|
||||
for suf, typ := range modTypes {
|
||||
if f == suf || strings.HasSuffix(f, "/"+suf) {
|
||||
p := filepath.Dir(f)
|
||||
if p == "." {
|
||||
p = ""
|
||||
}
|
||||
modMap[p] = Module{Path: p, Type: typ, Untracked: false}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
untrackedS, _ := runGit("ls-files", "--others", "--exclude-standard")
|
||||
for f := range strings.SplitSeq(untrackedS, "\n") {
|
||||
if f == "" {
|
||||
continue
|
||||
}
|
||||
for suf, typ := range modTypes {
|
||||
if f == suf || strings.HasSuffix(f, "/"+suf) {
|
||||
p := filepath.Dir(f)
|
||||
if p == "." {
|
||||
p = ""
|
||||
}
|
||||
if _, ok := modMap[p]; !ok {
|
||||
modMap[p] = Module{Path: p, Type: typ, Untracked: true}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modules := make([]Module, 0, len(modMap))
|
||||
for _, m := range modMap {
|
||||
modules = append(modules, m)
|
||||
}
|
||||
sort.Slice(modules, func(i, j int) bool { return modules[i].Path < modules[j].Path })
|
||||
|
||||
// 5. dirty (porcelain)
|
||||
por, _ := runGit("status", "--porcelain", ".")
|
||||
porLines := strings.Split(por, "\n")
|
||||
dirtyMod := make(map[string]bool)
|
||||
|
||||
modPrefixes := make([]string, 0, len(modules))
|
||||
for _, m := range modules {
|
||||
if m.Path != "" {
|
||||
modPrefixes = append(modPrefixes, m.Path+"/")
|
||||
}
|
||||
}
|
||||
sort.Slice(modPrefixes, func(i, j int) bool { return len(modPrefixes[i]) > len(modPrefixes[j]) })
|
||||
|
||||
for _, line := range porLines {
|
||||
if len(line) < 3 || line[2] != ' ' {
|
||||
continue
|
||||
}
|
||||
status := line[0:2]
|
||||
fileP := line[3:]
|
||||
|
||||
// classify
|
||||
types := []rune{}
|
||||
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
|
||||
for _, t := range types {
|
||||
if !ignoreSet[t] {
|
||||
thisDirty = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !thisDirty {
|
||||
continue
|
||||
}
|
||||
|
||||
// assign to module
|
||||
matched := false
|
||||
for _, pre := range modPrefixes {
|
||||
if strings.HasPrefix(fileP, pre) {
|
||||
modP := strings.TrimSuffix(pre, "/")
|
||||
dirtyMod[modP] = true
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
dirtyMod[""] = true // root
|
||||
}
|
||||
}
|
||||
|
||||
// 7. tags (highest first)
|
||||
tagsS, _ := runGit("tag", "--list", "--sort=-version:refname")
|
||||
tags := strings.Split(tagsS, "\n")
|
||||
modLatest := make(map[string]string)
|
||||
for _, t := range tags {
|
||||
if t == "" {
|
||||
continue
|
||||
}
|
||||
matched := false
|
||||
for _, m := range modules {
|
||||
match := false
|
||||
if m.Path == "" {
|
||||
if !strings.Contains(t, "/") && isVersion(t) {
|
||||
match = true
|
||||
}
|
||||
} else if strings.HasPrefix(t, m.Path+"/") {
|
||||
suf := t[len(m.Path)+1:]
|
||||
if isVersion(suf) {
|
||||
match = true
|
||||
}
|
||||
}
|
||||
if match {
|
||||
if _, ok := modLatest[m.Path]; !ok {
|
||||
modLatest[m.Path] = t
|
||||
}
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched && *verbose {
|
||||
fmt.Printf("unmatched tag: %s\n", t)
|
||||
}
|
||||
}
|
||||
|
||||
// 8+9. display
|
||||
for _, m := range modules {
|
||||
d := dirtyMod[m.Path]
|
||||
latest := modLatest[m.Path]
|
||||
ver := "v0.0.0-1"
|
||||
commits := 0
|
||||
if latest != "" {
|
||||
suf := latest
|
||||
if m.Path != "" {
|
||||
suf = strings.TrimPrefix(latest, m.Path+"/")
|
||||
}
|
||||
ver = suf
|
||||
|
||||
pArg := "."
|
||||
if m.Path != "" {
|
||||
pArg = "./" + m.Path
|
||||
}
|
||||
cS, _ := runGit("rev-list", "--count", latest+"..", "--", pArg)
|
||||
commits, _ = strconv.Atoi(cS)
|
||||
if commits > 0 {
|
||||
ver += fmt.Sprintf("-%d", commits)
|
||||
}
|
||||
}
|
||||
if d {
|
||||
ver += "-dirty"
|
||||
}
|
||||
if m.Untracked {
|
||||
fmt.Printf("./%s (%s, untracked): %s\n", m.Path, m.Type, ver)
|
||||
} else {
|
||||
fmt.Printf("./%s (%s): %s\n", m.Path, m.Type, ver)
|
||||
}
|
||||
if *verbose && commits > 0 && latest != "" {
|
||||
pArg := "."
|
||||
if m.Path != "" {
|
||||
pArg = "./" + m.Path
|
||||
}
|
||||
logS, _ := runGit("log", latest+"..", "--pretty=format:- %h %s", "--", pArg)
|
||||
if logS != "" {
|
||||
fmt.Println(logS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user