Compare commits

..

No commits in common. "master" and "v0.0.136" have entirely different histories.

22 changed files with 669 additions and 700 deletions

View File

@ -5,7 +5,7 @@
# go install # go install
all: forgeConfig.pb.go patch.pb.go set.pb.go goimports vet all: forgeConfig.pb.go patchset.pb.go goimports vet
generate: clean generate: clean
autogenpb --proto patchset.proto autogenpb --proto patchset.proto
@ -27,14 +27,8 @@ clean:
forgeConfig.pb.go: forgeConfig.proto forgeConfig.pb.go: forgeConfig.proto
autogenpb --proto forgeConfig.proto autogenpb --proto forgeConfig.proto
# patchset.pb.go: patchset.proto patchset.pb.go: patchset.proto
# autogenpb --proto patchset.proto autogenpb --proto patchset.proto
patch.pb.go: patch.proto
autogenpb --proto patch.proto
set.pb.go: set.proto
autogenpb --proto set.proto
protoc-test: protoc-test:
cd ~/go/src && protoc \ cd ~/go/src && protoc \

View File

@ -1,10 +1,10 @@
package forgepb package forgepb
import ( import (
"os"
"path/filepath" "path/filepath"
"time" "time"
"go.wit.com/lib/config"
"go.wit.com/lib/gui/shell" "go.wit.com/lib/gui/shell"
"go.wit.com/lib/protobuf/gitpb" "go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log" "go.wit.com/log"
@ -81,7 +81,7 @@ func (f *Forge) DoAllCheckoutMaster() error {
f.RillFuncError(rillCheckoutMaster) f.RillFuncError(rillCheckoutMaster)
count := f.RillReload() count := f.RillReload()
if count != 0 { if count != 0 {
config.SetChanged("repos", true) f.ConfigSave()
} }
total, count, nope, err := f.IsEverythingOnMaster() total, count, nope, err := f.IsEverythingOnMaster()
@ -111,12 +111,13 @@ func rillCheckoutMaster(repo *gitpb.Repo) error {
// 'giterr' means something is very wrong with this repo // 'giterr' means something is very wrong with this repo
if repo.GetMasterVersion() == "giterr" { if repo.GetMasterVersion() == "giterr" {
repo.CheckoutMaster() repo.CheckoutMaster()
log.Info(repo.GetFullPath(), "master == giterr. BAD REPO") log.Info("master == giterr. BAD REPO", repo.GetFullPath())
log.Info(repo.GetFullPath(), "master == giterr. BAD REPO git describe --tags master --always") log.Info("master == giterr. BAD REPO", repo.GetFullPath())
log.Info(repo.GetFullPath(), "master == giterr. BAD REPO. todo: figure this out in rillCheckoutMaster()") log.Info("master == giterr. BAD REPO", repo.GetFullPath())
// cmd := []string{"git", "checkout", "main"} // todo: figure out main cmd := []string{"git", "checkout", "main"} // todo: figure out main
// repo.RunVerbose(cmd) repo.RunVerbose(cmd)
return log.Errorf("master version can not be determined") os.Exit(-1)
return nil
} }
if repo.GetCurrentBranchName() == repo.GetMasterBranchName() { if repo.GetCurrentBranchName() == repo.GetMasterBranchName() {
// repo is already on master // repo is already on master
@ -160,7 +161,7 @@ func (f *Forge) DoAllCheckoutUser(force bool) error {
f.RillFuncError(rillCheckoutUser) f.RillFuncError(rillCheckoutUser)
count := f.RillReload() count := f.RillReload()
if count != 0 { if count != 0 {
config.SetChanged("repos", true) f.ConfigSave()
} }
total, count, nope, err := f.IsEverythingOnUser() total, count, nope, err := f.IsEverythingOnUser()
@ -204,10 +205,10 @@ func (f *Forge) makeUserBranches() error {
} }
func testReload(repo *gitpb.Repo) error { func testReload(repo *gitpb.Repo) error {
if !repo.HasChanged() { if !repo.DidRepoChange() {
return nil return nil
} }
repo.ReloadCheck() repo.Reload()
return log.Errorf("repo changed") return log.Errorf("repo changed")
} }
@ -244,7 +245,7 @@ func (f *Forge) DoAllCheckoutDevelNew(force bool) error {
counter += 1 counter += 1
} }
log.Info("reloaded", counter, "repos") log.Info("reloaded", counter, "repos")
config.SetChanged("repos", true) f.configSave = true
return nil return nil
} }
@ -259,7 +260,7 @@ func (f *Forge) DoAllCheckoutDevel(force bool) error {
f.RillFuncError(rillCheckoutDevel) f.RillFuncError(rillCheckoutDevel)
count := f.RillReload() count := f.RillReload()
if count != 0 { if count != 0 {
config.SetChanged("repos", true) f.ConfigSave()
} }
total, count, nope, err := f.IsEverythingOnDevel() total, count, nope, err := f.IsEverythingOnDevel()

View File

@ -3,9 +3,7 @@
package forgepb package forgepb
import ( import (
"os" "go.wit.com/lib/fhelp"
"go.wit.com/lib/config"
"go.wit.com/lib/gui/prep" "go.wit.com/lib/gui/prep"
"go.wit.com/log" "go.wit.com/log"
) )
@ -18,69 +16,29 @@ func (f *Forge) ConfigSave() error {
return log.Errorf("forge.Config == nil") return log.Errorf("forge.Config == nil")
} }
if config.HasChanged("forge") { if f.Config.Mode != ForgeMode_NORMAL {
// only let forge save the config files (?) fhelp.DumpENV("forge:")
if prep.AppName() == "forge" || prep.AppName() == "guireleaser" { f.Config.DumpENV()
log.Info("Okay, this is", prep.AppName()) }
} else {
log.Info("This is not forge")
return log.Errorf("Only forge can save the config files")
}
if e := f.Config.ConfigSave(); e != nil { // only let forge save the config files (?)
log.Info("forge.Config.ConfigSave() error", e) if prep.AppName() == "forge" || prep.AppName() == "guireleaser" {
log.Info("Okay, this is", prep.AppName())
} else {
log.Info("This is not forge")
return log.Errorf("Only forge can save the config files")
}
if e := f.Config.ConfigSave(); e != nil {
log.Info("forge.Config.ConfigSave() error", e)
err = e
}
if f.Repos != nil {
if e := f.Repos.ConfigSave(f.Config.ReposPB); e != nil {
log.Info("forge.Repos.ConfigSave() error", e)
err = e err = e
} }
} }
return err return err
} }
// functions to import and export the protobuf
// data to and from config files
// write to ~/.config/forge/
func (cfg *ForgeConfigs) ConfigSave() error {
var header string
header += "\n"
header += "# the forge config file\n"
header += "# You can customize things like:\n"
header += "#\n"
header += "# * which repos you have write access to\n"
header += "# * custom branch names for 'master', 'devel' and 'user'\n"
header += "# * 'favorites' so you can remember which things you like\n"
header += "# * sometimes protobuf TEXT can fail so as a backup this also creates a .json file\n"
header += "#\n"
header += "\n"
return config.ConfigSaveWithHeader(cfg, header)
}
func (cfg *ForgeConfigs) DumpENV() {
log.Infof("CfgPB.Filename = %s\n", cfg.Filename)
log.Infof("CfgPB.ReposPB = %s\n", cfg.ReposPB)
log.Infof("CfgPB.ReposDir = %s\n", cfg.ReposDir)
log.Infof("CfgPB.PatchDir = %s\n", cfg.PatchDir)
log.Infof("CfgPB.ForgeURL = %s\n", cfg.ForgeURL)
if cfg.GoWork {
log.Infof("CfgPB.GoWork = %v\n", cfg.GoWork)
}
log.Infof("CfgPB.Mode = %s\n", cfg.Mode)
// log.Infof("CfgPB.Hostname=%s\n", cfg.Hostname)
if cfg.ReposPB != os.Getenv("FORGE_REPOSPB") {
log.Infof("CfgPB file problem: cfg.ReposPB=%s != FORGE_REPOSPB=%s\n", cfg.ReposPB, os.Getenv("FORGE_REPOSPB"))
}
}
/*
if f.Config.Xterm == "" {
f.Config.Xterm = "xterm"
f.Config.XtermArgv = append(f.Config.XtermArgv, "-bg")
f.Config.XtermArgv = append(f.Config.XtermArgv, "black")
f.Config.XtermArgv = append(f.Config.XtermArgv, "-fg")
f.Config.XtermArgv = append(f.Config.XtermArgv, "white")
f.SetConfigSave(true)
}
*/
func (cfg *ForgeConfigs) InitDefaults() {
}

View File

@ -6,7 +6,6 @@ package forgepb
import ( import (
"time" "time"
"go.wit.com/lib/config"
"go.wit.com/lib/gui/shell" "go.wit.com/lib/gui/shell"
"go.wit.com/lib/protobuf/gitpb" "go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log" "go.wit.com/log"
@ -23,7 +22,8 @@ func (f *Forge) CheckDirtyQuiet() {
for _, s := range stats { for _, s := range stats {
if s.Err == nil { if s.Err == nil {
} else { } else {
config.SetChanged("repos", true) // log.Info("forge SetConfigSave(true)")
f.SetConfigSave(true)
changed = true changed = true
} }
} }
@ -45,7 +45,7 @@ func (f *Forge) CheckDirty() *gitpb.Repos {
if s.Err == nil { if s.Err == nil {
} else { } else {
log.Info(i, s.Err) log.Info(i, s.Err)
config.SetChanged("repos", true) f.SetConfigSave(true)
} }
} }
@ -74,11 +74,13 @@ func doCheckDirty(repo *gitpb.Repo) error {
} else { } else {
log.Info("Repo changed to clean", repo.FullPath) log.Info("Repo changed to clean", repo.FullPath)
return log.Errorf("%s repo changed to clean", repo.FullPath) return log.Errorf("%s repo changed to clean", repo.FullPath)
// f.SetConfigSave(true)
} }
} else { } else {
if repo.CheckDirty() { if repo.CheckDirty() {
log.Info("Repo changed to dirty", repo.FullPath) log.Info("Repo changed to dirty", repo.FullPath)
return log.Errorf("%s repo changed to dirty", repo.FullPath) return log.Errorf("%s repo changed to dirty", repo.FullPath)
// f.SetConfigSave(true)
} else { } else {
// nothing changed // nothing changed
} }

52
forgeConfig.config.go Normal file
View File

@ -0,0 +1,52 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
"go.wit.com/lib/config"
"go.wit.com/log"
)
// functions to import and export the protobuf
// data to and from config files
// write to ~/.config/forge/
func (cfg *ForgeConfigs) ConfigSave() error {
var header string
header += "\n"
header += "# the forge config file\n"
header += "# You can customize things like:\n"
header += "#\n"
header += "# * which repos you have write access to\n"
header += "# * custom branch names for 'master', 'devel' and 'user'\n"
header += "# * 'favorites' so you can remember which things you like\n"
header += "#\n"
header += "\n"
return config.ConfigSaveWithHeader(cfg, header)
}
func (cfg *ForgeConfigs) DumpENV() {
log.Info("todo: DumpENV()")
}
// load the ~/.config/forge/ files
func (c *ForgeConfigs) ConfigLoad(fullpath string) error {
return nil
}
/*
if f.Config.Username == "" {
usr, _ := user.Current()
f.Config.Username = usr.Username
f.SetConfigSave(true)
}
if f.Config.Xterm == "" {
f.Config.Xterm = "xterm"
f.Config.XtermArgv = append(f.Config.XtermArgv, "-bg")
f.Config.XtermArgv = append(f.Config.XtermArgv, "black")
f.Config.XtermArgv = append(f.Config.XtermArgv, "-fg")
f.Config.XtermArgv = append(f.Config.XtermArgv, "white")
f.SetConfigSave(true)
}
*/

View File

@ -55,15 +55,13 @@ message ForgeConfigs { // `autogenpb:mar
repeated string xtermArgv = 6; // the argv line for xterm repeated string xtermArgv = 6; // the argv line for xterm
string defaultGui = 7; // default GUI plugin to use string defaultGui = 7; // default GUI plugin to use
ForgeMode mode = 8; // what "mode" forge is in ForgeMode mode = 8; // what "mode" forge is in
bool goWork = 9; // true if there is a go.work file string goSrc = 9; // is ~/go/src unless a go.work file is found
bool pathLock = 10; // the path is locked bool pathLock = 10; // the path is locked
string ReposPB = 11; // where the repos.pb is string ReposPB = 11; // where the repos.pb is
string ReposDir = 12; // where the repos are string ReposDir = 12; // where the repos are
string PatchDir = 13; // patch dir string PatchDir = 13; // patch dir
string ForgeURL = 14; // forge URL string ForgeURL = 14; // forge URL
string Filename = 15; // filename of the config file string Filename = 15; // filename of the config file
int32 rillX = 16; // used by rill
int32 rillY = 17; // used by rill
} }
// this generic message is used by autogen to identify and // this generic message is used by autogen to identify and

90
goSrcFind.go Normal file
View File

@ -0,0 +1,90 @@
package forgepb
// returns whatever your golang source dir is
// If there is a go.work file in your parent, that directory will be returned
// otherwise, return ~/go/src
import (
"fmt"
"os"
"path/filepath"
)
func (f *Forge) GetHome() string {
return f.Config.ReposDir
}
/*
// look for a go.work file
// otherwise use ~/go/src
func (f *Forge) findGoSrc() (string, error) {
pwd, err := os.Getwd()
// startpwd, _ := os.Getwd()
if err == nil {
// Check for go.work in the current directory and then move up until root
if pwd, err := digup(pwd); err == nil {
f.goWork = true
// found an existing go.work file
// os.Chdir(pwd)
return pwd, nil
} else {
// if there is an error looking for the go.work file
// default to using ~/go/src
// log.Info("forge.digup() err", pwd, err)
}
} else {
// this shouldn't really happen. maybe your working directory got deleted
log.Info("forge.findGoSrc() os.Getwd() was probably deleted", pwd, err)
}
// there are no go.work files, resume the ~/go/src behavior from prior to golang 1.22
pwd, err = useGoSrc()
return pwd, err
}
// this is the 'old way" and works fine for me. I use it because I like the ~/go/src directory
// because I know exactly what is in it: GO stuff & nothing else
func useGoSrc() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
pwd := filepath.Join(homeDir, "go/src")
err = os.MkdirAll(pwd, os.ModePerm)
return pwd, err
}
func (f *Forge) goWorkExists() bool {
var err error
workFilePath := filepath.Join(f.Config.ReposDir, "go.work")
if _, err = os.Stat(workFilePath); err == nil {
// log.Info("f.goWorkExists() found", workFilePath)
return true
} else if !os.IsNotExist(err) {
// log.Info("f.goWorkExists() missing", workFilePath)
return false
}
// probably false, but some other error
// log.Info("f.goWorkExists() os.Stat() error", err, workFilePath)
return false
}
*/
func digup(path string) (string, error) {
for {
workFilePath := filepath.Join(path, "go.work")
if _, err := os.Stat(workFilePath); err == nil {
return path, nil // Found the go.work file
} else if !os.IsNotExist(err) {
return "", err // An error other than not existing
}
parentPath := filepath.Dir(path)
if parentPath == path {
break // Reached the filesystem root
}
path = parentPath
}
return "", fmt.Errorf("no go.work file found")
}

225
goSrcScan.go Normal file
View File

@ -0,0 +1,225 @@
package forgepb
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/destel/rill"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
func reloadCheck(repo *gitpb.Repo) error {
if err := repo.ReloadCheck(); err != nil {
log.Infof("%s reload() says %v\n", repo.FullPath, err)
// configSave = true
return err
}
return nil
}
func (f *Forge) ScanGoSrc() (bool, error) {
dirs, err := gitDirectoriesNew(f.Config.ReposDir)
if err != nil {
return false, err
}
stats := f.RillRepos(reloadCheck)
for _, stat := range stats {
if stat.Err == nil {
continue
}
f.SetConfigSave(true)
/*
dur := stat.End.Sub(stat.Start)
if dur > time.Second {
log.Infof("%s checkRemoteBranches() took a long time (%s) (err=%v)\n", path, shell.FormatDuration(dur), stat.Err)
}
*/
}
var gopaths []string
for _, dir := range dirs {
// log.Info("forge.ScanGoSrc()", dir)
if strings.HasPrefix(dir, f.Config.ReposDir) {
gopath := strings.TrimPrefix(dir, f.Config.ReposDir)
gopath = strings.Trim(gopath, "/")
if r := f.FindByGoPath(gopath); r != nil {
// log.Info("already have", gopath)
continue
}
gopaths = append(gopaths, gopath)
} else {
log.Log(WARN, "ScanGoSrc() bad:", dir)
return false, errors.New("forgepb.ScanGoSrc() bad dir: " + dir)
}
}
newcount, err := f.rillScanDirs(gopaths)
if err != nil {
log.Info("go src dir problem. exit for now?", err)
return false, err
}
if newcount != 0 {
log.Info("forge go src scan found", newcount, "repos")
f.SetConfigSave(true)
}
return true, err
}
// returns a repo protobuf for a directory if the directory is a git repo
func (f *Forge) ScanDir(dir string) *gitpb.Repo {
repo, err := f.Repos.NewRepo(dir, "")
if err != nil {
log.Info("ScanDir() error", dir, err)
return nil
}
return repo
}
// doesn't enter the directory any further when it finds a .git/
// not stupid like my old version
func gitDirectoriesNew(srcDir string) ([]string, error) {
var all []string
var trip bool
err := filepath.WalkDir(srcDir, func(path string, d os.DirEntry, err error) error {
if err != nil {
// Handle possible errors, like permission issues
fmt.Fprintf(os.Stderr, "error accessing path %q: %v\n", path, err)
return err
}
if d.IsDir() {
// log.Info("path is dir", path)
} else {
_, fname := filepath.Split(path)
switch fname {
case "repos.pb":
case "go.work":
case "go.work.last":
case "go.work.sum":
default:
// todo: figure out a way to do padding for init()
if trip == false {
log.Info("WARNING:")
}
log.Info("WARNING: you have an untracked file outside of any .git repository:", path)
trip = true
}
return nil
}
gitdir := filepath.Join(path, ".git")
_, err2 := os.Stat(gitdir)
if !os.IsNotExist(err2) {
all = append(all, path)
return filepath.SkipDir
}
return nil
})
//
// probably always leave this here forever
// this check, along with CheckDirty() makes sure you can safely delete ~/go/src or the go.work directory
// because everything is either checked in or deleted. An important thing to know!
if trip {
log.Info("WARNING:")
log.Info("WARNING: there isn't a way to disable this warning yet")
log.Info("WARNING: probably this is a good thing however. you don't want to leave files outside of git repos here")
log.Info("WARNING: so this warning should probably stay")
log.Info("WARNING:")
log.Info("WARNING: this also might mean you put these files here because you are actively working on them")
log.Info("WARNING: and you don't want to forget about them")
log.Info("WARNING:")
}
return all, err
}
func gitDirectoriesOld(srcDir string) ([]string, error) {
var all []string
err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Log(WARN, "Error accessing path:", path, err)
return nil
}
// Check if the path is a directory and has a .git subdirectory
if info.IsDir() && IsGitDir(path) {
all = append(all, path)
}
return nil
})
if err != nil {
log.Log(WARN, "Error walking the path:", srcDir, err)
}
return all, err
}
// IsGitDir checks if a .git directory exists inside the given directory
func IsGitDir(dir string) bool {
gitDir := filepath.Join(dir, ".git")
info, err := os.Stat(gitDir)
if os.IsNotExist(err) {
return false
}
return info.IsDir()
}
// rill is awesome. long live rill
// attempt scan with rill
func (f *Forge) rillScanDirs(gopaths []string) (int, error) {
// Convert a slice of user IDs into a channel
ids := rill.FromSlice(gopaths, nil)
// Read users from the API.
// Concurrency = 20
dirs := rill.Map(ids, 20, func(id string) (*gitpb.Repo, error) {
return f.checkpath(id, "")
})
var counter int
// Activate users.
// Concurrency = 10
err := rill.ForEach(dirs, 10, func(repo *gitpb.Repo) error {
counter += 1
return nil
})
return counter, err
}
func (f *Forge) checkpath(gopath string, url string) (*gitpb.Repo, error) {
fullpath := filepath.Join(f.Config.ReposDir, gopath)
log.Info("forge creating protobuf for", fullpath)
repo, err := f.NewGoRepo(gopath, "")
if err != nil {
log.Info("\tprotobuf error", gopath, err)
}
return repo, err
}
// deletes the repo from the protobuf (pray your mutex locks are working)
// re-scans the repo
// returns the new repo
func (f *Forge) ReAdd(repo *gitpb.Repo) (*gitpb.Repo, error) {
if repo == nil {
return nil, log.Errorf("can't delete repo == nil")
}
fullpath := repo.GetFullPath()
ns := repo.GetNamespace()
if !f.Repos.Delete(repo) {
return nil, log.Errorf("delete of repo failed")
}
repo, err := f.AddNamespaceDir(ns, fullpath)
if err != nil {
log.Info("ReAdd() error", fullpath, err)
return nil, err
}
f.configSave = true
return repo, err
}

View File

@ -215,7 +215,7 @@ func (f *Forge) printRepoToTable(repo *gitpb.Repo, sizes []int, full bool) {
end += "(u:" + repo.GetUserBranchName() + ") " end += "(u:" + repo.GetUserBranchName() + ") "
} }
debname := f.Config.DebName(repo.GetNamespace()) debname := f.Config.DebName(repo.GetGoPath())
if debname != filepath.Base(gopath) { if debname != filepath.Base(gopath) {
end += "(deb:" + debname + ") " end += "(deb:" + debname + ") "
} }
@ -246,7 +246,7 @@ func (f *Forge) printForgedToTable(repo *gitpb.Repo, sizes []int) {
log.Info(cobol.TerminalChomp(start + " " + end)) log.Info(cobol.TerminalChomp(start + " " + end))
} }
func (psets *Sets) PrintTable() { func (psets *Patchsets) PrintTable() {
if psets == nil { if psets == nil {
return return
} }
@ -257,31 +257,29 @@ func (psets *Sets) PrintTable() {
sizes := []int{12, 12, 3, 3, 40, 80, 2, 2, 2, 2} sizes := []int{12, 12, 3, 3, 40, 80, 2, 2, 2, 2}
log.Info(cobol.TerminalChomp(cobol.StandardTableSize10(sizes, args))) log.Info(cobol.TerminalChomp(cobol.StandardTableSize10(sizes, args)))
/* var countCONTENTS int
var countCONTENTS int var countPARTS int
var countPARTS int for x, pset := range psets.GetPatchsets() {
for x, pset := range psets.GetSets() { log.Info(pset.Uuid, pset.Name, pset.State)
log.Info(pset.Patches.Uuid, pset.Patches.Name, pset.Patches.State) if pset.State == "DONE" {
if pset.State == "DONE" { // old patchset
// old patchset continue
continue }
} cId := log.Sprintf("%d", x)
cId := log.Sprintf("%d", x) countCONTENTS += 1
countCONTENTS += 1 for i, p := range pset.Patches.GetPatches() {
for i, p := range pset.Patches.GetPatches() { var args []string
var args []string partId := log.Sprintf("%d", i)
partId := log.Sprintf("%d", i)
_, fname := filepath.Split(p.GetFilename()) _, fname := filepath.Split(p.GetFilename())
args = []string{p.CommitHash, p.NewHash, cId, partId, fname, p.GetNamespace(), "", "", "", "", ""} args = []string{p.CommitHash, p.NewHash, cId, partId, fname, p.GetNamespace(), "", "", "", "", ""}
start := cobol.StandardTableSize10(sizes, args) start := cobol.StandardTableSize10(sizes, args)
log.Info(cobol.TerminalChomp(start)) log.Info(cobol.TerminalChomp(start))
countPARTS += 1 countPARTS += 1
} }
} }
log.Infof("Total Contents (%d) Parts (%d)\n", countCONTENTS, countPARTS) log.Infof("Total Contents (%d) Parts (%d)\n", countCONTENTS, countPARTS)
*/
} }
func (patches *Patches) PrintTable() { func (patches *Patches) PrintTable() {
@ -295,19 +293,17 @@ func (patches *Patches) PrintTable() {
sizes := []int{12, 12, 3, 3, 40, 80, 2, 2, 2, 2} sizes := []int{12, 12, 3, 3, 40, 80, 2, 2, 2, 2}
log.Info(cobol.TerminalChomp(cobol.StandardTableSize10(sizes, args))) log.Info(cobol.TerminalChomp(cobol.StandardTableSize10(sizes, args)))
/* var countPARTS int
var countPARTS int for x, p := range patches.GetPatches() {
for x, p := range patches.GetPatches() { var args []string
var args []string partId := log.Sprintf("%d", x)
partId := log.Sprintf("%d", x)
_, fname := filepath.Split(p.GetFilename()) _, fname := filepath.Split(p.GetFilename())
args = []string{p.CommitHash, p.NewHash, partId, fname, p.GetNamespace(), "", "", "", "", ""} args = []string{p.CommitHash, p.NewHash, partId, fname, p.GetNamespace(), "", "", "", "", ""}
start := cobol.StandardTableSize10(sizes, args) start := cobol.StandardTableSize10(sizes, args)
log.Info(cobol.TerminalChomp(start)) log.Info(cobol.TerminalChomp(start))
countPARTS += 1 countPARTS += 1
} }
log.Infof("Total Patches (%d)\n", countPARTS) log.Infof("Total Patches (%d)\n", countPARTS)
*/
} }

96
init.go
View File

@ -4,8 +4,6 @@ package forgepb
import ( import (
"os" "os"
"os/user"
"path/filepath"
"go.wit.com/lib/config" "go.wit.com/lib/config"
"go.wit.com/lib/fhelp" "go.wit.com/lib/fhelp"
@ -14,42 +12,27 @@ import (
) )
/* better syntax from gin /* better syntax from gin
Default returns an Engine instance with the Logger and Recovery middleware already attached. Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default(opts ...OptionFunc) *Engine { func Default(opts ...OptionFunc) *Engine {
debugPrintWARNINGDefault()
engine := New() engine := New()
engine.Use(Logger(), Recovery()) engine.Use(Logger(), Recovery())
return engine.With(opts...) return engine.With(opts...)
} }
*/ */
func Init() *Forge { func Init() *Forge {
f := new(Forge)
cfg := new(ForgeConfigs) cfg := new(ForgeConfigs)
err := config.ConfigLoad(cfg, "forge", "forge") err := config.ConfigLoad(cfg, "forge", "forge")
f.Config = cfg
if err != nil { if err != nil {
// fhelp.DumpENV("finit:") log.Info("forge has not been configured yet filename =", cfg.Filename)
f.setenv() log.Info("go install go.wit.com/apps/forge@latest")
if !fhelp.QuestionUser("This is your first time using forge, use these default values?") { os.Exit(-1)
os.Exit(-1)
}
f.Config.InitDefaults()
f.Config.ConfigSave()
f.initFromConfig()
f.Config.DumpENV()
return f
} }
if f.Config.Username == "" { f := initFromConfig(cfg)
usr, _ := user.Current() if f.Config.Mode != ForgeMode_NORMAL {
f.Config.Username = usr.Username
f.Config.ConfigSave()
}
f.initFromConfig()
if f.Config.Mode == ForgeMode_MASTER {
log.Printf("forge.Init() %s len()=%d\n", f.Config.Filename, f.Repos.Len()) log.Printf("forge.Init() %s len()=%d\n", f.Config.Filename, f.Repos.Len())
// fhelp.DumpENV("finit:") fhelp.DumpENV("finit:")
f.Config.DumpENV() f.Config.DumpENV()
} }
return f return f
@ -63,66 +46,51 @@ func InitByAppname(argname string) *Forge {
log.Info("go install go.wit.com/apps/forge@latest") log.Info("go install go.wit.com/apps/forge@latest")
os.Exit(-1) os.Exit(-1)
} }
f := new(Forge) f := initFromConfig(cfg)
f.Config = cfg
f.initFromConfig()
log.Printf("forge.Init() %s len()=%d\n", f.Config.Filename, f.Repos.Len()) log.Printf("forge.Init() %s len()=%d\n", f.Config.Filename, f.Repos.Len())
return f return f
} }
func (f *Forge) initFromConfig() { func initFromConfig(cfg *ForgeConfigs) *Forge {
f := new(Forge)
f.Config = cfg
if f.configENV() { if f.configENV() {
log.Info("ENV changed config. todo: save config here") log.Info("ENV changed config. todo: save config here")
f.Config.ConfigSave() f.Config.ConfigSave()
} }
if f.Config.ReposPB != os.Getenv("FORGE_REPOSPB") {
// if different, use the ENV var
// this probably means that it gets saved as the default in the config
// we probably want that (?)
f.Config.ReposPB = os.Getenv("FORGE_REPOSPB")
}
if _, s := filepath.Split(f.Config.ReposPB); s != "repos.pb" {
fhelp.DumpENV("forge:")
f.Config.DumpENV()
log.Infof("ReposPB invalid filename '%s'\n", f.Config.ReposPB)
os.Exit(-1)
}
f.Repos = gitpb.NewRepos() f.Repos = gitpb.NewRepos()
f.Repos.ConfigLoad(f.Config.ReposPB) f.Repos.ConfigLoad(f.Config.ReposPB)
// init the Patchsets // init the Patchsets
f.Patchsets = NewSets() f.Patchsets = NewPatchsets()
// todo: play with these / determine good values based on user's machine // todo: play with these / determine good values based on user's machine
if f.Config.RillX == 0 { f.rillX = 10
f.Config.RillX = 10 f.rillY = 20
} return f
if f.Config.RillY == 0 {
f.Config.RillY = 20
}
} }
/*
func (f *Forge) InitMachine() {
if f.Config.Username == "" {
usr, _ := user.Current()
f.Config.Username = usr.Username
}
f.hostname, _ = os.Hostname()
// log.Info(hostname, err)
}
*/
func (f *Forge) SetConfigSave(b bool) { func (f *Forge) SetConfigSave(b bool) {
config.SetChanged("forge", b) f.configSave = b
} }
// saves the config if there have been changes // saves the config if there have been changes
func (f *Forge) Exit() { func (f *Forge) Exit() {
// log.Info("forge.configSave =", f.configSave) // log.Info("forge.configSave =", f.configSave)
if f.Config.Mode == ForgeMode_MASTER { if f.configSave {
// fhelp.DumpENV("forge:") f.ConfigSave()
// f.Config.DumpENV()
// todo: tell the user to switch to NORMAL mode
}
f.ConfigSave()
if f.Repos != nil {
if config.HasChanged("repos") {
if err := f.Repos.ConfigSave(f.Config.ReposPB); err != nil {
log.Info("forge.Repos.ConfigSave() error", err)
}
}
} }
// log.Info("forge.Exit() ok") // log.Info("forge.Exit() ok")
os.Exit(0) os.Exit(0)
@ -133,7 +101,6 @@ func (f *Forge) Exit() {
// all initial ENV settings should be stored in the forge struct // all initial ENV settings should be stored in the forge struct
func (f *Forge) setenv() { func (f *Forge) setenv() {
f.once.Do(func() { f.once.Do(func() {
log.Info("doing setenv()")
if err := fhelp.ConfigureENV(); err != nil { if err := fhelp.ConfigureENV(); err != nil {
log.Info("forge ConfigureENV() failed", err) log.Info("forge ConfigureENV() failed", err)
os.Exit(-1) os.Exit(-1)
@ -142,17 +109,16 @@ func (f *Forge) setenv() {
log.Info("forge.Config() was nil") log.Info("forge.Config() was nil")
os.Exit(-1) os.Exit(-1)
} }
// f.configDir = os.Getenv("FORGE_CONFIG")
// f.forgeURL = os.Getenv("FORGE_URL") // f.forgeURL = os.Getenv("FORGE_URL")
f.hostname = os.Getenv("HOSTNAME") f.hostname = os.Getenv("HOSTNAME")
if os.Getenv("FORGE_GOWORK") == "true" { if os.Getenv("FORGE_GOWORK") == "true" {
f.goWork = true f.goWork = true
} }
f.Config.ReposPB = os.Getenv("FORGE_REPOSPB") f.Config.ReposPB = os.Getenv("FORGE_REPOPB")
f.Config.ReposDir = os.Getenv("FORGE_REPOSDIR")
f.Config.PatchDir = os.Getenv("FORGE_PATCHDIR") f.Config.PatchDir = os.Getenv("FORGE_PATCHDIR")
f.Config.ForgeURL = os.Getenv("FORGE_URL") f.Config.ForgeURL = os.Getenv("FORGE_URL")
fhelp.DumpENV("setenv end()")
}) })
} }

View File

@ -1,69 +0,0 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
syntax = "proto3";
package forgepb;
import "google/protobuf/timestamp.proto"; // Import the well-known type for Timestamp
// Forge doesn't need this kind of specificity
// but this is what the patch files contain and how git sees them
// message Blob {
// string hunkLine = 1;
// bytes data = 2;
// }
//
// message File {
// string filename = 1;
// string hashLine = 2;
// repeated Blob Blobs = 3;
// }
//
// message Patch {
// repeated File Files = 1;
// string repoNamespace = 2;
// string gH = 3;
// string gaI = 4;
// string gcI = 5;
// }
// git log -1 --format="%H %aI %cI %an %ae %cn %ce"
message Patch {
string namespace = 1; // the base repo git namespace
bytes data = 2; // the raw data of the whole patch
string gH = 3; // Commit Hash (%H)
string gT = 4; // Tree Hash (%T)
string gP = 5; // Parent Hashes (%P)
string gs = 6; // Subject (%s)
string gaI = 7; // Author Date, ISO 8601 format (%aI)
string gan = 8; // Author Name (%an)
string gae = 9; // Author Email (%ae)
string gcI = 10; // Committer Date, ISO 8601 format (%cI)
string gcn = 11; // Committer Name (%cn)
string gce = 12; // Committer Email (%ce)
string gN = 13; // Commit Notes (%N)
string gGG = 14; // GPG Signature, raw (%GG)
string gGS = 15; // GPG Signer Name (%GS)
string gGK = 16; // GPG Key ID (%GK)
string newHash = 17; // new hash
string state = 18; // the 'state' of the patch
string filename = 19; // `autogenpb:unique` `autogenpb:sort`
string startHash = 20; // the start commit hash
string commitHash = 21; // the git commit hash of this patch `autogenpb:sort` `autogenpb:unique`
string comment = 22; // the git commit message (in patch form)
repeated string Files = 23; // the filenames this patch changes
google.protobuf.Timestamp ctime = 24; // create time of the patch
bool applied = 25; // have you applied this patch?
bool upstream = 26; // has this patch been applied upstream?
string patchId = 27; // patchId `autogenpb:unique`
string treeHash = 28; // final tree Hash
}
// this is a "PATCH: [1/x]" series
message Patches { // `autogenpb:marshal` `autogenpb:gui:Patch` `autogenpb:http`
string uuid = 1; // `autogenpb:uuid:2679065e-c81d-4a00-aca4-03c158a834fb`
string version = 2; // `autogenpb:version:v2.0.0`
repeated Patch patches = 3;
string Error = 4; // when passing these around, if there is an error, store it here
}

View File

@ -1,17 +1,14 @@
package forgepb package forgepb
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"go.wit.com/lib/hostname"
"go.wit.com/lib/protobuf/gitpb" "go.wit.com/lib/protobuf/gitpb"
"go.wit.com/lib/protobuf/httppb" "go.wit.com/lib/protobuf/httppb"
"go.wit.com/log" "go.wit.com/log"
@ -23,30 +20,24 @@ func (p *Patches) HttpPostVerbose(baseURL string, route string) (*Patches, *http
return p.HttpPost(baseURL, route) return p.HttpPost(baseURL, route)
} }
func (p *Set) HttpPostVerbose(baseURL string, route string) (*Set, *httppb.HttpRequest, error) { func (p *Patchsets) HttpPostVerbose(baseURL string, route string) (*Patchsets, *httppb.HttpRequest, error) {
p.PrintTable() p.PrintTable()
return p.HttpPost(baseURL, route) return p.HttpPost(baseURL, route)
} }
func (p *Sets) HttpPostVerbose(baseURL string, route string) (*Sets, *httppb.HttpRequest, error) { func newPatchset(name string) *Patchset {
p.PrintTable() pset := new(Patchset)
return p.HttpPost(baseURL, route)
}
func newPatchset(name string) *Set {
pset := new(Set)
pset.Name = name pset.Name = name
pset.Ctime = timestamppb.New(time.Now()) pset.Ctime = timestamppb.New(time.Now())
pset.Uuid = uuid.New().String() pset.Uuid = uuid.New().String()
pset.Hostname, _ = hostname.Get() pset.Hostname, _ = os.Hostname()
pset.Patches = NewPatches()
return pset return pset
} }
// creates a patchset // creates a patchset
// works from the user branches against the devel branches // works from the user branches against the devel branches
func (f *Forge) MakeDevelPatchSet(name string) (*Set, error) { func (f *Forge) MakeDevelPatchSet(name string) (*Patchset, error) {
pset := newPatchset(name) pset := newPatchset(name)
if os.Getenv("GIT_AUTHOR_NAME") == "" { if os.Getenv("GIT_AUTHOR_NAME") == "" {
return nil, fmt.Errorf("GIT_AUTHOR_NAME not set") return nil, fmt.Errorf("GIT_AUTHOR_NAME not set")
@ -63,7 +54,7 @@ func (f *Forge) MakeDevelPatchSet(name string) (*Set, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// defer os.RemoveAll(dir) // clean up defer os.RemoveAll(dir) // clean up
pset.TmpDir = dir pset.TmpDir = dir
all := f.Repos.SortByFullPath() all := f.Repos.SortByFullPath()
@ -79,10 +70,6 @@ func (f *Forge) MakeDevelPatchSet(name string) (*Set, error) {
continue continue
} }
if repo.ActualGetDevelHash() == repo.ActualGetUserHash() {
continue
}
// make a patchset from user to devel // make a patchset from user to devel
// TODO: verify branches are otherwise exact // TODO: verify branches are otherwise exact
pset.StartBranchName = repo.GetDevelBranchName() pset.StartBranchName = repo.GetDevelBranchName()
@ -96,7 +83,41 @@ func (f *Forge) MakeDevelPatchSet(name string) (*Set, error) {
return pset, nil return pset, nil
} }
func (pset *Set) makePatchSetNew(repo *gitpb.Repo) error { /*
func (f *Forge) MakeMasterPatchSet() (*Patchset, error) {
pset := newPatchset("masterBranchPS")
dir, err := os.MkdirTemp("", "forge")
if err != nil {
return nil, err
}
defer os.RemoveAll(dir) // clean up
pset.TmpDir = dir
all := f.Repos.SortByFullPath()
for all.Scan() {
repo := all.Next()
startb := repo.GetMasterBranchName()
endb := repo.GetUserBranchName()
if startb == "" {
continue
}
if endb == "" {
continue
}
// log.Info("repo", repo.GetGoPath(), startb, "..", endb)
pset.StartBranchName = startb
pset.EndBranchName = endb
err := pset.makePatchSetNew(repo)
if err != nil {
return nil, err
}
}
return pset, nil
}
*/
func (pset *Patchset) makePatchSetNew(repo *gitpb.Repo) error {
startBranch := pset.StartBranchName startBranch := pset.StartBranchName
endBranch := pset.EndBranchName endBranch := pset.EndBranchName
repoDir := filepath.Join(pset.TmpDir, repo.GetGoPath()) repoDir := filepath.Join(pset.TmpDir, repo.GetGoPath())
@ -133,23 +154,22 @@ func (pset *Set) makePatchSetNew(repo *gitpb.Repo) error {
return errors.New(fmt.Sprintf("git returned %d", r.Exit)) return errors.New(fmt.Sprintf("git returned %d", r.Exit))
} }
if len(r.Stdout) == 0 { if len(r.Stdout) == 0 {
log.Infof("No patches in %s (%s,%s)\n", repo.FullPath, repo.ActualGetDevelHash(), repo.ActualGetUserHash())
// git created no files to add // git created no files to add
return nil return nil
} }
err = pset.addPatchFiles(repo, repoDir) err = pset.addPatchFiles(repo)
log.Infof("Added %d patches for %s len=%d\n", len(r.Stdout), repo.FullPath, pset.Patches.Len()) pset.Ctime = timestamppb.New(time.Now())
// pset.PrintTable()
return err return err
} }
// git show <original_commit_hash> | git patch-id
// git cat-file -p <commit_hash> | grep tree
// process each file in pDir/ // process each file in pDir/
func (p *Set) addPatchFiles(repo *gitpb.Repo, fullDir string) error { func (p *Patchset) addPatchFiles(repo *gitpb.Repo) error {
psetDir := repo.GetGoPath()
tmpDir := p.TmpDir
// log.Info("ADD PATCH FILES ADDED DIR", tmpDir)
fullDir := filepath.Join(tmpDir, psetDir)
var baderr error var baderr error
// log.Info("ADD PATCH FILES ADDED DIR", fullDir)
filepath.Walk(fullDir, func(path string, info os.FileInfo, err error) error { filepath.Walk(fullDir, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
// Handle possible errors, like permission issues // Handle possible errors, like permission issues
@ -161,6 +181,10 @@ func (p *Set) addPatchFiles(repo *gitpb.Repo, fullDir string) error {
if info.IsDir() { if info.IsDir() {
return nil return nil
} }
// log.Info("IS THIS A FULL PATH ?", path)
// log.Info("trim this from path ?", fullDir)
// log.Info("trim this from path ?", psetDir)
// log.Info("trim this from path ?", tmpDir)
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
log.Info("addPatchFile() failed", path) log.Info("addPatchFile() failed", path)
@ -170,22 +194,15 @@ func (p *Set) addPatchFiles(repo *gitpb.Repo, fullDir string) error {
patch := new(Patch) patch := new(Patch)
patch.Filename, _ = filepath.Rel(p.TmpDir, path) patch.Filename, _ = filepath.Rel(p.TmpDir, path)
patch.Data = data patch.Data = data
if err := patch.parseData(); err != nil { patch.parseData()
log.Info("parseData() failed", err)
return err
}
if err := findPatchId(repo, patch); err != nil {
log.Info("findPatchId() failed", err)
return err
}
patch.StartHash = repo.ActualDevelHash() patch.StartHash = repo.ActualDevelHash()
patch.NewHash = "na" patch.NewHash = "na"
patch.Namespace = repo.GetGoPath() patch.Namespace = repo.GetGoPath()
if p.Patches == nil { if p.Patches == nil {
log.Info("SHOULD NOT HAVE HAPPENED. p.Patches == nil")
p.Patches = new(Patches) p.Patches = new(Patches)
} }
p.Patches.Append(patch) p.Patches.Append(patch)
p.Patches.Uuid = uuid.New().String()
// log.Info("ADDED PATCH FILE", path) // log.Info("ADDED PATCH FILE", path)
return nil return nil
}) })
@ -195,7 +212,7 @@ func (p *Set) addPatchFiles(repo *gitpb.Repo, fullDir string) error {
// looks at the git format-patch output // looks at the git format-patch output
// saves the commit Hash // saves the commit Hash
// saves the diff lines // saves the diff lines
func (p *Patch) parseData() error { func (p *Patch) parseData() string {
lines := strings.Split(string(p.Data), "\n") lines := strings.Split(string(p.Data), "\n")
for _, line := range lines { for _, line := range lines {
fields := strings.Fields(line) fields := strings.Fields(line)
@ -211,7 +228,7 @@ func (p *Patch) parseData() error {
p.Files = append(p.Files, line) p.Files = append(p.Files, line)
} }
} }
return nil return ""
} }
// just an example of how to walk only directories // just an example of how to walk only directories
@ -236,59 +253,3 @@ func onlyWalkDirs(pDir string) error {
}) })
return baderr return baderr
} }
// func runPipe() error {
func findPatchId(repo *gitpb.Repo, p *Patch) error {
if p.CommitHash == "" {
return log.Errorf("%s commit hash not found", p.Filename)
}
// 1. Create the command to get the diff for the commit.
// "git show" is the perfect tool for this.
cmdShow := exec.Command("git", "show", p.CommitHash)
cmdShow.Dir = repo.GetFullPath()
// 2. Create the command to calculate the patch-id from stdin.
cmdPipeID := exec.Command("git", "patch-id", "--stable")
cmdPipeID.Dir = repo.GetFullPath()
// 3. Connect the output of "git show" to the input of "git patch-id".
// This is the Go equivalent of the shell pipe `|`.
pipe, err := cmdShow.StdoutPipe()
if err != nil {
return fmt.Errorf("failed to create pipe: %w", err)
}
cmdPipeID.Stdin = pipe
// 4. We need a buffer to capture the final output from git patch-id.
var output bytes.Buffer
cmdPipeID.Stdout = &output
// 5. Start the reading command (patch-id) first.
if err := cmdPipeID.Start(); err != nil {
return fmt.Errorf("failed to start git-patch-id: %w", err)
}
// 6. Run the writing command (show). This will block until it's done.
if err := cmdShow.Run(); err != nil {
return fmt.Errorf("failed to run git-show: %w", err)
}
// 7. Wait for the reading command to finish.
if err := cmdPipeID.Wait(); err != nil {
return fmt.Errorf("failed to wait for git-patch-id: %w", err)
}
fields := strings.Fields(output.String())
if len(fields) != 2 {
return fmt.Errorf("git-patch-id produced empty output")
}
if fields[1] != p.CommitHash {
return fmt.Errorf("patchid did not match %s != %v", p.CommitHash, fields)
}
// log.Infof("hash=%s patchid(%v) %s\n", p.CommitHash, fields, p.Filename)
p.PatchId = fields[0]
return nil
}

View File

@ -14,7 +14,7 @@ import (
) )
func (f *Forge) LoadPatchsets() error { func (f *Forge) LoadPatchsets() error {
f.Patchsets = NewSets() f.Patchsets = NewPatchsets()
filename := filepath.Join(f.Config.PatchDir, "all-patches.pb") filename := filepath.Join(f.Config.PatchDir, "all-patches.pb")
@ -39,7 +39,7 @@ func (f *Forge) InitPatchsets() error {
log.Info("LoadPatchsets() failed", err) log.Info("LoadPatchsets() failed", err)
} }
// TODO: check if Unmarshal failed here // TODO: check if Unmarshal failed here
f.Patchsets = NewSets() f.Patchsets = NewPatchsets()
f.findAutoPatchset() // adds the default values f.findAutoPatchset() // adds the default values
return f.SavePatchsets() return f.SavePatchsets()
} }
@ -54,7 +54,7 @@ func (f *Forge) SavePatchsets() error {
} }
defer regfile.Close() defer regfile.Close()
newpb := proto.Clone(f.Patchsets).(*Sets) newpb := proto.Clone(f.Patchsets).(*Patchsets)
if newpb == nil { if newpb == nil {
for pset := range f.Patchsets.IterAll() { for pset := range f.Patchsets.IterAll() {
pset.ShowPatchsets() pset.ShowPatchsets()
@ -79,7 +79,7 @@ func cleanSubject(line string) string {
return strings.TrimSpace(cleaned) return strings.TrimSpace(cleaned)
} }
func (pb *Set) ShowPatchsets() error { func (pb *Patchset) ShowPatchsets() error {
author := "Author: " + pb.GitAuthorName author := "Author: " + pb.GitAuthorName
author += " <" + pb.GitAuthorEmail + ">" author += " <" + pb.GitAuthorEmail + ">"
log.Printf("%-16s %s %s %s\n", string(pb.Uuid)[0:8], pb.Name, pb.Comment, author) log.Printf("%-16s %s %s %s\n", string(pb.Uuid)[0:8], pb.Name, pb.Comment, author)
@ -109,7 +109,7 @@ func (f *Forge) AddPatch(patch *Patch) bool {
} }
// adds a patchset or just the patches // adds a patchset or just the patches
func (f *Forge) AddPatchset(pb *Set) bool { func (f *Forge) AddPatchset(pb *Patchset) bool {
var changed bool var changed bool
// if the name of the patchset is "forge auto commit" // if the name of the patchset is "forge auto commit"
// then just add all the patches // then just add all the patches
@ -146,18 +146,23 @@ func (f *Forge) AddPatchset(pb *Set) bool {
} }
} }
f.Patchsets.Append(pb) // Clone() this protobuf into me.forge.Patchsets
var newpb *Patchset
newpb = proto.Clone(pb).(*Patchset)
if newpb != nil {
f.Patchsets.Patchsets = append(f.Patchsets.Patchsets, newpb)
}
return true return true
} }
func (f *Forge) findAutoPatchset() *Set { func (f *Forge) findAutoPatchset() *Patchset {
for pset := range f.Patchsets.IterAll() { for pset := range f.Patchsets.IterAll() {
if pset.Name == "forge auto commit" { if pset.Name == "forge auto commit" {
return pset return pset
} }
} }
var fauto *Set var fauto *Patchset
log.Warn("findAutoPatchset() had to create 'forge auto commit'") log.Warn("findAutoPatchset() had to create 'forge auto commit'")
if fauto == nil { if fauto == nil {
fauto = makeDefaultPatchset() fauto = makeDefaultPatchset()
@ -166,8 +171,8 @@ func (f *Forge) findAutoPatchset() *Set {
return fauto return fauto
} }
func makeDefaultPatchset() *Set { func makeDefaultPatchset() *Patchset {
fauto := new(Set) fauto := new(Patchset)
fauto.Name = "forge auto commit" fauto.Name = "forge auto commit"
fauto.Patches = NewPatches() fauto.Patches = NewPatches()
fauto.Uuid = uuid.New().String() fauto.Uuid = uuid.New().String()

View File

@ -1,11 +1,9 @@
package forgepb package forgepb
/*
// makes a new patches protobuf. These are all the patches on your machine. // makes a new patches protobuf. These are all the patches on your machine.
func NewPatches() *Patchs { func NewPatches() *Patches {
x := new(Patchs) x := new(Patches)
x.Uuid = "2679065e-c81d-4a00-aca4-03c158a834fb" x.Uuid = "2679065e-c81d-4a00-aca4-03c158a834fb"
x.Version = "v2.0.0 go.wit.com/lib/protobuf/forgepb" x.Version = "v2.0.0 go.wit.com/lib/protobuf/forgepb"
return x return x
} }
*/

89
patchset.proto Normal file
View File

@ -0,0 +1,89 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
syntax = "proto3";
package forgepb;
import "google/protobuf/timestamp.proto"; // Import the well-known type for Timestamp
// Forge doesn't need this kind of specificity
// but this is what the patch files contain and how git sees them
// message Blob {
// string hunkLine = 1;
// bytes data = 2;
// }
//
// message File {
// string filename = 1;
// string hashLine = 2;
// repeated Blob Blobs = 3;
// }
//
// message Patch {
// repeated File Files = 1;
// string repoNamespace = 2;
// string gH = 3;
// string gaI = 4;
// string gcI = 5;
// }
// git log -1 --format="%H %aI %cI %an %ae %cn %ce"
message Patch {
string namespace = 1; // the base repo git namespace
bytes data = 2; // the raw data of the whole patch
string gH = 3; // Commit Hash (%H)
string gT = 4; // Tree Hash (%T)
string gP = 5; // Parent Hashes (%P)
string gs = 6; // Subject (%s)
string gaI = 7; // Author Date, ISO 8601 format (%aI)
string gan = 8; // Author Name (%an)
string gae = 9; // Author Email (%ae)
string gcI = 10; // Committer Date, ISO 8601 format (%cI)
string gcn = 11; // Committer Name (%cn)
string gce = 12; // Committer Email (%ce)
string gN = 13; // Commit Notes (%N)
string gGG = 14; // GPG Signature, raw (%GG)
string gGS = 15; // GPG Signer Name (%GS)
string gGK = 16; // GPG Key ID (%GK)
string newHash = 17; // new hash
string state = 18; // the 'state' of the patch
string filename = 19; // `autogenpb:unique` `autogenpb:sort`
string startHash = 20; // the start commit hash
string commitHash = 21; // the git commit hash of this patch `autogenpb:sort` `autogenpb:unique`
string comment = 22; // the git commit message (in patch form)
repeated string Files = 23; // the filenames this patch changes
google.protobuf.Timestamp ctime = 24; // create time of the patch
bool applied = 25; // have you applied this patch?
bool upstream = 26; // has this patch been applied upstream?
}
// this is a "PATCH: [1/x]" series
message Patches { // `autogenpb:marshal` `autogenpb:gui:Patch` `autogenpb:http`
string uuid = 1; // `autogenpb:uuid:2679065e-c81d-4a00-aca4-03c158a834fb`
string version = 2; // `autogenpb:version:v2.0.0`
repeated Patch Patches = 3;
string Error = 5; // when passing these around, if there is an error, store it here
}
message Patchset { // `autogenpb:marshal`
Patches patches = 1; //
string name = 2; // `autogenpb:sort`
string comment = 3; //
string gitAuthorName = 4; // `autogenpb:sort`
string gitAuthorEmail = 5; //
google.protobuf.Timestamp ctime = 6; // create time of the patchset
string tmpDir = 7; // temp dir
string startBranchName = 8; //
string endBranchName = 9; //
string startBranchHash = 10; //
string endBranchHash = 11; //
string state = 12; // the state of the patch
string uuid = 13; // `autogenpb:sort` `autogenpb:unique`
string hostname = 14; //
}
message Patchsets { // `autogenpb:marshal` `autogenpb:gui` `autogenpb:nomutex` `autogenpb:http`
string uuid = 1; // `autogenpb:uuid:be926ad9-f07f-484c-adf2-d96eeabf3079`
string version = 2; // `autogenpb:version:v0.0.45`
repeated Patchset Patchsets = 3;
}

View File

@ -9,7 +9,6 @@ import (
"regexp" "regexp"
"strings" "strings"
"go.wit.com/lib/config"
"go.wit.com/lib/protobuf/gitpb" "go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log" "go.wit.com/log"
) )
@ -36,7 +35,7 @@ func (f *Forge) NewGoRepo(gopath string, url string) (*gitpb.Repo, error) {
if f.Config.IsReadOnly(repo.GetGoPath()) { if f.Config.IsReadOnly(repo.GetGoPath()) {
repo.ReadOnly = true repo.ReadOnly = true
} }
repo.ReloadCheck() repo.Reload()
return repo, nil return repo, nil
} }
@ -53,13 +52,13 @@ func (f *Forge) AddNamespaceDir(ns string, fullpath string) (*gitpb.Repo, error)
} }
f.VerifyBranchNames(repo) f.VerifyBranchNames(repo)
repo.ReloadCheck() repo.Reload()
// set the readonly flag based on the users' forge config // set the readonly flag based on the users' forge config
if f.Config.IsReadOnly(repo.GetGoPath()) { if f.Config.IsReadOnly(repo.GetGoPath()) {
repo.ReadOnly = true repo.ReadOnly = true
} }
config.SetChanged("repos", true) f.configSave = true
return repo, nil return repo, nil
} }
@ -164,9 +163,6 @@ func (f *Forge) VerifyBranchNames(repo *gitpb.Repo) {
if repo.GetUserBranchName() == "" { if repo.GetUserBranchName() == "" {
uname := f.configUserBranchName(repo) uname := f.configUserBranchName(repo)
if uname == "" {
log.Info("configUserBranchName() ERROR: failed with blank")
}
if repo.IsBranch(uname) { if repo.IsBranch(uname) {
repo.SetUserBranchName(uname) repo.SetUserBranchName(uname)
} else { } else {
@ -174,9 +170,6 @@ func (f *Forge) VerifyBranchNames(repo *gitpb.Repo) {
repo.SetUserBranchName(uname) repo.SetUserBranchName(uname)
} }
} }
if repo.GetUserBranchName() == "" || repo.GetDevelBranchName() == "" {
log.Infof("VerifyBranchNames() failed m=%s d=%s u=%s\n", repo.GetMasterBranchName(), repo.GetDevelBranchName(), repo.GetUserBranchName())
}
} }
// what name should be used for the user branch? // what name should be used for the user branch?
@ -192,9 +185,6 @@ func (f *Forge) configUserBranchName(repo *gitpb.Repo) string {
if uname != "" { if uname != "" {
return uname return uname
} }
if f.Config.Username == "" {
// something is wrong!
}
// use the os.Username // use the os.Username
uname = f.Config.Username uname = f.Config.Username

33
rill.go
View File

@ -5,7 +5,6 @@ import (
"time" "time"
"github.com/destel/rill" "github.com/destel/rill"
"go.wit.com/lib/config"
"go.wit.com/lib/protobuf/gitpb" "go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log" "go.wit.com/log"
) )
@ -34,6 +33,7 @@ func (f *Forge) rillUpdate(pool1 int, pool2 int) (int, error) {
// Concurrency = 10 // Concurrency = 10
err := rill.ForEach(rills, pool2, func(repo *gitpb.Repo) error { err := rill.ForEach(rills, pool2, func(repo *gitpb.Repo) error {
counter += 1 counter += 1
// log.Info("rill.ForEach() gopath=", repo.GetGoPath())
return f.updateRepo(repo) return f.updateRepo(repo)
}) })
@ -42,14 +42,16 @@ func (f *Forge) rillUpdate(pool1 int, pool2 int) (int, error) {
func (f *Forge) updateRepo(repo *gitpb.Repo) error { func (f *Forge) updateRepo(repo *gitpb.Repo) error {
if !repo.IsValidDir() { if !repo.IsValidDir() {
log.Printf("%10s %-50s gopath=%s\n", "git dir is missing\n", repo.FullPath, repo.GetNamespace()) log.Printf("%10s %-50s gopath=%s\n", "git dir is missing\n", repo.FullPath, repo.GetGoPath())
f.Repos.DeleteByFullPath(repo.FullPath) f.Repos.DeleteByFullPath(repo.FullPath)
f.configSave = true
return nil return nil
} }
if repo.HasChanged() { if repo.DidRepoChange() {
f.configSave = true
// log.Info("repo changed ", repo.FullPath, repo.StateChange) // log.Info("repo changed ", repo.FullPath, repo.StateChange)
if err := repo.ReloadCheck(); err != nil { if err := repo.Reload(); err != nil {
return err return err
} }
} else { } else {
@ -60,6 +62,7 @@ func (f *Forge) updateRepo(repo *gitpb.Repo) error {
} else { } else {
log.Info("readonly flag on repo is wrong", repo.GetGoPath()) log.Info("readonly flag on repo is wrong", repo.GetGoPath())
repo.ReadOnly = true repo.ReadOnly = true
f.configSave = true
} }
} }
return nil return nil
@ -77,9 +80,6 @@ func (f *Forge) RillReload() int {
for repo := range f.Repos.IterAll() { for repo := range f.Repos.IterAll() {
if !repo.IsValidDir() { if !repo.IsValidDir() {
log.Printf("%s %-50s\n", "got an invalid repo in forgepb.RillReload()", repo.GetFullPath()) log.Printf("%s %-50s\n", "got an invalid repo in forgepb.RillReload()", repo.GetFullPath())
f.Repos.Delete(repo)
log.Info("reposSave = true")
config.SetChanged("repos", true)
continue continue
} }
all = append(all, repo) all = append(all, repo)
@ -95,10 +95,11 @@ func (f *Forge) RillReload() int {
}) })
rill.ForEach(dirs, RillY, func(repo *gitpb.Repo) error { rill.ForEach(dirs, RillY, func(repo *gitpb.Repo) error {
if !repo.HasChanged() { if !repo.DidRepoChange() {
return nil return nil
} }
repo.ReloadCheck() f.configSave = true
repo.Reload()
counter += 1 counter += 1
return nil return nil
}) })
@ -115,9 +116,9 @@ func (f *Forge) RillFuncError(rillf func(*gitpb.Repo) error) map[string]*RillSta
} }
func (f *Forge) ConfigRill(rillX int, rillY int) { func (f *Forge) ConfigRill(rillX int, rillY int) {
f.Config.RillX = int32(rillX) f.rillX = rillX
f.Config.RillY = int32(rillY) f.rillY = rillY
log.Infof("Setting rill values to %d,%d\n", f.Config.RillX, f.Config.RillY) log.Infof("Setting rill values to %d,%d\n", f.rillX, f.rillY)
} }
type RillStats struct { type RillStats struct {
@ -141,14 +142,10 @@ func (f *Forge) RillRepos(rillf func(*gitpb.Repo) error) map[string]*RillStats {
for repo := range f.Repos.IterAll() { for repo := range f.Repos.IterAll() {
if !repo.IsValidDir() { if !repo.IsValidDir() {
log.Printf("got an invalid repo in forgepb.RillRepos() %-50s\n", repo.GetFullPath()) log.Printf("got an invalid repo in forgepb.RillRepos() %-50s\n", repo.GetFullPath())
f.Repos.Delete(repo)
log.Info("reposSave = true")
config.SetChanged("repos", true)
continue continue
} }
all = append(all, repo) all = append(all, repo)
} }
// log.Info("Rill Repos len =", len(all))
// Convert a slice of user IDs into a channel // Convert a slice of user IDs into a channel
ids := rill.FromSlice(all, nil) ids := rill.FromSlice(all, nil)
@ -157,11 +154,11 @@ func (f *Forge) RillRepos(rillf func(*gitpb.Repo) error) map[string]*RillStats {
// Read users from the API. // Read users from the API.
// Concurrency = 20 // Concurrency = 20
dirs := rill.Map(ids, int(f.Config.RillX), func(id *gitpb.Repo) (*gitpb.Repo, error) { dirs := rill.Map(ids, f.rillX, func(id *gitpb.Repo) (*gitpb.Repo, error) {
return id, nil return id, nil
}) })
rill.ForEach(dirs, int(f.Config.RillY), func(repo *gitpb.Repo) error { rill.ForEach(dirs, f.rillY, func(repo *gitpb.Repo) error {
// todo: make this a goroutine to show stats to the user // todo: make this a goroutine to show stats to the user
rillMu.Lock() rillMu.Lock()
counter += 1 counter += 1

View File

@ -1,170 +0,0 @@
package forgepb
import (
"fmt"
"os"
"path/filepath"
"github.com/destel/rill"
"go.wit.com/lib/config"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
func reloadCheck(repo *gitpb.Repo) error {
if err := repo.ReloadCheck(); err != nil {
log.Infof("%s reload() says %v\n", repo.FullPath, err)
return err
}
return nil
}
func (f *Forge) TestScan() error {
f.Repos = gitpb.NewRepos()
dirs, err := gitDirectoriesNew(f.Config.ReposDir)
if err != nil {
return err
}
for i, fullpath := range dirs {
repo, err := gitpb.NewRepo(fullpath)
if err != nil {
log.Info("ReAdd() error", fullpath, err)
}
log.Info(i, "worked", repo.FullPath)
repo = f.Repos.Append(repo)
f.VerifyBranchNames(repo)
if f.Config.IsReadOnly(repo.GetGoPath()) {
repo.ReadOnly = true
}
repo.ReloadCheck()
if i > 5 {
break
}
}
return nil
}
func (f *Forge) checkNamespace(fullpath string) (*gitpb.Repo, error) {
if repo := f.Repos.FindByFullPath(fullpath); repo != nil {
return nil, nil
}
repo, err := gitpb.NewRepo(fullpath)
if err != nil {
log.Info(fullpath, err)
return nil, err
}
return repo, err
}
func (f *Forge) ScanRepoDir() error {
dirs, err := gitDirectoriesNew(f.Config.ReposDir)
if err != nil {
return err
}
stats := f.RillRepos(reloadCheck)
for _, stat := range stats {
if stat.Err == nil {
continue
}
config.SetChanged("repos", true)
}
newcount, err := f.rillScanDirsNew(dirs)
if err != nil {
log.Info("go src dir problem. exit for now?", err)
return err
}
if newcount != 0 {
log.Info("forge go src scan found", newcount, "repos")
config.SetChanged("repos", true)
}
return err
}
// rill is awesome. long live rill
// attempt scan with rill
func (f *Forge) rillScanDirsNew(fullpaths []string) (int, error) {
// Convert a slice of user IDs into a channel
ids := rill.FromSlice(fullpaths, nil)
// Read users from the API. // Concurrency = 20
dirs := rill.Map(ids, int(f.Config.RillX), func(id string) (*gitpb.Repo, error) {
return f.checkNamespace(id)
})
var counter int
// Activate users. // Concurrency = 10
err := rill.ForEach(dirs, int(f.Config.RillY), func(repo *gitpb.Repo) error {
if repo == nil {
return nil
}
repo = f.Repos.Append(repo)
f.VerifyBranchNames(repo)
if f.Config.IsReadOnly(repo.GetGoPath()) {
repo.ReadOnly = true
}
repo.ReloadCheck()
counter += 1
return nil
})
return counter, err
}
// doesn't enter the directory any further when it finds a .git/
// not stupid like my old version
func gitDirectoriesNew(srcDir string) ([]string, error) {
var all []string
var trip bool
err := filepath.WalkDir(srcDir, func(path string, d os.DirEntry, err error) error {
if err != nil {
// Handle possible errors, like permission issues
fmt.Fprintf(os.Stderr, "error accessing path %q: %v\n", path, err)
return err
}
if d.IsDir() {
// log.Info("path is dir", path)
} else {
_, fname := filepath.Split(path)
switch fname {
case "repos.pb":
case "go.work":
case "go.work.last":
case "go.work.sum":
default:
// todo: figure out a way to do padding for init()
if trip == false {
log.Info("WARNING:")
}
log.Info("WARNING: you have an untracked file outside of any .git repository:", path)
trip = true
}
return nil
}
gitdir := filepath.Join(path, ".git")
_, err2 := os.Stat(gitdir)
if !os.IsNotExist(err2) {
all = append(all, path)
return filepath.SkipDir
}
return nil
})
//
// probably always leave this here forever
// this check, along with CheckDirty() makes sure you can safely delete ~/go/src or the go.work directory
// because everything is either checked in or deleted. An important thing to know!
if trip {
log.Info("WARNING:")
log.Info("WARNING: there isn't a way to disable this warning yet")
log.Info("WARNING: probably this is a good thing however. you don't want to leave files outside of git repos here")
log.Info("WARNING: so this warning should probably stay")
log.Info("WARNING:")
log.Info("WARNING: this also might mean you put these files here because you are actively working on them")
log.Info("WARNING: and you don't want to forget about them")
log.Info("WARNING:")
}
return all, err
}

View File

@ -1,32 +0,0 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
syntax = "proto3";
package forgepb;
import "google/protobuf/timestamp.proto"; // Import the well-known type for Timestamp
import "patch.proto"; // Import the well-known type for Timestamp
message Set { // `autogenpb:http`
Patches patches = 1;
string uuid = 2;
google.protobuf.Timestamp ctime = 3; // when the patches were submitted
string submitter = 4; // who submitted these // deprecate this
string name = 5; // "fixes for foo"
string gitAuthorName = 6;
string gitAuthorEmail = 7;
string hostname = 8;
string tmpDir = 9; // temp dir for 'git am' deprecate this
string startBranchName = 10; // deprecate this
string endBranchName = 11; // deprecate this
string startBranchHash = 12; // deprecate this
string endBranchHash = 13; // deprecate this
string comment = 14; // deprecate this
string state = 15; // deprecate this
}
message Sets { // `autogenpb:marshal` `autogenpb:gui` `autogenpb:nomutex` `autogenpb:http`
string uuid = 1; // `autogenpb:uuid:be926ad9-f07f-484c-adf2-d96eeabf3079`
string version = 2; // `autogenpb:version:v0.0.45`
repeated Set sets = 3;
}

View File

@ -1,89 +0,0 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
"time"
"go.wit.com/lib/cobol"
"go.wit.com/log"
)
func (pset *Set) PrintTable() {
if pset == nil || pset.Patches == nil {
return
}
log.DaemonMode(true) // don't timestamp lines
tablePB := pset.Patches.makeStandardTable()
tablePB.MakeTable()
tablePB.PrintTable()
}
func (mt *PatchesTable) PrintTable() {
// log.Info("ShowTable() SENDING TO GUI")
mt.MakeTable()
cobol.PrintTable(mt.pb)
}
func (pb *Patches) makeStandardTable() *PatchesTable {
t := pb.NewTable("tagList")
t.NewUuid()
col := t.AddNamespace()
col.Width = 28
col = t.AddCommitHash()
col.Width = 8
col = t.AddPatchId()
col.Width = 8
col = t.AddNewHash()
col.Width = 8
col = t.AddTimeFunc("ctime", func(p *Patch) time.Time {
// todo
return p.Ctime.AsTime()
})
col.Width = 4
/*
col = t.AddTimeFunc("age", func(repo *gitpb.GitTag) time.Time {
// todo
return time.Now()
})
col.Width = 4
*/
// col = t.AddStringFunc("filename", func(p *Patch) string {
// _, base := filepath.Split(p.Filename)
// return base
// })
// col.Width = 24
col = t.AddComment()
col.Width = 80
return t
}
/*
func (pb *Sets) makeStandardTablePB() *SetsTable {
t := pb.NewTable("tagList")
t.NewUuid()
col := t.AddUuid()
col.Width = 12
col = t.AddTimeFunc("ctime", func(pset *Set) time.Time {
// todo
return pset.Ctime.AsTime()
})
col.Width = 4
col = t.AddComment()
col.Width = -1
return t
}
*/

View File

@ -9,12 +9,19 @@ import (
// maybe an interface someday? // maybe an interface someday?
type Forge struct { type Forge struct {
// one-time initialized data // one-time initialized data
once sync.Once once sync.Once
Config *ForgeConfigs // config repos for readonly, private, etc Config *ForgeConfigs // config repos for readonly, private, etc
Repos *gitpb.Repos // the repo protobufs Repos *gitpb.Repos // the repo protobufs
Patchsets *Sets // patches that are in progress Patchsets *Patchsets // patches that are in progress
hostname string // your hostname configSave bool // if you need to save the config because things changed
goWork bool // means the user is currently using a go.work file hostname string // your hostname
rillX int // used for Rill()
rillY int // used for Rill()
goWork bool // means the user is currently using a go.work file
// goSrc string // the path to go/src
// forgeURL string // URL to use to forge.wit.com
// configDir string // normally ~/.config/forge
// patchDir string // where patches are stored
} }
func (f *Forge) IsGoWork() bool { func (f *Forge) IsGoWork() bool {