Compare commits

...

141 Commits

Author SHA1 Message Date
Jeff Carr 7e1804f6e3 needed FindByUuid() 2025-03-12 09:21:29 -05:00
Jeff Carr 8ceec9210d track applied or upstream for patches 2025-03-11 12:00:38 -05:00
Jeff Carr cf6db578a4 store ctime 2025-03-10 23:26:57 -05:00
Jeff Carr da485dcc3f generate a patchset table gui 2025-03-10 13:52:46 -05:00
Jeff Carr 5377a89d2c autogenpb gui code for patchsets 2025-03-10 09:12:08 -05:00
Jeff Carr f7d6dfa6a7 clearer text 2025-03-03 23:49:51 -06:00
Jeff Carr 281ffbc75b minor cleanups 2025-03-02 07:49:45 -06:00
Jeff Carr 4193f41847 move to better patchset protobuf 2025-03-02 04:13:27 -06:00
Jeff Carr 9d5bae8a14 add a 'forge mode' concept 2025-03-02 03:03:45 -06:00
Jeff Carr f540aab434 remove zoopb from forge 2025-02-22 06:53:42 -06:00
Jeff Carr a170250cb4 wrong logic 2025-02-21 17:17:05 -06:00
Jeff Carr 2fc67512c8 changes from gitpb 2025-02-21 09:34:24 -06:00
Jeff Carr eaadfa21d3 add a specific http POST option 2025-02-16 12:05:35 -06:00
Jeff Carr 22ebf174c8 show the lasttag 2025-02-15 18:18:30 -06:00
Jeff Carr 9a87c93bad quiet output 2025-02-15 12:31:00 -06:00
Jeff Carr b7b18626d8 more on Machine.Init() 2025-02-15 12:21:36 -06:00
Jeff Carr 7900b1416e add IdentifyProtobuf() 2025-02-15 05:35:01 -06:00
Jeff Carr 5c84b9ab66 needed to bypass go-deb problems 2025-02-14 20:43:47 -06:00
Jeff Carr c09e292a66 something to do abitrary dirs 2025-02-14 18:39:36 -06:00
Jeff Carr 3278f6400e add GetHome() 2025-02-10 23:30:15 -06:00
Jeff Carr 83ad663fc0 must send an error on nil 2025-02-09 15:05:23 -06:00
Jeff Carr f70f54615f fix nil 2025-02-09 14:48:47 -06:00
Jeff Carr 018772dbfb more things to check if release is needed 2025-02-08 06:32:59 -06:00
Jeff Carr 9baa477990 add a date and a uuid to the patchset 2025-02-02 15:07:23 -06:00
Your Name ab01c2cd60 auto formatting using autogenpb 2024-01-01 12:00:00 -06:00
Jeff Carr c89f101fb2 namechange 2025-02-01 06:58:27 -06:00
Jeff Carr 23d7ad1581 various updates 2025-02-01 06:57:57 -06:00
Jeff Carr ec4acd425c save the path to the config dir 2025-01-31 13:47:45 -06:00
Jeff Carr 0614066fdb add 'state' 2025-01-30 18:00:30 -06:00
Your Name b60279b19a a better protobuf file to switch to later 2024-01-01 12:00:00 -06:00
Jeff Carr 95fcacfde0 add a MakePatchset() 2025-01-30 07:42:25 -06:00
Jeff Carr b6a71a515f url via ENV 2025-01-30 06:21:15 -06:00
Jeff Carr 47ee3f1493 rm old code 2025-01-30 04:44:15 -06:00
Jeff Carr 329710f9e7 rm old code 2025-01-30 02:24:34 -06:00
Jeff Carr f7b5e1a83e try a way to track the times so they can be throttled 2025-01-30 01:15:15 -06:00
Jeff Carr 7c37e3841a move this here from go-mod-sum 2025-01-29 21:24:26 -06:00
Jeff Carr f146bf4ef0 quiet lots of debugging output 2025-01-29 20:00:49 -06:00
Jeff Carr d9d90e9e12 ignore stuff 2025-01-29 12:25:06 -06:00
Jeff Carr 393b91c415 store the new hash 2025-01-29 12:24:42 -06:00
Jeff Carr b412e50df0 save git commit msg and repo namespace 2025-01-29 12:24:42 -06:00
Jeff Carr aa06450042 minor 2025-01-29 12:24:42 -06:00
Jeff Carr 121e9f08da print how many patches there were 2025-01-29 12:24:42 -06:00
Jeff Carr df19b5b8f8 cleanup debugging output 2025-01-29 12:24:42 -06:00
Jeff Carr 0efc3c67ca turn of the verbose logging 2025-01-29 12:24:42 -06:00
Jeff Carr 667257595d add submit patchset 2025-01-29 12:24:42 -06:00
Jeff Carr 76a0347fdf mv 2025-01-29 12:24:42 -06:00
Jeff Carr 66738e4300 debugging releaser 2025-01-20 07:59:14 -06:00
Jeff Carr 3e4b1ddc83 cleanup protofile in total violaton of protobuf rules 2025-01-20 05:09:46 -06:00
Jeff Carr 9ec7b4394f make HttpPost() global 2025-01-20 03:31:19 -06:00
Jeff Carr 1191b9b65d moved GetPatchsets() here from forge 2025-01-20 03:23:33 -06:00
Jeff Carr 52b8a4e312 need better handling here 2025-01-20 01:40:32 -06:00
Jeff Carr b770759167 rm old code 2025-01-19 16:07:50 -06:00
Jeff Carr 0898c24f45 trying to debug the logic since it's failing 2025-01-19 11:52:20 -06:00
Jeff Carr 58c64cd53b might show branch age in table finally 2025-01-19 10:48:50 -06:00
Jeff Carr f29f25b9b7 hide uerr in table 2025-01-19 09:25:39 -06:00
Jeff Carr 4328692039 redo with rill 2025-01-19 04:32:20 -06:00
Jeff Carr 244bf612f9 load repos output is cleaner 2025-01-19 03:18:47 -06:00
Jeff Carr f4ac491490 debugging, but runs ok 2025-01-19 02:37:31 -06:00
Jeff Carr bdf9d97cf9 reorder target to the end 2025-01-19 00:36:32 -06:00
Jeff Carr a822e1e4f0 switch some more things to use rill 2025-01-18 23:26:19 -06:00
Jeff Carr cee7e25f3d rill is awesome. thank Viktor 2025-01-18 11:11:16 -06:00
Jeff Carr 9b8cb52b7b show deb package name if different than the standard 2025-01-18 10:34:38 -06:00
Jeff Carr 538531f503 this is a dumb idea. never do this. fix the repo. 2025-01-18 04:48:34 -06:00
Jeff Carr e8f29e593d more human readable but standard version timestamp 2025-01-18 03:14:41 -06:00
Jeff Carr b8d0864c37 show abnormal branch 2025-01-17 13:20:40 -06:00
Jeff Carr 156af56859 init patches 2025-01-17 10:59:28 -06:00
Jeff Carr 8d275ff054 less aoutput 2025-01-17 05:30:42 -06:00
Jeff Carr 0f232fe342 more accurate repo Age() 2025-01-17 04:47:47 -06:00
Jeff Carr 9e81be86da set the uuid and version 2025-01-17 02:51:34 -06:00
Jeff Carr 2398e30048 add xterm settings to the config 2025-01-16 10:26:19 -06:00
Jeff Carr 49a06843e9 debugging a hung mutex lock 2025-01-13 08:12:39 -06:00
Jeff Carr 9b9c51d964 maybe marshal() failed do to mutex entries? notsure 2025-01-13 04:14:30 -06:00
Jeff Carr 23887a155e fixes for new autogenpb 2025-01-12 20:05:46 -06:00
Jeff Carr 0600f54488 remove HANDMADE iterator after fixing autogenpb 2025-01-12 06:36:11 -06:00
Jeff Carr 0ea93faef2 can't use go:generate. boo. it requires go.mod and go.sum 2025-01-11 06:08:05 -06:00
Jeff Carr 48b19f1e70 new protobuf file 2025-01-11 05:55:42 -06:00
Jeff Carr 1e38cacfa7 no going back now 2025-01-10 04:39:33 -06:00
Jeff Carr 40c340c626 requires sort 2025-01-09 21:15:52 -06:00
Jeff Carr f9dd82cdcc work on new protobuf for patchsets 2025-01-08 10:11:17 -06:00
Jeff Carr 4f84a4e584 rename vars 2025-01-08 04:08:12 -06:00
Jeff Carr 8aff3f13b2 fix the dumb names 2025-01-08 03:12:53 -06:00
Jeff Carr 26cf5055a2 save the last Reload() time 2025-01-08 02:39:08 -06:00
Jeff Carr 14e5bf5fbd add a button for the table 2025-01-08 01:16:38 -06:00
Jeff Carr e0c0d3d9e6 haha. it's like COBOL in GO 2025-01-08 00:52:32 -06:00
Jeff Carr bd3e924e2b finally pull the origin repo name from the right place 2025-01-07 22:29:17 -06:00
Jeff Carr e6e70ccaa5 add a way to force git checkout anyway 2025-01-07 21:22:54 -06:00
Jeff Carr 67ae8d8773 use go generate. also fix verbose Build() flags 2025-01-07 17:44:23 -06:00
Jeff Carr e333ca726b also show debname 2025-01-06 15:32:18 -06:00
Jeff Carr 6cbd7e67af attempt custom branch names 2025-01-05 19:45:29 -06:00
Jeff Carr 7e5db53e9d slim new user defaults 2025-01-05 18:41:46 -06:00
Jeff Carr b0662fb61a add commit hash and diff file list to the protobuf 2025-01-05 04:54:31 -06:00
Jeff Carr ac57825c10 start adding something to check the hashes 2025-01-05 03:21:47 -06:00
Jeff Carr 338018376b ConfigLoad() was totally broken 2025-01-05 01:20:12 -06:00
Jeff Carr 275a7db0e0 oops 2024-12-28 19:36:17 -06:00
Jeff Carr f2ba4cab9c wrong path. funcknuts 2024-12-28 03:01:00 -06:00
Jeff Carr c7066e1766 filepath.HasPrefix() doesn't work. why? Microsoft sucks
for those of you that are new here and care about
	writing free software. this is something to remind you
	to NEVER EVER EVER trust Microsoft to do anything but
	try to make things worse. The are opposed and will always
	be opposed to making Linux work. The suck and are horrible
	villians in this story. Never trust them and do not
	support them. Always use the GPL vs BSD license for this
	reason also. They will steal anything BSD and use it
	in their proprietary products and not give anything back
	to anyone. They are complete jerks.
2024-12-27 23:03:00 -06:00
Jeff Carr f4837c807c user branch name settings 2024-12-27 14:37:20 -06:00
Jeff Carr e9776796dd todo: make go.* files git metadata 2024-12-18 20:09:03 -06:00
Jeff Carr 934daa5a3b maybe works somewhat 2024-12-18 19:36:05 -06:00
Jeff Carr a97b66e8f2 rm debugging 2024-12-18 18:51:39 -06:00
Jeff Carr 4582a76081 finally re-organize and rename Clone to GoClone() 2024-12-18 17:59:20 -06:00
Jeff Carr 24c96ccaa7 wasn't reloading versions 2024-12-17 23:01:02 -06:00
Jeff Carr 272f965eec process on a *gitpb.Repos 2024-12-17 21:58:31 -06:00
Jeff Carr c4f9430e46 keep trying to fix init() and update() 2024-12-17 21:14:39 -06:00
Jeff Carr 7cdb2bf6a0 rill repo.Update() 2024-12-17 20:48:08 -06:00
Jeff Carr 23b2b26643 formatting for releases 2024-12-17 13:13:34 -06:00
Jeff Carr 7db8376213 todo: fix this error, but not now 2024-12-17 07:09:39 -06:00
Jeff Carr ee7e8d5b2b lots of changes in gitpb 2024-12-17 06:37:00 -06:00
Jeff Carr b2e51ccb9e improve Init() 2024-12-17 01:15:17 -06:00
Jeff Carr 1369d2df31 save mtime's on Init() 2024-12-17 00:21:20 -06:00
Jeff Carr 075fce61e5 start cleaning up git interactions 2024-12-17 00:00:27 -06:00
Jeff Carr a9286af8fd minor new funcs 2024-12-16 03:04:21 -06:00
Jeff Carr 6922059a0b sortcut for finding Getwd() repo 2024-12-16 00:19:23 -06:00
Jeff Carr 0b09db58dd todo: need to fix this for go.work env 2024-12-15 21:27:29 -06:00
Jeff Carr a31f675e12 still trying to perfect this. todo: make gitpb perfect 2024-12-15 15:52:46 -06:00
Jeff Carr e3608b784e trying to fix clone 2024-12-15 12:14:36 -06:00
Jeff Carr 340872788e clone() and misc other fixes and improvements 2024-12-15 08:48:06 -06:00
Jeff Carr e796788e22 attempt to symlink plugin files 2024-12-14 22:32:11 -06:00
Jeff Carr b55548a58b remove os.Exit() as things are more stable now 2024-12-14 16:29:39 -06:00
Jeff Carr 5673e2cc2e fix go plugin build/install 2024-12-14 14:30:45 -06:00
Jeff Carr b1d6923ca2 add InitPB() for read-only init() 2024-12-14 11:28:15 -06:00
Jeff Carr d20ce6b0e8 add NewestAge(). this is dumb. move this to gitpb 2024-12-14 10:48:08 -06:00
Jeff Carr b715fdd11a make devel and user branches 2024-12-13 16:18:03 -06:00
Jeff Carr 4a2568fea3 cleaner output 2024-12-13 12:56:23 -06:00
Jeff Carr 9873bc3d57 clean up Init() 2024-12-13 02:22:13 -06:00
Jeff Carr 4e94089128 cleaner Init() output
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-12-12 23:50:38 -06:00
Jeff Carr 4928804bf4 move go.mod handling code to go-mod-clean
Signed-off-by: Jeff Carr <jcarr@wit.com>
2024-12-12 23:15:19 -06:00
Jeff Carr 6d25d1b1cb re-parse go.sum again after Trim() 2024-12-12 02:06:55 -06:00
Jeff Carr b7e36449de make patchset 2024-12-11 22:48:33 -06:00
Jeff Carr dae2653975 use 'all' instead of 'loop' 2024-12-11 19:31:37 -06:00
Jeff Carr 0235d4d096 add patch.proto 2024-12-11 18:50:36 -06:00
Jeff Carr 10e1f545bb attempt simple go.sum trim 2024-12-11 13:55:21 -06:00
Jeff Carr 7512463a57 logic for go-clean 2024-12-11 01:19:22 -06:00
Jeff Carr 757fd9ba55 go.work file fixes 2024-12-10 01:48:29 -06:00
Jeff Carr c08079fc2f deubgging auto go build 2024-12-07 16:50:04 -06:00
Jeff Carr 7f1f8d4028 code moved to gitpb 2024-12-06 01:50:03 -06:00
Jeff Carr e25095f2e7 add exception for go-gl 2024-12-05 17:40:23 -06:00
Jeff Carr 34a287a38e work on config file save 2024-12-05 12:37:07 -06:00
Jeff Carr f98179971e this .proto might allow generic marshal recovery 2024-12-04 15:36:49 -06:00
Jeff Carr 4d221c5220 test map for repos that have disappeared or are not working 2024-12-03 23:04:04 -06:00
Jeff Carr a58234f82c rename 2024-12-03 18:03:42 -06:00
33 changed files with 2659 additions and 880 deletions

6
.gitignore vendored
View File

@ -1,5 +1,5 @@
go.* go.*
*.swp
*.patch
*.mbox
*.pb.go *.pb.go
forgeConfig/forgeConfig

View File

@ -5,7 +5,11 @@
# go install # go install
all: forgeConfig.pb.go vet all: forgeConfig.pb.go patchset.pb.go goimports vet
generate: clean
autogenpb --proto patchset.proto
autogenpb --proto forgeConfig.proto
vet: vet:
@GO111MODULE=off go vet @GO111MODULE=off go vet
@ -15,15 +19,13 @@ vet:
goimports: goimports:
goimports -w *.go goimports -w *.go
redomod:
rm -f go.*
GO111MODULE= go mod init
GO111MODULE= go mod tidy
go mod edit -go=1.20
clean: clean:
rm -f *.pb.go rm -f *.pb.go *.patch
-rm -f go.* -rm -f go.*
go-mod-clean --purge
forgeConfig.pb.go: forgeConfig.proto forgeConfig.pb.go: forgeConfig.proto
autogenpb --proto forgeConfig.proto autogenpb --proto forgeConfig.proto
patchset.pb.go: patchset.proto
autogenpb --proto patchset.proto

175
build.go
View File

@ -1,14 +1,12 @@
package forgepb package forgepb
// for golang repos, this is an attempt to build the package // for golang repos, this is an attempt to build the package
// there might be some 'standard' ways to implement a build //
// that make sense.
// Additions to 'go build' that are attempted here: // Additions to 'go build' that are attempted here:
// //
// * detect packages that are plugins // * detect packages that are plugins
// * autogen packages that have .proto protobuf files // * run autogenpb packages that have .proto protobuf files
// * define some 'standard' ldflags // * use some 'standard' ldflags (VERISON, BUILDTIME)
// //
import ( import (
@ -19,6 +17,7 @@ import (
"strings" "strings"
"time" "time"
"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"
) )
@ -31,51 +30,96 @@ func (f *Forge) Install(repo *gitpb.Repo, userFlags []string) error {
return f.doBuild(repo, userFlags, "install") return f.doBuild(repo, userFlags, "install")
} }
// userflags are intended for "-v" and "-x" right now
func (f *Forge) doBuild(repo *gitpb.Repo, userFlags []string, goWhat string) error { func (f *Forge) doBuild(repo *gitpb.Repo, userFlags []string, goWhat string) error {
// always assume all sources have been downloaded if repo == nil {
// todo: detect when in ~/go/src vs go.work mode log.Warn("forge.doBuild repo == nil")
return errors.New("forge.doBuild repo == nil")
}
if f.IsGoWork() {
// when building using a go.work file, never use GO111MODULE=off
os.Unsetenv("GO111MODULE")
} else {
// when building from ~/go/src, always use GO111MODULE=off
os.Setenv("GO111MODULE", "off") os.Setenv("GO111MODULE", "off")
defer os.Unsetenv("GO111MODULE") defer os.Unsetenv("GO111MODULE")
// get the version
version := repo.GetCurrentBranchVersion()
/*
loop := repo.Tags.SortByRefname()
for loop.Scan() {
t := loop.Next()
log.Info("Build() tag:", t.Refname)
} }
*/
if repo.GoDeps == nil { if f.IsGoWork() {
repo.RedoGoMod() // there must be a valid go.mod file if compiling with go.work
if err := repo.ValidGoSum(); err != nil {
log.Warn("forge.doBuild() failed. run go-mod-clean here?")
return err
} }
if repo.GoDeps.Len() == 0 {
// eh, potentially runs it twice. don't care right now
log.Info("redo go.mod", repo.GetGoPath())
repo.RedoGoMod()
f.Repos.ConfigSave()
} }
loop1 := repo.GoDeps.SortByGoPath()
for loop1.Scan() { if repo.Exists(".forge") {
t := loop1.Next() log.Info("custom build instructions")
found := f.Repos.FindByGoPath(t.GetGoPath()) data, _ := repo.ReadFile(".forge")
if found.RepoType() == "protobuf" { log.Info(".forge =", string(data))
log.Info("todo: do the custom build instructions")
basedir, filename := filepath.Split(repo.GetGoPath())
log.Info("touching filename", basedir, filename)
repo.RunVerbose([]string{"touch", filename})
return nil
}
// if not GoPrimitive, autogen each dependent git repo
if repo.GoDepsLen() != 0 {
// build the protobuf files in all protobuf repos
all := repo.GoDeps.SortByGoPath()
for all.Scan() {
t := all.Next()
found := f.FindByGoPath(t.GetGoPath())
if found.GetRepoType() == "protobuf" {
if err := f.runAutogenpb(found); err != nil { if err := f.runAutogenpb(found); err != nil {
return err return err
} }
} }
} }
}
// get the version
version := repo.GetCurrentBranchVersion()
if version == "" {
version = "forgeErr"
}
if repo.CheckDirty() { if repo.CheckDirty() {
version = version + "-dirty" version = version + "-dirty"
} }
cmd := []string{"go", goWhat, "-v"} cmd := []string{"go"}
if repo.GetRepoType() == "plugin" {
if goWhat == "install" {
log.Info("Can not go install plugins yet. just building to ~/go/lib/")
}
cmd = append(cmd, "build")
} else {
cmd = append(cmd, goWhat)
}
_, fname := filepath.Split(repo.FullPath)
homeDir, _ := os.UserHomeDir()
soname := fname + "." + version + ".so"
linkname := fname + ".so"
sopath := filepath.Join(homeDir, "go/lib/go-gui")
// if this is a plugin, use buildmode=plugin
if repo.GetRepoType() == "plugin" {
if goWhat == "install" {
fullname := filepath.Join(sopath, soname)
cmd = append(cmd, "-buildmode=plugin", "-o", fullname)
} else {
cmd = append(cmd, "-buildmode=plugin", "-o", soname)
}
}
for _, flag := range userFlags {
cmd = append(cmd, flag)
}
// set standard ldflag options // set standard ldflag options
now := time.Now() now := time.Now()
datestamp := now.UTC().Format("2006/01/02_1504_UTC") // datestamp := now.UTC().Format("2006/01/02_1504_UTC")
datestamp := now.UTC().Format("2006-01-02_15:04:05_UTC") // 2006-01-02 15:04:05 UTC
// log.Info("datestamp =", datestamp) // log.Info("datestamp =", datestamp)
// add some standard golang flags // add some standard golang flags
ldflags := "-X main.VERSION=" + version + " " ldflags := "-X main.VERSION=" + version + " "
@ -83,26 +127,47 @@ func (f *Forge) doBuild(repo *gitpb.Repo, userFlags []string, goWhat string) err
ldflags += "-X main.GUIVERSION=" + version + "" // todo: git this from the filesystem ldflags += "-X main.GUIVERSION=" + version + "" // todo: git this from the filesystem
cmd = append(cmd, "-ldflags", ldflags) cmd = append(cmd, "-ldflags", ldflags)
// add any flags from the command line testenv := os.Getenv("GO111MODULE")
// this might not actually work if testenv == "off" {
// todo: test this log.Info("GO111MODULE=off", "f.goWork =", f.IsGoWork(), "f.gosrc =", f.GetGoSrc())
for _, flag := range userFlags { } else {
cmd = append(cmd, "-ldflags", "-X "+flag) log.Info("GO111MODULE=", testenv, "f.goWork =", f.IsGoWork(), "f.gosrc =", f.GetGoSrc())
} }
log.Info("running:", repo.FullPath)
log.Info("running:", cmd) log.Info("running:", cmd)
result := repo.RunRealtime(cmd) result := repo.RunRealtime(cmd)
if result.Exit == 0 { if result.Exit != 0 {
log.Info(strings.Join(result.Stdout, "\n")) // build failed
return nil
} else {
log.DaemonMode(true) log.DaemonMode(true)
log.Info(strings.Join(result.Stdout, "\n")) log.Info(strings.Join(result.Stdout, "\n"))
log.Info(strings.Join(result.Stderr, "\n")) log.Info(strings.Join(result.Stderr, "\n"))
log.Info("result.Error =", result.Error)
log.Info("result.Exit =", result.Exit)
log.DaemonMode(false) log.DaemonMode(false)
log.Warn("go build failed", cmd) log.Warn("go build failed", cmd)
return errors.New("go build failed: " + fmt.Sprint(result.Error)) /*
pwd, _ := os.Getwd()
log.Warn("go build pwd", pwd)
res2 := shell.RunEcho(cmd)
if res2.Exit == 0 {
log.Info("again failed", res2.Exit)
log.Info("again failed cmd", strings.Join(cmd, "a"))
log.Info("again failed", strings.Join(res2.Stdout, "\n"))
} }
*/
return errors.New("go " + goWhat + " failed: " + fmt.Sprint(result.Error))
}
// make symlinks
if repo.GetRepoType() == "plugin" {
cmd := []string{"ln", "-sf", soname, linkname}
if goWhat == "install" {
shell.PathRun(sopath, cmd)
} else {
repo.Run(cmd)
}
}
log.Info(strings.Join(result.Stdout, "\n"))
return nil
} }
func (f *Forge) runAutogenpb(repo *gitpb.Repo) error { func (f *Forge) runAutogenpb(repo *gitpb.Repo) error {
@ -136,3 +201,29 @@ func (f *Forge) runAutogenpb(repo *gitpb.Repo) error {
} }
return nil return nil
} }
// sortcut to find
func (f *Forge) FindWorkingDirRepo() *gitpb.Repo {
pwd, _ := os.Getwd()
basedir := strings.TrimPrefix(pwd, f.GetGoSrc())
basedir = strings.Trim(basedir, "/")
return f.FindByGoPath(basedir)
}
// Never do this. this is stupid. fix your repo
// Never try to version & release broken repos
// leave this code here as a reminder to never attempt this
func (f *Forge) forgeIgnoreGoMod(repo *gitpb.Repo) bool {
if !repo.Exists(".forge") {
return false
}
log.Info("custom build instructions")
data, _ := repo.ReadFile(".forge")
log.Info(".forge =", string(data))
for _, line := range strings.Split(string(data), "\n") {
if strings.Contains(line, "forge:ignore:gomod") { // this is stupid
return true
}
}
return false
}

161
cleanGoSum.go Normal file
View File

@ -0,0 +1,161 @@
package forgepb
import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
// This will recreate your go.sum and go.mod files
var cleanVerbose bool = false
// checks to see if every 'master' git branch version
// matches the go.sum file
func (f *Forge) CleanGoDepsCheckOk(check *gitpb.Repo) error {
var err error = nil
var fixes [][]string
log.Printf("current repo %s go dependancy count: %d", check.GetGoPath(), check.GoDepsLen())
if check.GoDeps == nil {
return errors.New("check.GoDeps == nil")
}
all := check.GoDeps.SortByGoPath()
for all.Scan() {
depRepo := all.Next()
found := f.FindByGoPath(depRepo.GetGoPath())
if found == nil {
if f.CheckOverride(depRepo.GetGoPath()) {
// skip this gopath because it's probably broken forever
continue
}
log.Info("not found:", depRepo.GetGoPath())
err = errors.New("not found: " + depRepo.GetGoPath())
continue
}
// log.Info("found dep", depRepo.GetGoPath())
if depRepo.GetVersion() != found.GetMasterVersion() {
check := f.FindByGoPath(depRepo.GetGoPath())
var ends string
if check.CheckDirty() {
ends = "(dirty) "
}
if f.Config.IsReadOnly(check.GetGoPath()) {
ends += "(ignoring read-only) "
if cleanVerbose {
log.Printf("%-48s ok error .%s. vs .%s. %s", depRepo.GetGoPath(),
depRepo.GetVersion(), found.GetMasterVersion(), ends)
}
} else {
if f.CheckOverride(depRepo.GetGoPath()) {
ends += "(override) "
if cleanVerbose {
log.Printf("%-48s ok error .%s. vs .%s. %s", depRepo.GetGoPath(),
depRepo.GetVersion(), found.GetMasterVersion(), ends)
// skip this gopath because it's probably broken forever
}
continue
} else {
log.Printf("%-48s error %10s vs %10s %s", depRepo.GetGoPath(),
depRepo.GetVersion(), found.GetMasterVersion(), ends)
errs := fmt.Sprintf("%s error %s vs %s %s", depRepo.GetGoPath(),
depRepo.GetVersion(), found.GetMasterVersion(), ends)
if ok, _ := ValidGoVersion(found.GetMasterVersion()); ok {
// can't go get invalid version numbers
cmd := []string{"go", "get", depRepo.GetGoPath() + "@" + found.GetMasterVersion()}
fixes = append(fixes, cmd)
}
err = errors.New(errs)
}
}
}
}
for i, cmd := range fixes {
log.Info("try cmd", i, cmd)
check.RunRealtime(cmd)
}
return err
}
func (f *Forge) TrimGoSum(check *gitpb.Repo) error {
var stuff map[string]string
stuff = make(map[string]string)
var modver map[string]string
modver = make(map[string]string)
var good map[string]bool
good = make(map[string]bool)
if check == nil {
log.Info("boo, check == nil")
return errors.New("*repo == nil")
}
filename := filepath.Join(filepath.Join(check.FullPath, "go.sum"))
data, err := os.ReadFile(filename)
if err != nil {
return err
}
for _, line := range strings.Split(string(data), "\n") {
parts := strings.Fields(line)
if len(parts) < 3 {
log.Info("WIERD OR BAD:", line)
continue
}
gopath := parts[0]
version := parts[1]
hash := parts[2]
if strings.HasSuffix(version, "/go.mod") {
if _, ok := stuff[gopath]; ok {
if cleanVerbose {
log.Info("MATCHED: gopath:", gopath, "version:", version)
}
modver[gopath] = version + " " + hash
good[gopath] = true
} else {
if cleanVerbose {
log.Info("GARBAGE: gopath:", gopath, "version:", version)
}
}
} else {
if cleanVerbose {
log.Info("GOOD : gopath:", gopath, "version:", version)
}
stuff[gopath] = version + " " + hash
}
}
keys := make([]string, 0, len(stuff))
for k := range stuff {
keys = append(keys, k)
}
// rewrite the go.sum file
newfilename := filepath.Join(filepath.Join(check.FullPath, "go.sum"))
newf, err := os.OpenFile(newfilename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer newf.Close()
sort.Strings(keys)
for _, gopath := range keys {
if good[gopath] {
fmt.Fprintf(newf, "%s %s\n", gopath, stuff[gopath])
fmt.Fprintf(newf, "%s %s\n", gopath, modver[gopath])
check := f.FindByGoPath(gopath)
if check == nil {
log.Info("gopath does not really exist:", gopath)
}
}
}
// fmt.Fprintln(newf, "test")
return nil
}

216
clone.go Normal file
View File

@ -0,0 +1,216 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"strings"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
// will not violate filesystem namespace
// always returns the path or a parent path
//
// attemps to exec git clone based off of a golang path
// will transferse parent directories in case the path
// is a child of a git repo
//
// returns *gitpb.Repo if already cloned
//
// example gopath = go.wit.com/apps/go-clone
// or "go.googlesource.com/go/src/cmd/internal/pkgpath/" returns repo for "go.googlesource.com/go"
func (f *Forge) GoClone(gopath string) (*gitpb.Repo, error) {
// will match /root/go/src/go.wit.com/apps/go-clone/something/inside
// and return the *gitpb.Repo for "go.wit.com/apps/go-clone"
fullpath := filepath.Join(f.goSrc, gopath)
if pb := f.FindAnyPath(fullpath); pb != nil {
// repo already exists
return pb, nil
}
// try a direct git clone against the gopath
// if this doesn't work, probably the package is abandoned and you
// are probably using something old, broken, or wrong
if repo, err := f.urlClone(gopath, "https://"+gopath); repo != nil {
return repo, err
}
// check for parent git repos
if repo, err := f.goClonePop(gopath); repo != nil {
return repo, err
}
// query the golang package system for the last known location
// NOTE: take time to thank the go developers and google for designing this wonderful system
if pkgurl, err := runGoList(gopath); err == nil {
if repo, err := f.urlClone(gopath, pkgurl); repo != nil {
return repo, err
}
}
// todo: emit some sort of warning?
// hacks
if repo, err := f.clonePathHack(gopath); repo != nil {
return repo, err
}
return nil, errors.New("can not find git sources for gopath " + gopath)
}
// this is obvious a experiemental hack
// todo: make a config file for this?
func overridePath(gopath string) string {
switch gopath {
case "golang.org/x/crypto":
return "https://" + "go.googlesource.com/crypto"
case "golang.org/x/mod":
return "https://" + "go.googlesource.com/mod"
case "golang.org/x/net":
return "https://" + "go.googlesource.com/net"
case "golang.org/x/sys":
return "https://" + "go.googlesource.com/sys"
case "golang.org/x/sync":
return "https://" + "go.googlesource.com/sync"
case "golang.org/x/term":
return "https://" + "go.googlesource.com/term"
case "golang.org/x/text":
return "https://" + "go.googlesource.com/text"
case "golang.org/x/tools":
return "https://" + "go.googlesource.com/tools"
case "golang.org/x/xerrors":
return "https://" + "go.googlesource.com/xerrors"
case "google.golang.org/protobuf":
return "https://" + "go.googlesource.com/protobuf"
case "google.golang.org/genproto":
return "https://" + "go.googlesource.com/genproto"
case "google.golang.org/api":
return "https://" + "go.googlesource.com/api"
case "google.golang.org/grpc":
return "https://" + "go.googlesource.com/grpc"
case "google.golang.org/appengine":
return "https://" + "go.googlesource.com/appengine"
}
if strings.HasPrefix(gopath, "github.com/go-gl/glfw") {
return "https://github.com/go-gl/glfw"
}
return ""
}
// TODO: make some config file for things like this
// can be used to work around temporary problems
func (f *Forge) clonePathHack(gopath string) (*gitpb.Repo, error) {
// newdir = helloworld
// basedir = /home/jcarr/go/src/go.wit.com/apps
// giturl = https://gitea.wit.com/gui/helloworld
url := overridePath(gopath)
if url == "" {
return nil, errors.New("no gopath override here")
}
return f.urlClone(gopath, url)
}
// for: github.com/gdamore/tcell/v2
// tries git clone github.com/gdamore/tcell/v2
// then git clone github.com/gdamore/tcell
// then git clone github.com/gdamore , etc
func (f *Forge) goClonePop(gopath string) (*gitpb.Repo, error) {
log.Info("forge.goClonePop() trying", gopath)
if gopath == "" {
return nil, nil
}
newpath, newdir := filepath.Split(gopath)
if newdir == "" {
// nothing to chop
return nil, nil
}
if repo, _ := f.urlClone(newpath, "https://"+newpath); repo != nil {
return repo, nil
}
if repo, err := f.goClonePop(newpath); repo != nil {
return repo, err
}
return nil, fmt.Errorf("forge.goClonePop() did not work %s", gopath)
}
// clone a URL directly, also try cloning if 'go-import' is sent
// newdir = helloworld
// basedir = /home/jcarr/go/src/go.wit.com/apps
// giturl = https://gitea.wit.com/gui/helloworld
func (f *Forge) urlClone(gopath, giturl string) (*gitpb.Repo, error) {
var err error
fullpath := filepath.Join(f.goSrc, gopath)
basedir, newdir := filepath.Split(fullpath)
// clone the URL directly
if err = RunGitClone(newdir, basedir, giturl); err == nil {
return f.NewGoRepo(gopath, giturl)
}
// see if the URL has go-import for a new URL
if giturl, err = findGoImport(giturl); err == nil {
if err = RunGitClone(newdir, basedir, giturl); err == nil {
return f.NewGoRepo(gopath, giturl)
}
}
// log.Info("git clone from 'go-import' info failed", url)
// yes, this misses the first error
return nil, err
}
// check the server for the current go path to git url mapping
// for example:
// This will check go.wit.com for "go.wit.com/apps/go-clone"
// and return where the URL to do git clone should be
func findGoImport(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
tmp := string(bodyBytes)
parts := strings.Split(tmp, "go-import")
if len(parts) < 2 {
return "", errors.New("missing go-import")
}
// this is terrible, it doesn't even look for 'content='
// but again, this is just a hack for my own code to be
// usuable after the removal in go v1.22 of the go get clone behavior that was in v1.21
parts = strings.Split(parts[1], "\"")
var newurl string
for {
if len(parts) == 0 {
break
}
tmp := strings.TrimSpace(parts[0])
fields := strings.Split(tmp, " ")
// log.Info("FIELDS:", fields)
if len(fields) == 3 {
newurl = fields[2]
break
}
parts = parts[1:]
}
if newurl == "" {
return "", errors.New("missing git content string")
}
return newurl, nil
}

177
config.go
View File

@ -1,3 +1,5 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb package forgepb
// functions to import and export the protobuf // functions to import and export the protobuf
@ -5,115 +7,103 @@ package forgepb
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"time"
"go.wit.com/log" "go.wit.com/log"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
) )
// write to ~/.config/forge/ unless ENV{FORGE_HOME} is set
func (f *Forge) ConfigSave() error { func (f *Forge) ConfigSave() error {
// f.Config.Lock() var err error
// defer f.Config.UnLock() // backup the current config files
if os.Getenv("FORGE_HOME") == "" { if e := backupConfig(); e != nil {
homeDir, _ := os.UserHomeDir() log.Info("forge.BackupConfig() error", e)
fullpath := filepath.Join(homeDir, ".config/forge") err = e
os.Setenv("FORGE_HOME", fullpath) // continue here? notsure. could be bad either way
// out of disk space?
}
if f.Config != nil {
if e := f.Config.ConfigSave(); e != nil {
log.Info("forge.Config.ConfigSave() error", e)
err = e
}
}
if f.Repos != nil {
if f.HasFullScan() {
f.Repos.HasFullScan = true
t := time.Now()
f.Repos.FullScan = timestamppb.New(t)
}
if e := f.Repos.ConfigSave(); e != nil {
log.Info("forge.Repos.ConfigSave() error", e)
err = e
}
} }
// try to backup the current cluster config files
if err := backupConfig(); err != nil {
return err return err
} }
data, err := f.Config.Marshal()
// write to ~/.config/forge/ unless ENV{FORGE_CONFIG} is set
func (f *ForgeConfigs) ConfigSave() error {
data, err := f.Marshal()
if err != nil { if err != nil {
log.Info("proto.Marshal() failed len", len(data), err) log.Info("proto.Marshal() failed len", len(data), err)
return err return err
} }
log.Info("forgepb.ConfigSave() proto.Marshal() worked len", len(data)) // log.Info("forgepb.ConfigSave() proto.Marshal() worked len", len(data))
configWrite("forge.pb", data)
s := f.Config.FormatTEXT() s := f.FormatTEXT()
configWrite("forge.text", []byte(s)) configWrite("forge.text", []byte(s))
s = f.Config.FormatJSON() s = f.FormatJSON()
configWrite("forge.json", []byte(s)) configWrite("forge.json", []byte(s))
if f.Repos != nil {
f.Repos.ConfigSave()
}
return nil return nil
} }
// load the ~/.config/forge/ files // load the ~/.config/forge/ files
func (c *ForgeConfigs) ConfigLoad() error { func (c *ForgeConfigs) ConfigLoad() error {
if os.Getenv("FORGE_HOME") == "" { if os.Getenv("FORGE_CONFIG") == "" {
homeDir, _ := os.UserHomeDir() homeDir, _ := os.UserHomeDir()
fullpath := filepath.Join(homeDir, ".config/forge") fullpath := filepath.Join(homeDir, ".config/forge")
os.Setenv("FORGE_HOME", fullpath) os.Setenv("FORGE_CONFIG", fullpath)
} }
var data []byte // var data []byte
var err error // var err error
if c == nil { if c == nil {
// can't safely do c = new(ForgeConfig) if c is in a struct from the caller. notsure why // can't safely do c = new(ForgeConfig) if c is in a struct from the caller. notsure why
// TODO: recheck this. it might work now? It's probably still a bad idea(?)
return errors.New("It's not safe to run ConfigLoad() on a nil") return errors.New("It's not safe to run ConfigLoad() on a nil")
} }
if data, err = loadFile("forge.pb"); err != nil { if err := c.loadText(); err == nil {
// something went wrong loading the file
return err
}
if data != nil {
// this means the forge.pb file exists and was read
if len(data) == 0 {
// todo: error out if the file is empty?
// try forge.text & forge.json?
}
if err = c.Unmarshal(data); err != nil {
log.Warn("broken forge.pb config file")
return err
}
log.Info("found", len(c.ForgeConfigs), "entries in ~/.config/forge")
return nil
}
// forge.db doesn't exist. try forge.text
// this lets the user hand edit the config
if data, err = loadFile("forge.text"); err != nil {
// something went wrong loading the file
return err
}
if data != nil {
// this means the forge.text file exists and was read
if len(data) == 0 {
// todo: error out if the file is empty?
}
if err = c.UnmarshalTEXT(data); err != nil {
log.Warn("broken forge.text config file")
return err
}
log.Info("found", len(c.ForgeConfigs), "entries in ~/.config/forge")
return nil return nil
} }
// forge.text doesn't exist. try forge.json // forge.text doesn't exist. try forge.json
// this lets the user hand edit the config // this lets the user hand edit the JSON config
if data, err = loadFile("forge.json"); err != nil { // probably just deprecate this
// something went wrong loading the file if data, err := loadFile("forge.json"); err != nil {
return err if data != nil {
// this means the forge.json file exists and was read
if len(data) != 0 {
if err = c.UnmarshalJSON(data); err == nil {
log.Info("forge.ConfigLoad()", len(c.ForgeConfigs), "entries in ~/.config/forge")
// forge.text file was broken. save on load right away
log.Info("attempting forge.ConfigSave()")
c.ConfigSave()
return nil
}
}
}
} }
if data != nil { cpath := filepath.Join(os.Getenv("FORGE_CONFIG"), ".")
// this means the forge.text file exists and was read if _, err := os.Stat(cpath); err == nil {
if len(data) == 0 { log.Info("Something has gone wrong. Your", os.Getenv("FORGE_CONFIG"), "directory exists")
// todo: error out if the file is empty? log.Info("However, the config files could not be loaded")
}
if err = c.UnmarshalJSON(data); err != nil {
log.Warn("broken forge.json config file")
return err
}
log.Info("found", len(c.ForgeConfigs), "entries in ~/.config/forge")
return nil
} }
// first time user. make a template config file // first time user. make a template config file
@ -122,8 +112,29 @@ func (c *ForgeConfigs) ConfigLoad() error {
return nil return nil
} }
func (c *ForgeConfigs) loadText() error {
// this lets the user hand edit the config
data, err := loadFile("forge.text")
if err != nil {
return err
}
if data == nil {
return fmt.Errorf("forge.text data was nil")
}
if len(data) == 0 {
return fmt.Errorf("forge.text was empty")
}
// attempt to marshal forge.text
if err := c.UnmarshalTEXT(data); err != nil {
return err
}
log.Log(INFO, "forge.ConfigLoad()", len(c.ForgeConfigs), "entries in ~/.config/forge")
return nil
}
func loadFile(filename string) ([]byte, error) { func loadFile(filename string) ([]byte, error) {
fullname := filepath.Join(os.Getenv("FORGE_HOME"), filename) fullname := filepath.Join(os.Getenv("FORGE_CONFIG"), filename)
data, err := os.ReadFile(fullname) data, err := os.ReadFile(fullname)
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
// if file does not exist, just return nil. this // if file does not exist, just return nil. this
@ -139,9 +150,9 @@ func loadFile(filename string) ([]byte, error) {
} }
func configWrite(filename string, data []byte) error { func configWrite(filename string, data []byte) error {
fullname := filepath.Join(os.Getenv("FORGE_HOME"), filename) fullname := filepath.Join(os.Getenv("FORGE_CONFIG"), filename)
cfgfile, err := os.OpenFile(fullname, os.O_RDWR|os.O_CREATE, 0666) cfgfile, err := os.OpenFile(fullname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
defer cfgfile.Close() defer cfgfile.Close()
if err != nil { if err != nil {
log.Warn("open config file :", err) log.Warn("open config file :", err)
@ -149,13 +160,21 @@ func configWrite(filename string, data []byte) error {
} }
if filename == "forge.text" { if filename == "forge.text" {
// add header // add header
cfgfile.Write([]byte("# this file is automatically re-generated from forge.pb, however,\n"))
cfgfile.Write([]byte("# if you want to edit it by hand, you can:\n"))
cfgfile.Write([]byte("# stop forge; remove forge.pb; edit forge.text; start forge\n"))
cfgfile.Write([]byte("# this will cause the default behavior to fallback to parsing this file for the config\n"))
cfgfile.Write([]byte("\n")) cfgfile.Write([]byte("\n"))
cfgfile.Write([]byte("# this file is intended to be used to customize settings on what\n")) cfgfile.Write([]byte("# this file is intended to be used to customize settings on what\n"))
cfgfile.Write([]byte("# git repos you have write access to. That is, where you can run 'git push'\n")) cfgfile.Write([]byte("# git repos you have write access to. That is, where you can run 'git push'\n"))
cfgfile.Write([]byte("\n"))
}
if filename == "forge.json" {
// add header
cfgfile.Write([]byte("\n"))
cfgfile.Write([]byte("# this file is intended to be used to customize settings on what\n"))
cfgfile.Write([]byte("# git repos you have write access to. That is, where you can run 'git push'\n"))
cfgfile.Write([]byte("\n"))
cfgfile.Write([]byte("# this file is parsed only if forge.text is missing\n"))
cfgfile.Write([]byte("# also, these comment lines don't work in json files and have to be removed for Marshal() to work\n"))
cfgfile.Write([]byte("# probably, JSON syntax for this is just going to be deprecated for the TEXT syntax\n"))
cfgfile.Write([]byte("\n"))
} }
cfgfile.Write(data) cfgfile.Write(data)
return nil return nil

View File

@ -7,7 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@ -15,12 +14,8 @@ import (
func backupConfig() error { func backupConfig() error {
// make a new dir to backup the files // make a new dir to backup the files
now := time.Now() srcDir := filepath.Join(os.Getenv("FORGE_CONFIG"))
// timestamp := now.Format("2022.07.18.190545") // 50yr shout out to K&R destDir := filepath.Join(os.Getenv("FORGE_CONFIG"), "backup")
timestamp := now.Format("2006.01.02.150405") // bummer. other date doesn't work?
srcDir := filepath.Join(os.Getenv("FORGE_HOME"))
destDir := filepath.Join(os.Getenv("FORGE_HOME"), timestamp)
return backupFiles(srcDir, destDir) return backupFiles(srcDir, destDir)
} }
@ -44,7 +39,7 @@ func backupFiles(srcDir string, destDir string) error {
continue continue
} }
log.Println("backing up file", entry.Name()) // log.Println("backing up file", entry.Name())
srcPath := filepath.Join(srcDir, entry.Name()) srcPath := filepath.Join(srcDir, entry.Name())
destPath := filepath.Join(destDir, entry.Name()) destPath := filepath.Join(destDir, entry.Name())
@ -64,6 +59,9 @@ func copyFile(src, dest string) error {
} }
defer srcFile.Close() defer srcFile.Close()
now := time.Now()
timestamp := now.Format("2006.01.02.150405") // bummer. other date doesn't work?
dest = dest + timestamp
destFile, err := os.Create(dest) destFile, err := os.Create(dest)
if err != nil { if err != nil {
return err return err

View File

@ -2,8 +2,6 @@ package forgepb
import ( import (
"fmt" "fmt"
"go.wit.com/log"
) )
func (all *ForgeConfigs) sampleConfig() { func (all *ForgeConfigs) sampleConfig() {
@ -11,53 +9,7 @@ func (all *ForgeConfigs) sampleConfig() {
new1.GoPath = "go.wit.com" new1.GoPath = "go.wit.com"
new1.Writable = true new1.Writable = true
new1.Directory = true new1.Directory = true
if all.Append(new1) { all.Append(new1)
log.Info("added", new1.GoPath, "ok")
} else {
log.Info("added", new1.GoPath, "failed")
}
new1 = new(ForgeConfig)
new1.GoPath = "go.wit.com/apps/zookeeper"
new1.DebName = "zookeeper-go"
if all.Append(new1) {
log.Info("added", new1.GoPath, "ok")
} else {
log.Info("added", new1.GoPath, "failed")
}
new1 = new(ForgeConfig)
new1.GoPath = "go.wit.com/apps/wit-package"
new1.Private = true
if all.Append(new1) {
log.Info("added", new1.GoPath, "ok")
} else {
log.Info("added", new1.GoPath, "failed")
}
new1 = new(ForgeConfig)
new1.GoPath = "go.wit.com/apps/networkQuality"
new1.DebName = "networkquality"
new1.ReadOnly = true
if all.Append(new1) {
log.Info("added", new1.GoPath, "ok")
} else {
log.Info("added", new1.GoPath, "failed")
}
new2 := new(ForgeConfig)
new2.GoPath = "go.wit.com/apps/go-clone"
if all.Append(new2) {
log.Info("added", new2.GoPath, "ok")
} else {
log.Info("added", new2.GoPath, "failed")
}
if all.Append(new2) {
log.Info("added", new2.GoPath, "ok (this is bad)")
} else {
log.Info("added", new2.GoPath, "failed (but ok)")
}
fmt.Println("first time user. adding an example config file with", len(all.ForgeConfigs), "repos") fmt.Println("first time user. adding an example config file with", len(all.ForgeConfigs), "repos")
} }

View File

@ -1,27 +1,25 @@
package forgepb package forgepb
/* /*
check settings for a particular gopath lookup settings for a particular *gitpb.Repo or gopath string
this provides checks for:
IsReadOnly() // user can't push commits user settings are configured in ~/.config/forge/forge.text
IsPrivate() // repo can't be published to the pkg.go.dev system
// searchs by string
Configs.IsReadOnly(path) // user can't push commits
Configs.IsWritable(path) // the opposite, but maybe different so I put both here
IsPrivate(repo) // repo can't be published to the pkg.go.dev system
DebName() // for 'zookeeper' returns 'zookeeper-go' DebName() // for 'zookeeper' returns 'zookeeper-go'
This code is practical, not perfect
*/ */
import ( import (
"path/filepath" "path/filepath"
"strings" "strings"
"go.wit.com/lib/protobuf/gitpb"
) )
/*
func (f *Forge) SortByGoPath() *ForgeConfigIterator {
return f.Config.SortByPath()
}
*/
/* /*
func (all *ForgeConfigs) UpdateGoPath(name string, gopath string) bool { func (all *ForgeConfigs) UpdateGoPath(name string, gopath string) bool {
oldr := all.DeleteByGoPath(name) oldr := all.DeleteByGoPath(name)
@ -38,21 +36,12 @@ func (all *ForgeConfigs) UpdateGoPath(name string, gopath string) bool {
// returns true if gopath is readonly() // returns true if gopath is readonly()
// will attempt to match IsWritable("foo") against anything ending in "foo" // will attempt to match IsWritable("foo") against anything ending in "foo"
func (f *Forge) IsReadOnly(repo *gitpb.Repo) bool { func (fc *ForgeConfigs) IsReadOnly(gopath string) bool {
// var match *ForgeConfig
return f.Config.IsReadOnly(repo.GoPath)
}
// returns true if gopath is readonly()
// will attempt to match IsWritable("foo") against anything ending in "foo"
func (f *ForgeConfigs) IsReadOnly(gopath string) bool {
var match *ForgeConfig var match *ForgeConfig
all := fc.SortByGoPath() // get the list of repos
loop := f.SortByGoPath() // get the list of repos for all.Scan() {
for loop.Scan() { r := all.Next()
r := loop.Next()
if r.GoPath == gopath { if r.GoPath == gopath {
// exact gopath match // exact gopath match
if r.Writable { if r.Writable {
@ -107,13 +96,13 @@ func (f *ForgeConfigs) IsReadOnly(gopath string) bool {
// this let's you check a git tag version against a package .deb version // this let's you check a git tag version against a package .deb version
// allows gopath's to not need to match the .deb name // allows gopath's to not need to match the .deb name
// this is important in lots of cases! It is normal and happens often enough. // this is important in lots of cases! It is normal and happens often enough.
func (f *Forge) DebName(gopath string) string { func (fc *ForgeConfigs) DebName(gopath string) string {
// get "zookeeper" from "go.wit.com/apps/zookeeper" // get "zookeeper" from "go.wit.com/apps/zookeeper"
normalBase := filepath.Base(gopath) normalBase := filepath.Base(gopath)
loop := f.Config.SortByGoPath() all := fc.SortByGoPath()
for loop.Scan() { for all.Scan() {
r := loop.Next() r := all.Next()
if r.GoPath == gopath { if r.GoPath == gopath {
// returns "zookeeper-go" for "go.wit.com/apps/zookeeper" // returns "zookeeper-go" for "go.wit.com/apps/zookeeper"
if r.DebName != "" { if r.DebName != "" {
@ -134,15 +123,15 @@ func (f *Forge) DebName(gopath string) string {
// //
// IsPrivate("go.foo.com/jcarr/foo") returns true if private // IsPrivate("go.foo.com/jcarr/foo") returns true if private
// IsPrivate("foo") also returns true if "go.bar.com/jcarr/foo" is private // IsPrivate("foo") also returns true if "go.bar.com/jcarr/foo" is private
func (f *Forge) IsPrivate(thing string) bool { func (fc *ForgeConfigs) IsPrivate(thing string) bool {
var match *ForgeConfig var match *ForgeConfig
// sort by path means the simple 'match' logic // sort by path means the simple 'match' logic
// here works in the sense the last directory match // here works in the sense the last directory match
// is the one that is used // is the one that is used
loop := f.Config.SortByGoPath() // get the list of repos all := fc.SortByGoPath() // get the list of repos
for loop.Scan() { for all.Scan() {
r := loop.Next() r := all.Next()
if r.GoPath == thing { if r.GoPath == thing {
// if private is set here, then ok, otherwise // if private is set here, then ok, otherwise
// still check if a Directory match exists // still check if a Directory match exists
@ -178,12 +167,12 @@ func (f *Forge) IsPrivate(thing string) bool {
// file that lets you set things as favorites // file that lets you set things as favorites
// so you can just go-clone a bunch of common things // so you can just go-clone a bunch of common things
// on a new box or after you reset/delete your ~/go/src dir // on a new box or after you reset/delete your ~/go/src dir
func (f *Forge) IsFavorite(thing string) bool { func (fc *ForgeConfigs) IsFavorite(thing string) bool {
var match *ForgeConfig var match *ForgeConfig
loop := f.Config.SortByGoPath() // get the list of repos all := fc.SortByGoPath() // get the list of repos
for loop.Scan() { for all.Scan() {
r := loop.Next() r := all.Next()
if r.GoPath == thing { if r.GoPath == thing {
if r.Favorite { if r.Favorite {
return true return true
@ -207,3 +196,129 @@ func (f *Forge) IsFavorite(thing string) bool {
return match.Favorite return match.Favorite
} }
// IsWritable() checks your .config/forge/ settings
// looks for an exact match, then
// looks for a directory match
func (fc *ForgeConfigs) IsWritable(thing string) bool {
var match *ForgeConfig
all := fc.SortByGoPath() // get the list of repos
for all.Scan() {
r := all.Next()
if r.GoPath == thing {
if r.Writable {
return true
}
}
base := filepath.Base(r.GoPath)
if base == thing {
if r.Writable {
return true
}
}
if r.Directory {
if strings.HasPrefix(thing, r.GoPath) {
match = r
}
}
}
if match == nil {
return false
}
return match.Writable
}
// allows custom user branch names in the forge config
func (fc *ForgeConfigs) FindUserBranch(thing string) string {
var match *ForgeConfig
all := fc.SortByGoPath() // get the list of repos
for all.Scan() {
r := all.Next()
if r.GoPath == thing {
if r.UserBranchName != "" {
return r.UserBranchName
}
}
base := filepath.Base(r.GoPath)
if base == thing {
if r.UserBranchName != "" {
return r.UserBranchName
}
}
if r.Directory {
if strings.HasPrefix(thing, r.GoPath) {
match = r
}
}
}
if match == nil {
return ""
}
return match.UserBranchName
}
// allows custom devel branch names in the forge config
func (fc *ForgeConfigs) FindDevelBranch(thing string) string {
var match *ForgeConfig
all := fc.SortByGoPath() // get the list of repos
for all.Scan() {
r := all.Next()
if r.GoPath == thing {
if r.DevelBranchName != "" {
return r.DevelBranchName
}
}
base := filepath.Base(r.GoPath)
if base == thing {
if r.DevelBranchName != "" {
return r.DevelBranchName
}
}
if r.Directory {
if strings.HasPrefix(thing, r.GoPath) {
match = r
}
}
}
if match == nil {
return ""
}
return match.DevelBranchName
}
// allows custom devel branch names in the forge config
func (fc *ForgeConfigs) FindMasterBranch(thing string) string {
var match *ForgeConfig
all := fc.SortByGoPath() // get the list of repos
for all.Scan() {
r := all.Next()
if r.GoPath == thing {
if r.MasterBranchName != "" {
return r.MasterBranchName
}
}
base := filepath.Base(r.GoPath)
if base == thing {
if r.MasterBranchName != "" {
return r.MasterBranchName
}
}
if r.Directory {
if strings.HasPrefix(thing, r.GoPath) {
match = r
}
}
}
if match == nil {
return ""
}
return match.MasterBranchName
}

154
finalGoSumCheck.go Normal file
View File

@ -0,0 +1,154 @@
package forgepb
import (
"errors"
"fmt"
"strings"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
// DOES NOT MODIFY ANYTHING
//
// this is a final check to make sure, before pushing
// a golang repo, that the go.sum file has the correct
// and current version of every package
//
// it re-scans the go.sum file. DOES NOT MODIFY ANYTHING
// this is the last thing to run to double check everything
// before 'git tag' or git push --tags
func (f *Forge) FinalGoDepsCheckOk(check *gitpb.Repo, verbose bool) error {
if check == nil {
return errors.New("FinalGoDepsCheckOk() boo, check == nil")
}
// parse the go.mod and go.sum files
if !check.ParseGoSum() {
return fmt.Errorf("forge.ParseGoSum() failed. go.mod & go.sum are broken")
}
if check.GetGoPrimitive() {
return nil
}
deps := check.GoDeps.SortByGoPath()
for deps.Scan() {
depRepo := deps.Next()
found := f.FindByGoPath(depRepo.GetGoPath())
if found == nil {
if f.CheckOverride(depRepo.GetGoPath()) {
// skip this gopath because it's probably broken forever
continue
}
return fmt.Errorf("dep not found: %s", depRepo.GetGoPath())
}
if depRepo.GetVersion() == found.GetMasterVersion() {
// log.Printf("%-48s error ?? %-10s vs %-10s\n", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetMasterVersion())
continue
}
// log.Info("found dep", depRepo.GetGoPath())
if depRepo.GetVersion() != found.GetTargetVersion() {
check := f.FindByGoPath(depRepo.GetGoPath())
if f.Config.IsReadOnly(check.GetGoPath()) {
if verbose {
log.Printf("%-48s ok error .%s. vs .%s. (ignoring read-only repo)\n", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetTargetVersion())
}
} else {
if f.CheckOverride(depRepo.GetGoPath()) {
if verbose {
log.Printf("%-48s ok error .%s. vs .%s. (forge.CheckOverride())\n", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetTargetVersion())
}
// skip this gopath because it's probably broken forever
continue
} else {
// log.Printf("%-48s error ?? %-10s vs %-10s\n", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetMasterVersion())
// log.Printf("%-48s error %10s vs %10s\n", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetTargetVersion())
return fmt.Errorf("%-48s error %10s vs %10s", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetMasterVersion())
}
}
}
}
return nil
}
func (f *Forge) CheckOverride(gopath string) bool {
if gopath == "cloud.google.com/go" {
// log.Info("CheckOverride() is ignoring", gopath)
return true
}
if gopath == "bou.ke/monkey" {
// log.Info("CheckOverride() is ignoring", gopath)
return true
}
if gopath == "github.com/posener/complete/v2" {
// log.Info("CheckOverride() is ignoring", gopath)
return true
}
if strings.HasPrefix(gopath, "github.com/go-gl") {
// log.Info("CheckOverride() is ignoring", gopath)
return true
}
if strings.HasPrefix(gopath, "google.golang.org") {
// log.Info("CheckOverride() is ignoring", gopath)
return true
}
if strings.HasPrefix(gopath, "go.opencensus.io") {
// log.Info("CheckOverride() is ignoring", gopath)
return true
}
if strings.HasPrefix(gopath, "github.com/nicksnyder/go-i18n") {
// log.Info("CheckOverride() is ignoring", gopath)
return true
}
// fuckit for now. just blacklist github.com
if strings.HasPrefix(gopath, "github.com/") {
// log.Info("CheckOverride() is ignoring", gopath)
return true
}
return false
}
func (f *Forge) TestGoDepsCheckOk(godeps *gitpb.GoDeps, verbose bool) error {
if godeps == nil {
return errors.New("forge.TestGoDepsCheckOk() godeps == nil")
}
all := godeps.SortByGoPath()
for all.Scan() {
depRepo := all.Next()
found := f.FindByGoPath(depRepo.GetGoPath())
if found == nil {
if f.CheckOverride(depRepo.GetGoPath()) {
// skip this gopath because it's probably broken forever
continue
}
return fmt.Errorf("dep not found: %s", depRepo.GetGoPath())
}
if depRepo.GetVersion() == found.GetMasterVersion() {
// log.Printf("%-48s error ?? %-10s vs %-10s\n", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetMasterVersion())
continue
}
// log.Info("found dep", depRepo.GetGoPath())
if depRepo.GetVersion() != found.GetTargetVersion() {
check := f.FindByGoPath(depRepo.GetGoPath())
if f.Config.IsReadOnly(check.GetGoPath()) {
if verbose {
log.Printf("%-48s ok error .%s. vs .%s. (ignoring read-only repo)\n", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetTargetVersion())
}
} else {
if f.CheckOverride(depRepo.GetGoPath()) {
if verbose {
log.Printf("%-48s ok error .%s. vs .%s. (forge.CheckOverride())\n", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetTargetVersion())
}
// skip this gopath because it's probably broken forever
continue
} else {
// log.Printf("%-48s error ?? %-10s vs %-10s\n", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetMasterVersion())
// log.Printf("%-48s error %10s vs %10s\n", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetTargetVersion())
return fmt.Errorf("%-48s error %10s vs %10s", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetMasterVersion())
}
}
}
}
return nil
}

View File

@ -1,17 +1,21 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
syntax = "proto3"; syntax = "proto3";
package forgepb; package forgepb;
import "google/protobuf/timestamp.proto"; // Import the well-known type for Timestamp import "google/protobuf/timestamp.proto"; // Import the well-known type for Timestamp
// `autogenpb:uuid:7267f5d5-954b-44b7-9f67-2eb808791355` // todo: add file support
// define 3 branches. that is all that is supported // define 3 branches. that is all that is supported
// the term 'master' is used in the code because 'main' is a reserved word in golang already // the term 'master' is used in the code because 'main' is a reserved word in golang already
// allow 'read only' and 'private' flags // allow 'read only' and 'private' flags
// package names sometimes must be different than the binary name // package names sometimes must be different than the binary name
// for example 'zookeeper' is packaged as 'zookeeper-go' // for example 'zookeeper' is packaged as 'zookeeper-go'
// due to the prior apache foundation project. This happens and is ok! // due to the prior apache foundation project. This happens and is ok!
message ForgeConfig { message ForgeConfig { // `autogenpb:nomutex`
string goPath = 1; // `autogenpb:unique` // Examples: 'go.wit.com/apps/go-clone' or "~/mythings" or "/home/src/foo" string goPath = 1; // `autogenpb:unique` `autogenpb:sort` // Examples: 'go.wit.com/apps/go-clone' or "~/mythings" or "/home/src/foo"
bool writable = 2; // if you have write access to the repo bool writable = 2; // if you have write access to the repo
bool readOnly = 3; // the opposite, but needed for now because I don't know what I'm doing bool readOnly = 3; // the opposite, but needed for now because I don't know what I'm doing
@ -25,15 +29,34 @@ message ForgeConfig {
string userBranchName = 10; // whatever your username branch is string userBranchName = 10; // whatever your username branch is
string debName = 11; // the actual name used with 'apt install' (or distro apt equivalent. string debName = 11; // the actual name used with 'apt install' (or distro apt equivalent.
// todo: appeal to everyone to alias 'apt' on rhat, gentoo, arch, etc to alias 'apt install' // todo: appeal to everyone to alias 'apt' on fedora, gentoo, arch, etc to alias 'apt install'
// so we can make easier instructions for new linux users. KISS // so we can make easier instructions for new linux users. KISS
google.protobuf.Timestamp verstamp = 12; // the git commit timestamp of the version google.protobuf.Timestamp verstamp = 12; // the git commit timestamp of the version
string goSrc = 13; // is ~/go/src unless a go.work file is found
} }
// TODO: autogen 'sort', 'marshal' // todo: fix autogenpb to look for enum
message ForgeConfigs { // `autogenpb:marshal` enum ForgeMode {
string uuid = 1; // could be useful for /usr/share/file/magic someday? MASTER = 0; // "release mode"
string version = 2; // could be used for protobuf schema change violations? DEVEL = 1; // "patch mode"
repeated ForgeConfig ForgeConfigs = 3; USER = 2; // "work mode"
}
message ForgeConfigs { // `autogenpb:marshal` `autogenpb:nomutex`
string uuid = 1; // `autogenpb:uuid:1941cd4f-1cfd-4bf6-aa75-c2c391907e81`
string version = 2; // `autogenpb:version:v0.0.47`
repeated ForgeConfig ForgeConfigs = 3;
string username = 4; // what to use for the user branch (default ENV{USER})
string xterm = 5; // what xterm the user wants as the default
repeated string xtermArgv = 6; // the argv line for xterm
string defaultGui = 7; // default GUI plugin to use
ForgeMode mode = 8; // what "mode" forge is in
}
// this generic message is used by autogen to identify and
// then dump the uuid and version from any arbitrary .pb file
message Identify { // `autogenpb:marshal`
string uuid = 1; //
string version = 2; //
} }

View File

@ -1,92 +0,0 @@
package forgepb
import (
"errors"
"go.wit.com/lib/gui/repostatus"
"go.wit.com/log"
)
func (f *Forge) GitPull() bool {
var localonly int
var badmap int
log.Log(FORGEPBWARN, "running git pull everywhere")
var failed int = 0
loop := f.Repos.SortByGoPath()
for loop.Scan() {
repo := loop.Next()
if out, err := repo.GitPull(); err == nil {
log.Log(FORGEPBWARN, "Ran git pull ok", repo.GetGoPath(), out)
} else {
failed += 1
// repo.DumpTags()
if errors.Is(repostatus.ErrorGitPullOnLocal, err) {
localonly += 1
continue
}
badmap += 1
log.Log(FORGEPBWARN, "bad unknown git error", repo.GetGoPath(), out, err)
}
}
log.Log(FORGEPBWARN, "Ran git pull in all repos. failure count =", failed)
log.Log(FORGEPBWARN, "Ran git pull in all repos. bad errors =", badmap)
if localonly != 0 {
log.Log(FORGEPBWARN, "Ran git pull in all repos. ignored local only branches =", localonly)
}
return true
}
func (f *Forge) CheckoutDevel() bool {
log.Log(FORGEPBWARN, "running git checkout devel everwhere")
var failed int = 0
var count int = 0
loop := f.Repos.SortByGoPath()
for loop.Scan() {
repo := loop.Next()
count += 1
if repo.CheckoutDevel() {
// checkout ok
} else {
failed += 1
}
}
log.Log(FORGEPBWARN, "Ran git checkout in", count, "repos. failure count =", failed)
return true
}
func (f *Forge) CheckoutMaster() bool {
log.Log(FORGEPBWARN, "running git checkout master everwhere")
var failed int = 0
var count int = 0
loop := f.Repos.SortByGoPath()
for loop.Scan() {
repo := loop.Next()
count += 1
if repo.CheckoutMaster() {
// checkout ok
} else {
failed += 1
}
}
log.Log(FORGEPBWARN, "Ran git checkout in", count, "repos. failure count =", failed)
return true
}
func (f *Forge) CheckoutUser() bool {
log.Log(FORGEPBWARN, "running git checkout master everwhere")
var failed int = 0
var count int = 0
loop := f.Repos.SortByGoPath()
for loop.Scan() {
repo := loop.Next()
count += 1
if repo.CheckoutUser() {
// checkout ok
} else {
failed += 1
}
}
log.Log(FORGEPBWARN, "Ran git checkout in", count, "repos. failure count =", failed)
return true
}

View File

@ -1,84 +0,0 @@
package forgepb
import (
"strings"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
// this is a final check to make sure, before pushing
// a golang repo, that the go.sum file has the correct
// and current version of every package
//
// it re-scans the go.sum file. DOES NOT MODIFY ANYTHING
// this is the last thing to run to double check everything
// before 'git tag' or git push --tags
func (f *Forge) FinalGoDepsCheckOk(check *gitpb.Repo) bool {
var good bool = true
if check == nil {
log.Info("boo, check == nil")
return false
}
// clear out the protobuf and rescan from the file
check.GoDeps = nil
if ok, err := check.ParseGoSum(); !ok {
log.Info("FinalGoDepsCheckOk() error", err)
return false
}
if check.GoDepsLen() == 0 {
// this is a primitive
check.GoPrimitive = true
return true
}
log.Printf("current repo %s go dependancy count: %d", check.GetGoPath(), check.GoDepsLen())
deps := check.GoDeps.SortByGoPath()
for deps.Scan() {
depRepo := deps.Next()
found := f.Repos.FindByGoPath(depRepo.GetGoPath())
if found == nil {
if f.checkOverride(depRepo.GetGoPath()) {
// skip this gopath because it's probably broken forever
continue
}
log.Info("not found:", depRepo.GetGoPath())
good = false
continue
}
// log.Info("found dep", depRepo.GetGoPath())
if depRepo.GetVersion() != found.GetTargetVersion() {
check := f.Repos.FindByGoPath(depRepo.GoPath)
if f.IsReadOnly(check) {
log.Printf("%-48s ok error .%s. vs .%s. (ignoring read-only repo)", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetTargetVersion())
} else {
if f.checkOverride(depRepo.GetGoPath()) {
log.Printf("%-48s ok error .%s. vs .%s. (forge.checkOverride())", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetTargetVersion())
// skip this gopath because it's probably broken forever
continue
} else {
log.Printf("%-48s error %10s vs %10s", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetTargetVersion())
good = false
}
}
}
}
return good
}
func (f *Forge) checkOverride(gopath string) bool {
if gopath == "cloud.google.com/go" {
log.Info("checkOverride() is ignoring", gopath)
return false
}
if strings.HasPrefix(gopath, "github.com/go-gl") {
log.Info("checkOverride() is ignoring", gopath)
return false
}
if gopath == "github.com/posener/complete/v2" {
log.Info("checkOverride() is ignoring", gopath)
return false
}
return false
}

View File

@ -9,28 +9,37 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"go.wit.com/lib/gui/shell"
"go.wit.com/log" "go.wit.com/log"
) )
func (f *Forge) GetHome() string {
return f.goSrc
}
// look for a go.work file // look for a go.work file
// otherwise use ~/go/src // otherwise use ~/go/src
func (f *Forge) findGoSrc() (string, error) { func (f *Forge) findGoSrc() (string, error) {
pwd, err := os.Getwd() pwd, err := os.Getwd()
// startpwd, _ := os.Getwd()
if err == nil { if err == nil {
// Check for go.work in the current directory and then move up until root // Check for go.work in the current directory and then move up until root
if pwd, err := digup(pwd); err == nil { if pwd, err := digup(pwd); err == nil {
log.Info("using go.work file in directory", pwd)
f.goWork = true f.goWork = true
// found an existing go.work file // found an existing go.work file
// os.Chdir(pwd) // os.Chdir(pwd)
return pwd, nil 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 // there are no go.work files, resume the ~/go/src behavior from prior to golang 1.22
pwd, err = useGoSrc() pwd, err = useGoSrc()
log.Info("using ~/go/src directory", pwd)
return pwd, err return pwd, err
} }
@ -42,9 +51,23 @@ func useGoSrc() (string, error) {
return "", err return "", err
} }
pwd := filepath.Join(homeDir, "go/src") pwd := filepath.Join(homeDir, "go/src")
shell.Mkdir(pwd) err = os.MkdirAll(pwd, os.ModePerm)
// os.Chdir(pwd) return pwd, err
return pwd, nil }
func (f *Forge) goWorkExists() bool {
var err error
workFilePath := filepath.Join(f.GetGoSrc(), "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) { func digup(path string) (string, error) {

View File

@ -24,32 +24,40 @@ func (f *Forge) ScanGoSrc() (bool, error) {
if strings.HasPrefix(dir, f.goSrc) { if strings.HasPrefix(dir, f.goSrc) {
gopath := strings.TrimPrefix(dir, f.goSrc) gopath := strings.TrimPrefix(dir, f.goSrc)
gopath = strings.Trim(gopath, "/") gopath = strings.Trim(gopath, "/")
if r := f.Repos.FindByGoPath(gopath); r != nil { if r := f.FindByGoPath(gopath); r != nil {
// log.Info("already have", gopath) // log.Info("already have", gopath)
continue continue
} }
gopaths = append(gopaths, gopath) gopaths = append(gopaths, gopath)
} else { } else {
log.Log(FORGEPBWARN, "ScanGoSrc() bad:", dir) log.Log(WARN, "ScanGoSrc() bad:", dir)
return false, errors.New("forgepb.ScanGoSrc() bad dir: " + dir) return false, errors.New("forgepb.ScanGoSrc() bad dir: " + dir)
} }
} }
newcount, err := f.rillScanDirs(gopaths) newcount, err := f.rillScanDirs(gopaths)
if err != nil { if err != nil {
log.Info("go src dir problem. exit for now?", err) log.Info("go src dir problem. exit for now?", err)
os.Exit(-1) return false, err
} }
if newcount != 0 { if newcount != 0 {
log.Info("forge go src scan found", newcount, "repos") log.Info("forge go src scan found", newcount, "repos")
f.Repos.ConfigSave() f.SetConfigSave(true)
} }
return true, err return true, err
} }
func (f *Forge) ScanDir(dir string) *gitpb.Repo {
// repo, err := f.NewGoRepo(gopath, "")
repo, err := f.Repos.NewGoRepo(dir, "")
log.Info("need to implement ScanDir()", dir, err)
return repo
}
// doesn't enter the directory any further when it finds a .git/ // doesn't enter the directory any further when it finds a .git/
// not stupid like my old version // not stupid like my old version
func gitDirectoriesNew(srcDir string) ([]string, error) { func gitDirectoriesNew(srcDir string) ([]string, error) {
var all []string var all []string
var trip bool
err := filepath.WalkDir(srcDir, func(path string, d os.DirEntry, err error) error { err := filepath.WalkDir(srcDir, func(path string, d os.DirEntry, err error) error {
if err != nil { if err != nil {
// Handle possible errors, like permission issues // Handle possible errors, like permission issues
@ -60,7 +68,19 @@ func gitDirectoriesNew(srcDir string) ([]string, error) {
if d.IsDir() { if d.IsDir() {
// log.Info("path is dir", path) // log.Info("path is dir", path)
} else { } else {
log.Info("warning: you have an untracked file:", path) _, fname := filepath.Split(path)
switch fname {
case "repos.pb":
case "go.work":
case "go.work.last":
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 return nil
} }
@ -72,6 +92,20 @@ func gitDirectoriesNew(srcDir string) ([]string, error) {
} }
return nil 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 return all, err
} }
@ -79,7 +113,7 @@ func gitDirectoriesOld(srcDir string) ([]string, error) {
var all []string var all []string
err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
log.Log(FORGEPBWARN, "Error accessing path:", path, err) log.Log(WARN, "Error accessing path:", path, err)
return nil return nil
} }
@ -92,7 +126,7 @@ func gitDirectoriesOld(srcDir string) ([]string, error) {
}) })
if err != nil { if err != nil {
log.Log(FORGEPBWARN, "Error walking the path:", srcDir, err) log.Log(WARN, "Error walking the path:", srcDir, err)
} }
return all, err return all, err
@ -108,31 +142,6 @@ func IsGitDir(dir string) bool {
return info.IsDir() return info.IsDir()
} }
/*
// rill is awesome. long live rill
func rillAddDirs(gopaths []string) {
// 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) (*repolist.RepoRow, error) {
return me.repos.View.FindByName(id), nil
})
// Activate users.
// Concurrency = 10
err := rill.ForEach(dirs, 10, func(repo *repolist.RepoRow) error {
fmt.Printf("Repo found : %s\n", repo.GoPath())
repo.Run([]string{"git", "pull"})
return nil
})
// Handle errors
fmt.Println("Error:", err)
}
*/
// rill is awesome. long live rill // rill is awesome. long live rill
// attempt scan with rill // attempt scan with rill
func (f *Forge) rillScanDirs(gopaths []string) (int, error) { func (f *Forge) rillScanDirs(gopaths []string) (int, error) {
@ -142,7 +151,7 @@ func (f *Forge) rillScanDirs(gopaths []string) (int, error) {
// Read users from the API. // Read users from the API.
// Concurrency = 20 // Concurrency = 20
dirs := rill.Map(ids, 20, func(id string) (*gitpb.Repo, error) { dirs := rill.Map(ids, 20, func(id string) (*gitpb.Repo, error) {
return f.NewGoPathRepo(id) return f.checkpath(id, "")
}) })
var counter int var counter int
@ -156,36 +165,12 @@ func (f *Forge) rillScanDirs(gopaths []string) (int, error) {
return counter, err return counter, err
} }
func (f *Forge) RillRedoGoMod() int { func (f *Forge) checkpath(gopath string, url string) (*gitpb.Repo, error) {
var all []*gitpb.Repo fullpath := filepath.Join(f.GetGoSrc(), gopath)
repos := f.Repos.SortByGoPath() log.Info("forge creating protobuf for", fullpath)
for repos.Scan() { repo, err := f.NewGoRepo(gopath, "")
repo := repos.Next()
if !repo.IsValid() {
log.Printf("%10s %-50s", "old?", repo.GetGoPath())
continue
}
all = append(all, repo)
}
// Convert a slice of user IDs into a channel
ids := rill.FromSlice(all, nil)
var counter int
// Read users from the API.
// Concurrency = 20
dirs := rill.Map(ids, 50, func(id *gitpb.Repo) (*gitpb.Repo, error) {
return id, nil
})
err := rill.ForEach(dirs, 20, func(repo *gitpb.Repo) error {
counter += 1
repo.RedoGoMod()
return nil
})
if err != nil { if err != nil {
log.Info("rill.ForEach() error:", err) log.Info("\tprotobuf error", gopath, err)
} }
return repo, err
return counter
} }

58
goWork.go Normal file
View File

@ -0,0 +1,58 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
"errors"
"fmt"
"os"
"path/filepath"
)
// very much a hack job
func (f *Forge) MakeGoWork() error {
if f.IsGoWork() {
// a go.work file was found
} else {
// you can use a go.work file in ~/go/src , but you probably shouldn't unless something
// has gone terribly wrong
return errors.New("if you want a go.work file in ~/go/src/, touch it first")
}
filename := filepath.Join(f.GetGoSrc(), "go.work")
workf, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer workf.Close()
fmt.Fprintln(workf, "go 1.21") // fix this
fmt.Fprintln(workf, "")
fmt.Fprintln(workf, "use (")
all := f.Repos.SortByFullPath()
for all.Scan() {
repo := all.Next()
/*
if !repo.IsGoLang() == "" {
// skip repos that aren't go
// todo: handle non-flat repos?
log.Info("skip non-go", repo.GetGoPath)
continue
}
*/
if repo.GetGoPath() == "" {
continue
}
fmt.Fprintln(workf, "\t"+repo.GetGoPath())
/*
if repo.pb.Exists("go.mod") {
// log.Info("ADDING REPO", goSrcDir, repo.GetGoPath())
} else {
fmt.Fprintln(workf, "\t"+repo.GetGoPath)
log.Log(REPO, "missing go.mod for", repo.GetGoPath())
}
*/
}
fmt.Fprintln(workf, ")")
return nil
}

78
http.go Normal file
View File

@ -0,0 +1,78 @@
// Copyright 1994-2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
"bytes"
"io/ioutil"
"net/http"
"os/user"
"go.wit.com/log"
)
/*
func (f *Forge) HttpPostMachine(url string) ([]byte, error) {
if f.Machine == nil {
// run f.InitMachine() here?
log.Info("you must run f.InitMachine()")
return nil, fmt.Errorf("you must run f.InitMachine()")
}
if f.Machine.Hostname == "" {
log.Info("WTF. hostname is blank")
} else {
log.Info("GOOD. hostname is set to", f.Machine.Hostname)
}
log.Info("GOOD2. hostname is set to", f.Machine.Hostname)
msg, err := f.Machine.Marshal()
if err != nil {
log.Info("proto.Marshal() failed:", err)
return nil, err
}
log.Info("GOOD3. hostname is set to", f.Machine.Hostname)
check := new(zoopb.Machine)
check.Unmarshal(msg)
if check == nil {
log.Info("WTF. check == nil")
}
log.Info("good? check.hostname =", check.Hostname)
return f.HttpPost(url, msg)
}
*/
func (f *Forge) HttpPost(url string, data []byte) ([]byte, error) {
var err error
var req *http.Request
req, err = http.NewRequest(http.MethodPost, url, bytes.NewBuffer(data))
// log.Info("httpPost() with len", len(data), "url", url)
usr, _ := user.Current()
req.Header.Set("author", usr.Username)
/*
if f.Machine == nil {
// run f.InitMachine() here?
log.Info("you must run f.InitMachine()")
return nil, fmt.Errorf("you must run f.InitMachine()")
}
*/
req.Header.Set("hostname", "fixme:hostname")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Error(err)
return []byte("client.Do(req) error"), err
}
defer resp.Body.Close()
// log.Info("httpPost() with len", len(data))
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error(err)
return body, err
}
return body, nil
}

View File

@ -1,8 +1,9 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb package forgepb
import ( import (
"fmt" "fmt"
"time"
"go.wit.com/lib/gui/shell" "go.wit.com/lib/gui/shell"
"go.wit.com/lib/protobuf/gitpb" "go.wit.com/lib/protobuf/gitpb"
@ -23,10 +24,10 @@ func standardHeader() string {
func (f *Forge) standardHeader(r *ForgeConfig) string { func (f *Forge) standardHeader(r *ForgeConfig) string {
var flags string var flags string
var readonly string var readonly string
if f.IsPrivate(r.GoPath) { if f.Config.IsPrivate(r.GoPath) {
flags += "(private) " flags += "(private) "
} }
if f.IsFavorite(r.GoPath) { if f.Config.IsFavorite(r.GoPath) {
flags += "(favorite) " flags += "(favorite) "
} }
if f.Config.IsReadOnly(r.GoPath) { if f.Config.IsReadOnly(r.GoPath) {
@ -34,6 +35,18 @@ func (f *Forge) standardHeader(r *ForgeConfig) string {
} else { } else {
readonly = "r/w" readonly = "r/w"
} }
if r.MasterBranchName != "" {
flags += "(master=" + r.MasterBranchName + ") "
}
if r.DevelBranchName != "" {
flags += "(devel=" + r.DevelBranchName + ") "
}
if r.UserBranchName != "" {
flags += "(user=" + r.UserBranchName + ") "
}
if r.DebName != "" {
flags += "(deb=" + r.DebName + ") "
}
return fmt.Sprintf("%-4s %-40s %s", readonly, r.GoPath, flags) return fmt.Sprintf("%-4s %-40s %s", readonly, r.GoPath, flags)
} }
@ -43,38 +56,29 @@ func (f *Forge) ConfigPrintTable() {
return return
} }
log.Info(standardHeader()) log.Info(standardHeader())
loop := f.Config.SortByGoPath() all := f.Config.SortByGoPath()
for loop.Scan() { for all.Scan() {
r := loop.Next() r := all.Next()
log.Info(f.standardHeader(r)) log.Info(f.standardHeader(r))
} }
} }
func (f *Forge) newestAge(repo *gitpb.Repo) time.Duration {
loop := repo.Tags.SortByAge()
for loop.Scan() {
r := loop.Next()
return time.Since(r.GetAuthordate().AsTime())
}
return time.Since(time.Now())
}
// show information while doing golang releases // show information while doing golang releases
func (f *Forge) StandardReleaseHeader(repo *gitpb.Repo, state string) string { func (f *Forge) StandardReleaseHeader(repo *gitpb.Repo, state string) string {
lastTag := repo.GetLastTag()
// tag := repo.NewestTag() // tag := repo.NewestTag()
// gitAge, _ := tag.GetDate() // gitAge, _ := tag.GetDate()
dur := f.newestAge(repo) dur := repo.NewestAge()
curname := repo.GetCurrentBranchName() curname := repo.GetCurrentBranchName()
lastTag := repo.GetLastTag()
target := repo.GetTargetVersion()
master := repo.GetMasterVersion() master := repo.GetMasterVersion()
user := repo.GetUserVersion() user := repo.GetUserVersion()
target := repo.GetTargetVersion()
header := fmt.Sprintf("%-35s %5s %-10s %-10s %-10s %-20s %-20s %-15s", header := fmt.Sprintf("%-35s %5s %-10s %-10s %-10s %-20s %-20s %-15s",
repo.GetGoPath(), shell.FormatDuration(dur), curname, repo.GetGoPath(), shell.FormatDuration(dur), curname,
lastTag, target, lastTag, target, master, user,
master, user,
state) state)
return header return header
} }
@ -86,3 +90,26 @@ func ReleaseReportHeader() string {
"MASTER", "USER", "MASTER", "USER",
"STATE") "STATE")
} }
func (f *Forge) PrintReleaseReport(repos *gitpb.Repos) int {
var count int
log.Info(ReleaseReportHeader())
loop := repos.SortByFullPath()
for loop.Scan() {
check := loop.Next()
count += 1
if check == nil {
// wtf
continue
}
var state string
if check.CheckDirty() {
state = "(dirty)"
}
log.Info(f.StandardReleaseHeader(check, state))
}
log.Info(fmt.Sprintf("total repo count = %d", count))
return count
}

105
humanShowRepo.go Normal file
View File

@ -0,0 +1,105 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
"os"
"path/filepath"
"time"
"go.wit.com/lib/gui/shell"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
func (f *Forge) HumanPrintRepo(check *gitpb.Repo) {
if check == nil {
log.Info("forge: you sent me nil")
return
}
if check.GetTargetVersion() == "" {
log.Info("TargetVersion == blank")
}
if check.GetTargetVersion() == check.GetCurrentVersion() {
log.Info("IsReleased() == true. do not release this a second time")
} else {
log.Info("IsReleased() == false")
}
if check.CheckDirty() {
log.Info("CheckDirty() == true. do not release dirty repos")
} else {
log.Info("CheckDirty() == false")
}
if check.GetGoPrimitive() {
log.Info("IsPrimitive() == true")
} else {
log.Info("IsPrimitive() == false")
}
if f.Config.IsPrivate(check.GetGoPath()) {
log.Info("IsPrivate() == true")
} else {
log.Info("IsPrivate() == false")
}
if ok, compiled, err := check.IsProtobuf(); ok {
log.Info(log.Sprint("IsProtobuf() == true compiled protobuf files = ", compiled))
if err != nil {
log.Info("IsProtobuf() ERROR = ", err)
}
for _, s := range compiled {
log.Info("\tcompiled file found:", s)
}
} else {
log.Info("IsProtobuf() == false")
if err != nil {
log.Info("IsProtobuf() ERROR = ", err)
}
}
log.Info("git master name ==", check.GetMasterBranchName())
log.Info("git devel name ==", check.GetDevelBranchName())
log.Info("git user name ==", check.GetUserBranchName())
log.Info("git current name ==", check.GetCurrentBranchName())
// testNext(check)
found := new(gitpb.Repos)
if !found.AppendByGoPath(check) {
log.Info("forgepb check. repo already existed", check.FullPath, check.GetGoPath())
} else {
log.Info("forgepb check. repo was new", check.FullPath, check.GetGoPath())
}
f.PrintHumanTable(found)
printTime("Last Pull", check.Times.LastPull.AsTime())
printTime("Last Dirty", check.Times.LastDirty.AsTime())
printTime("dir mtime", check.Times.MtimeDir.AsTime())
printTime("HEAD mtime", check.Times.MtimeHead.AsTime())
printTime("Index mtime", check.Times.MtimeIndex.AsTime())
printTime("fetch", check.Times.MtimeFetch.AsTime())
printTime("last go.sum", check.Times.LastGoDep.AsTime())
printTime("last commit", check.Times.NewestCommit.AsTime())
now := time.Now()
dur := now.Sub(check.Times.LastUpdate.AsTime())
log.Printf("Repo Last Reload: %s\n", shell.FormatDuration(dur))
}
func (f *Forge) testGoRepo(check *gitpb.Repo) {
data, _ := os.ReadFile(filepath.Join(check.FullPath, "go.mod"))
log.Info(string(data))
if err := f.FinalGoDepsCheckOk(check, true); err == nil {
log.Info("forge.FinalGoDepsCheck(check) worked!")
} else {
log.Info("forge.FinalGoDepsCheck(check) failed. boo.")
}
}
func printTime(s string, t time.Time) {
now := time.Now()
dur := now.Sub(t)
if dur < (time.Hour * 24) {
log.Printf("%s mtime last changed %s\n", s, shell.FormatDuration(dur))
}
}

245
humanTable.go Normal file
View File

@ -0,0 +1,245 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
"fmt"
"path/filepath"
"go.wit.com/lib/gui/shell"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
// you can replace all of COBOL with this amount of GO
// ah yes, COBOL. what an ancient throwback. for those that know
// then you know exactly what is in this file. For those that don't, here it is:
// All this does is output human readable text formatted to be viewable on
// a console with a fixed with font. AKA: a typerwriter. Which is exactly
// what COBOL did in the 1970's (60s? notsure) And the 80s.
// So, you want to dump out stuff on the console. Let's see. Something like
/*
forge --favorites
go.wit.com/apps/myapp v0.2.0 (installed)
go.wit.com/lib/somethingfun v0.0.7 (not downloaded)
*/
// anyway, you get the idea. This is also called COBOL because it does on
// thing and truncates every line output to the columns you see with stty -a
// my monitor is huge, so it's not going to work at 80x24. 160x48 is better
// actually, I'd predict some of these will probably end up 240 wide
// long live good eyesight and 4K monitors!
func (f *Forge) PrintHumanTable(allr *gitpb.Repos) {
log.DaemonMode(true)
var count int
// log.Info(standardStart5("gopath", "cur name", "master", "user", "repo type"))
log.Info(standardTable10("repopath", "cur br", "age", "master", "devel", "user", "curver", "lasttag", "next", "repo type"))
all := allr.SortByFullPath()
for all.Scan() {
repo := all.Next()
f.printRepoToTable(repo)
count += 1
}
log.Info("Total git repositories:", count)
}
// also shows which files are dirty
func (f *Forge) PrintHumanTableDirty(allr *gitpb.Repos) {
log.DaemonMode(true)
var count int
// log.Info(standardStart5("gopath", "cur name", "master", "user", "repo type"))
log.Info(standardTable10("repopath", "cur br", "age", "master", "devel", "user", "curver", "lasttag", "next", "repo type"))
// all := allr.SortByFullPath()
all := allr.All()
for all.Scan() {
repo := all.Next()
f.printRepoToTable(repo)
if len(repo.DirtyList) != 0 {
for _, line := range repo.DirtyList {
log.Info("\t", line)
}
}
var mver string = repo.GetMasterVersion()
repo.Tags.GetAge(mver)
count += 1
}
log.Info("Total git repositories:", count)
}
func standardTable5(arg1, arg2, arg3, arg4, arg5 string) string {
len1 := 40
len2 := 12
len3 := 12
len4 := 16
len5 := 8
var s string
if len(arg1) > len1 {
arg1 = arg1[:len1]
}
s = "%-" + fmt.Sprintf("%d", len1) + "s "
if len(arg2) > len2 {
arg2 = arg2[:len2]
}
s += "%-" + fmt.Sprintf("%d", len2) + "s "
if len(arg3) > len3 {
arg3 = arg3[:len3]
}
s += "%-" + fmt.Sprintf("%d", len3) + "s "
if len(arg4) > len4 {
arg4 = arg4[:len4]
}
s += "%-" + fmt.Sprintf("%d", len4) + "s "
if len(arg5) > len5 {
arg5 = arg5[:len5]
}
s += "%-" + fmt.Sprintf("%d", len5) + "s"
return fmt.Sprintf(s, arg1, arg2, arg3, arg4, arg5)
}
func standardTable10(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 string) string {
len1 := 40
len2 := 12
len3 := 6
len4 := 12
len5 := 16
len6 := 16
len7 := 16
len8 := 12
len9 := 12
len10 := 8
var s string
if len(arg1) > len1 {
arg1 = arg1[:len1]
}
s = "%-" + fmt.Sprintf("%d", len1) + "s "
if len(arg2) > len2 {
arg2 = arg2[:len2]
}
s += "%-" + fmt.Sprintf("%d", len2) + "s "
if len(arg3) > len3 {
arg3 = arg3[:len3]
}
s += "%-" + fmt.Sprintf("%d", len3) + "s "
if len(arg4) > len4 {
arg4 = arg4[:len4]
}
s += "%-" + fmt.Sprintf("%d", len4) + "s "
if len(arg5) > len5 {
arg5 = arg5[:len5]
}
s += "%-" + fmt.Sprintf("%d", len5) + "s "
if len(arg6) > len6 {
arg6 = arg6[:len6]
}
s += "%-" + fmt.Sprintf("%d", len6) + "s "
if len(arg7) > len7 {
arg7 = arg7[:len7]
}
s += "%-" + fmt.Sprintf("%d", len7) + "s "
if len(arg8) > len8 {
arg8 = arg8[:len8]
}
s += "%-" + fmt.Sprintf("%d", len8) + "s "
if len(arg9) > len9 {
arg9 = arg9[:len9]
}
s += "%-" + fmt.Sprintf("%d", len9) + "s "
if len(arg10) > len10 {
arg10 = arg10[:len10]
}
s += "%-" + fmt.Sprintf("%d", len10) + "s "
return fmt.Sprintf(s, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)
}
func (f *Forge) printRepoToTable(repo *gitpb.Repo) {
var end string
// shortened version numbers
var mhort string = repo.GetMasterVersion()
var dhort string = repo.GetDevelVersion()
var uhort string = repo.GetUserVersion()
if uhort == "uerr" {
// blank these out
uhort = ""
}
var lasttag string = repo.GetLastTag()
var thort string = repo.GetTargetVersion()
var chort string = repo.GetCurrentBranchVersion()
var cname string = repo.GetCurrentBranchName()
var gopath string = repo.GetGoPath()
var rtype string = repo.GetRepoType()
// ctime := repo.Tags.GetAge(mhort)
// age := shell.FormatDuration(time.Since(ctime))
age := shell.FormatDuration(repo.BranchAge(cname))
if f.Config.IsReadOnly(repo.GetGoPath()) {
// end += "(readonly) "
} else {
end += "(rw) "
}
if repo.IsDirty() {
age = ""
end += "(dirty) "
}
start := standardTable10(gopath, cname, age, mhort, dhort, uhort, chort, lasttag, thort, rtype)
if rtype == "protobuf" {
if repo.GoInfo.GoBinary {
end += "(binary) "
}
}
if repo.GetMasterBranchName() != "master" && repo.GetMasterBranchName() != "main" {
end += "(m:" + repo.GetMasterBranchName() + ") "
}
if repo.GetDevelBranchName() != "devel" {
end += "(d:" + repo.GetDevelBranchName() + ") "
}
if repo.GetUserBranchName() != f.Config.Username {
end += "(u:" + repo.GetUserBranchName() + ") "
}
debname := f.Config.DebName(repo.GetGoPath())
if debname != filepath.Base(gopath) {
end += "(deb:" + debname + ") "
}
switch repo.GetState() {
case "PERFECT":
case "unchanged":
case "dirty":
case "unknown branches":
if repo.CurrentTag == nil {
end += "(" + repo.GetState() + ") "
} else {
end += "(unknown branch " + repo.CurrentTag.Refname + ") "
}
// end += "(invalid tag) "
default:
end += "(" + repo.GetState() + ") "
}
log.Info(start, end)
}

17
identify.go Normal file
View File

@ -0,0 +1,17 @@
package forgepb
import (
"go.wit.com/log"
)
// print the protobuf in human form
func IdentifyProtobuf(data []byte) error {
var pb *Identify
pb = new(Identify)
if err := pb.Unmarshal(data); err != nil {
log.Info("data can't be identified as a standard protobuf. len =", len(data))
return err
}
log.Info("Identify protobuf file uuid =", pb.Uuid, "version =", pb.Version)
return nil
}

174
init.go
View File

@ -1,51 +1,181 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb package forgepb
import ( import (
"os" "os"
"os/user"
"path/filepath"
"time"
"go.wit.com/lib/gui/shell"
"go.wit.com/lib/protobuf/gitpb" "go.wit.com/lib/protobuf/gitpb"
"go.wit.com/lib/protobuf/zoopb"
"go.wit.com/log" "go.wit.com/log"
) )
// todo: use initOnce
// cache.go has Do()
// f.initOnce.Do(f.initWork)
func Init() *Forge { func Init() *Forge {
f := InitPB()
/*
f.Machine = new(zoopb.Machine)
if err := f.Machine.ConfigLoad(); err != nil {
log.Log(WARN, "zoopb.ConfigLoad() failed", err)
}
*/
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)
}
// f.Machine.InitWit()
if f.hasFullScan {
// duplicate time checking below. which one to keep?
if f.FullScanAge() > time.Minute {
log.Log(INFO, "forgepb.Scan() skipping scan. been run a minute ago", f.FullScanAge())
return f
}
}
now := time.Now()
start := f.Repos.Len()
f.ScanGoSrc()
f.FullScanRan()
end := f.Repos.Len()
if (end - start) == 0 {
log.Log(INFO, "forgepb.Scan() Scan did not find new git repositories. Total =", end)
if f.FullScanAge() > time.Minute {
f.rillUpdate(20, 10)
}
} else {
log.Log(INFO, "forgepb.Scan() Scan found", end-start, "new git repositories. Total =", end)
f.rillUpdate(20, 10)
}
if f.configSave {
// taking this out to debug Marshal() panic
// os.Exit(-1)
f.ConfigSave()
f.configSave = false
}
log.Log(INFO, "update() check took", shell.FormatDuration(time.Since(now)))
return f
}
func DetermineGoPath() *Forge {
f := new(Forge) f := new(Forge)
// TODO: rethink this but it works for now // TODO: rethink this but it works for now
gosrc := os.Getenv("FORGE_GOSRC") gosrc := os.Getenv("FORGE_GOSRC")
if gosrc == "" { if gosrc == "" {
// already set. ignore init()
goSrcDir, err := f.findGoSrc() goSrcDir, err := f.findGoSrc()
if err != nil { if err != nil {
log.Warn("forge init() findGoSrc()", err) log.Log(WARN, "forge init() findGoSrc()", err)
} }
os.Setenv("FORGE_GOSRC", goSrcDir) os.Setenv("FORGE_GOSRC", goSrcDir)
} }
f.goSrc = os.Getenv("FORGE_GOSRC") f.goSrc = os.Getenv("FORGE_GOSRC")
// cache.go has Do() // also rethink this, but maybe this is the right thing to do
// f.initOnce.Do(f.initWork) if os.Getenv("FORGE_CONFIG") == "" {
homeDir, _ := os.UserHomeDir()
f.Config = new(ForgeConfigs) fullpath := filepath.Join(homeDir, ".config/forge")
os.Setenv("FORGE_CONFIG", fullpath)
// load the ~/.config/forge/ config
if err := f.Config.ConfigLoad(); err != nil {
log.Warn("forgepb.ConfigLoad() failed", err)
os.Exit(-1)
} }
f.Repos = new(gitpb.Repos) f.configDir = os.Getenv("FORGE_CONFIG")
f.Repos.ConfigLoad()
f.Machine = new(zoopb.Machine)
if err := f.Machine.ConfigLoad(); err != nil { // check again for go.work // user could have a go.work file in ~/go/src
log.Warn("zoopb.ConfigLoad() failed", err) if f.goWorkExists() {
os.Exit(-1) f.goWork = true
} }
f.Machine.InitWit()
log.Info("forge pre scan ", f.Repos.Len(), "repos in", f.goSrc) // print out the settings that will be used
f.ScanGoSrc() log.Log(INFO, "forgepb.Init() FORGE_CONFIG", os.Getenv("FORGE_CONFIG"))
log.Info("forge.Init() found", f.Repos.Len(), "repos in", f.goSrc)
return f return f
} }
func (f *Forge) InitPB() {
// load the ~/.config/forge/ config
f.Config = new(ForgeConfigs)
if err := f.Config.ConfigLoad(); err != nil {
log.Log(WARN, "forgepb.ConfigLoad() failed", err)
}
if f.IsGoWork() {
log.Log(INFO, "forgepb.Init() FORGE_GOSRC ", os.Getenv("FORGE_GOSRC"), "(go.work = true)")
} else {
log.Log(INFO, "forgepb.Init() FORGE_GOSRC ", os.Getenv("FORGE_GOSRC"), "(go.work = false)")
}
f.Repos = gitpb.NewRepos()
f.Repos.ConfigLoad()
if f.Repos.HasFullScan {
f.hasFullScan = true
}
if os.Getenv("FORGE_URL") != "" {
forgeURL = os.Getenv("FORGE_URL")
log.Info("got forge url", forgeURL)
}
}
func (f *Forge) InitMachine() {
/*
f.Machine = new(zoopb.Machine)
if err := f.Machine.ConfigLoad(); err != nil {
log.Log(WARN, "zoopb.ConfigLoad() failed", err)
f.Machine.InitWit()
}
*/
if f.Config.Username == "" {
usr, _ := user.Current()
f.Config.Username = usr.Username
}
/*
if f.Machine.Hostname == "" {
r, err := shell.RunVerbose([]string{"hostname", "-f"})
if err == nil {
tmp := strings.Join(r.Stdout, "\n")
f.Machine.Hostname = strings.TrimSpace(tmp)
}
}
*/
}
// only init's the protobuf. intended to not scan or change anything
func InitPB() *Forge {
f := DetermineGoPath()
f.InitPB()
return f
}
func (f *Forge) SetConfigSave(b bool) {
f.configSave = b
}
// saves the config if there have been changes
func (f *Forge) Exit() {
// log.Info("forge.configSave =", f.configSave)
if f.configSave {
f.ConfigSave()
}
// log.Info("forge.Exit() ok")
os.Exit(0)
}

8
log.go
View File

@ -4,13 +4,13 @@ import (
"go.wit.com/log" "go.wit.com/log"
) )
var FORGEPB *log.LogFlag var INFO *log.LogFlag
var FORGEPBWARN *log.LogFlag var WARN *log.LogFlag
func init() { func init() {
full := "go.wit.com/lib/protobuf/forgepb" full := "go.wit.com/lib/protobuf/forgepb"
short := "forgepb" short := "forgepb"
FORGEPB = log.NewFlag("FORGEPB", false, full, short, "general forgepb things") INFO = log.NewFlag("INFO", false, full, short, "general forgepb things")
FORGEPBWARN = log.NewFlag("FORGEPBWARN", true, full, short, "forgepb warnings") WARN = log.NewFlag("WARN", true, full, short, "forgepb warnings")
} }

17
mode.go Normal file
View File

@ -0,0 +1,17 @@
// Copyright 1994-2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
// TODO: implement i18n with the protobuf's
func (f *Forge) GetMode() string {
switch f.Config.Mode {
case ForgeMode_MASTER:
return "Release Mode (master branch)"
case ForgeMode_DEVEL:
return "Patch Mode (devel branch)"
case ForgeMode_USER:
return "Hack Mode (user branch)"
default:
return f.Config.Mode.String()
}
}

34
patchset.Get.go Normal file
View File

@ -0,0 +1,34 @@
// Copyright 1994-2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
"go.wit.com/log"
)
var forgeURL string = "https://go.wit.com/"
func (f *Forge) GetPatchesets() (*Patchsets, error) {
url := forgeURL + "GetPatchsets"
log.Info("GetPatchsets() url", url)
body, err := f.HttpPost(url, nil)
if err != nil {
log.Info("httpPost() failed:", err)
return nil, err
}
log.Info("GetPatchets() len(body)", len(body))
var psets *Patchsets
psets = new(Patchsets)
err = psets.Unmarshal(body)
if err != nil {
log.Info("Unmarshal failed", err)
return nil, err
}
/*
filename := filepath.Join("/tmp", pbfile)
f, _ := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
f.Write(body)
f.Close()
*/
return psets, nil
}

272
patchset.Make.go Normal file
View File

@ -0,0 +1,272 @@
package forgepb
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
)
// creates a patchset
// works from the user branches against the devel branches
func (f *Forge) MakeDevelPatchSet(name string) (*Patchset, error) {
pset := new(Patchset)
pset.Name = name
pset.Ctime = timestamppb.New(time.Now())
pset.Uuid = uuid.New().String()
if os.Getenv("GIT_AUTHOR_NAME") == "" {
return nil, fmt.Errorf("GIT_AUTHOR_NAME not set")
} else {
pset.GitAuthorName = os.Getenv("GIT_AUTHOR_NAME")
}
if os.Getenv("GIT_AUTHOR_EMAIL") == "" {
return nil, fmt.Errorf("GIT_AUTHOR_EMAIL not set")
} else {
pset.GitAuthorEmail = os.Getenv("GIT_AUTHOR_EMAIL")
}
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()
if !repo.IsLocalBranch(repo.GetUserBranchName()) {
// log.Info("repo doesn't have user branch", repo.GetGoPath())
continue
}
if !repo.IsLocalBranch(repo.GetDevelBranchName()) {
// log.Info("repo doesn't have devel branch", repo.GetGoPath())
continue
}
// make a patchset from user to devel
// TODO: verify branches are otherwise exact
pset.StartBranchName = repo.GetDevelBranchName()
pset.EndBranchName = repo.GetUserBranchName()
err := pset.makePatchSetNew(repo)
if err != nil {
return nil, err
}
}
return pset, nil
}
func (f *Forge) SubmitDevelPatchSet(name string) (*Patchset, error) {
pset, err := f.MakeDevelPatchSet(name)
if err != nil {
return nil, err
}
if err := f.submitPatchset(pset); err != nil {
return nil, err
}
return pset, nil
}
func (f *Forge) MakeMasterPatchSet() (*Patchset, error) {
pset := new(Patchset)
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
endBranch := pset.EndBranchName
repoDir := filepath.Join(pset.TmpDir, repo.GetGoPath())
err := os.MkdirAll(repoDir, 0755)
if err != nil {
return err
}
// maybe better? maybe worse?
// git format-patch -o patches --stdout <commit-range> > my-patch.mbox
// git format-patch --stdout -5 > my-patch.mbox # last 5 patches
// git am < my-patch.mbox
// git format-patch branch1..branch2
// export GIT_COMMITTER_DATE="2024-01-01T12:00:00"
// export GIT_AUTHOR_DATE="2024-01-01T12:00:00"
// export GIT_COMMITTER_NAME="Your Name"
// export GIT_COMMITTER_EMAIL="your.email@example.com"
// export GIT_AUTHOR_NAME="Your Name"
// export GIT_AUTHOR_EMAIL="your.email@example.com"
// git am < patch.mbox
cmd := []string{"git", "format-patch", "-o", repoDir, startBranch + ".." + endBranch}
r := repo.Run(cmd)
if r.Error != nil {
log.Info("git format-patch", repo.FullPath)
log.Info("git format-patch", cmd)
log.Info("git format-patch error", r.Error)
return r.Error
}
if r.Exit != 0 {
log.Info("git format-patch", repo.FullPath)
log.Info("git format-patch", cmd)
log.Info("git format-patch exit", r.Exit)
return errors.New(fmt.Sprintf("git returned %d", r.Exit))
}
if len(r.Stdout) == 0 {
// git created no files to add
return nil
}
err = pset.addPatchFiles(repo)
pset.Ctime = timestamppb.New(time.Now())
return err
}
// process each file in pDir/
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
filepath.Walk(fullDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
// Handle possible errors, like permission issues
fmt.Fprintf(os.Stderr, "error accessing path %q: %v\n", path, err)
baderr = err
return err
}
if info.IsDir() {
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)
if err != nil {
log.Info("addPatchFile() failed", path)
baderr = err
return err
}
patch := new(Patch)
patch.Filename, _ = filepath.Rel(p.TmpDir, path)
patch.Data = data
patch.parseData()
patch.StartHash = repo.DevelHash()
patch.NewHash = "na"
patch.RepoNamespace = repo.GetGoPath()
if p.Patches == nil {
p.Patches = new(Patches)
}
p.Patches.Append(patch)
p.Patches.Uuid = uuid.New().String()
// log.Info("ADDED PATCH FILE", path)
return nil
})
return baderr
}
// looks at the git format-patch output
// saves the commit Hash
// saves the diff lines
func (p *Patch) parseData() string {
lines := strings.Split(string(p.Data), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
switch fields[0] {
case "From":
p.CommitHash = fields[1]
case "Subject:":
p.Comment = line
case "diff":
p.Files = append(p.Files, line)
}
}
return ""
}
// just an example of how to walk only directories
func onlyWalkDirs(pDir string) error {
log.Info("DIR", pDir)
// var all []string
var baderr error
filepath.WalkDir(pDir, 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)
baderr = err
return err
}
log.Info("TESTING DIR", path)
if d.IsDir() {
return filepath.SkipDir
}
log.Info("NEVER GETS HERE? WHAT IS THIS?", path)
return nil
})
return baderr
}
func (f *Forge) submitPatchset(pset *Patchset) error {
var url string
url = forgeURL + "patchset"
msg, err := pset.Marshal()
if err != nil {
log.Info("proto.Marshal() failed:", err)
return err
}
log.Info("proto.Marshal() msg len", len(msg))
body, err := f.HttpPost(url, msg)
if err != nil {
log.Info("httpPost() failed:", err)
return err
}
test := strings.TrimSpace(string(body))
lines := strings.Split(test, "\n")
count := 0
for _, line := range lines {
log.Info("got back:", line)
count += 1
}
log.Info("Total patches:", count)
return nil
}

27
patchset.Send.go Normal file
View File

@ -0,0 +1,27 @@
package forgepb
// functions to import and export the protobuf
// data to and from config files
import (
"errors"
"time"
"go.wit.com/log"
)
func (f *Forge) SendPatchSet(pset *Patchset) error {
var err error
data, err := pset.Marshal()
if err != nil {
log.Info("proto.Marshal() pset(len) error", len(data), err)
return err
}
now := time.Now()
timestamp := now.Format("2006.01.02.150405") // bummer. other date doesn't work?
cfgfile := "patchset/patchset." + timestamp + ".pb"
log.Info("proto.Marshal() pset(len)", len(data))
configWrite(cfgfile, data)
return errors.New("don't know how to send yet")
}

86
patchset.proto Normal file
View File

@ -0,0 +1,86 @@
// 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 repoNamespace = 1; // the base repo git URL
bytes data = 2; // the raw data of the whole patch
string gH = 3; // after some deliberation, I think I'll just try variable names
string gT = 4;
string gP = 5;
string gs = 6;
string gaI = 7; // that exactly match what git uses.
string gan = 8;
string gae = 9;
string gcI = 10;
string gcn = 11;
string gce = 12;
string gN = 13;
string gGG = 14;
string gGS = 15;
string gGK = 16;
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
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?
}
message Patches { // this is a "PATCH: [1/x]" series `autogenpb:gui:Patch`
string uuid = 1; // `autogenpb:uuid:be926ad9-1111-484c-adf2-d96eeabf3079`
string version = 2; // `autogenpb:version:v0.0.45`
repeated Patch Patches = 3;
}
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`
}
message Patchsets { // `autogenpb:marshal` `autogenpb:gui`
string uuid = 1; // `autogenpb:uuid:be926ad9-f07f-484c-adf2-d96eeabf3079`
string version = 2; // `autogenpb:version:v0.0.45`
repeated Patchset Patchsets = 3;
}

View File

@ -1,278 +0,0 @@
package forgepb
import (
"errors"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"go.wit.com/lib/gui/shell"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
/*
// guess the paths. returns
// realpath : the actual path on the filesystem
// goSrcPath : this could be ~/go/src, or where the go.work file is
// goPath : go.wit.com/lib/gui/repostatus (for example)
// true/false if the repo is a golang repo
func (f *Forge) guessPaths(path string) (string, string, string, bool, error) {
var realpath, goSrcDir string
var isGoLang bool = false
homeDir, err := os.UserHomeDir()
if err != nil {
log.Log(FORGEPBWARN, "Error getting home directory:", err)
return path, realpath, goSrcDir, false, err
}
goSrcDir = filepath.Join(homeDir, "go/src")
// allow arbitrary paths, otherwise, assume the repo is in ~/go/src
// unless REPO_WORK_PATH was set. to over-ride ~/go/src
// todo, look for go.work
if os.Getenv("REPO_WORK_PATH") == "" {
os.Setenv("REPO_WORK_PATH", goSrcDir)
} else {
goSrcDir = os.Getenv("REPO_WORK_PATH")
}
// todo: this is dumb
if strings.HasPrefix(path, "/") {
realpath = path
} else if strings.HasPrefix(path, "~") {
tmp := strings.TrimPrefix(path, "~")
realpath = filepath.Join(homeDir, tmp)
} else {
realpath = filepath.Join(goSrcDir, path)
isGoLang = true
}
if os.Getenv("REPO_AUTO_CLONE") == "true" {
err := f.Clone(goSrcDir, path)
if err != nil {
// directory doesn't exist. exit with nil and error nil
return path, realpath, goSrcDir, false, errors.New("git clone")
}
}
if !IsDirectory(realpath) {
log.Log(FORGEPBWARN, "directory doesn't exist", realpath)
// directory doesn't exist. exit with nil and error nil
return path, realpath, goSrcDir, false, errors.New(realpath + " does not exist")
}
filename := filepath.Join(realpath, ".git/config")
_, err = os.Open(filename)
if err != nil {
// log.Log(WARN, "Error reading .git/config:", filename, err)
// log.Log(WARN, "TODO: find .git/config in parent directory")
return path, realpath, goSrcDir, false, err
}
return path, realpath, goSrcDir, isGoLang, nil
}
*/
// TODO: make some config file for things like this
// can be used to work around temporary problems
func clonePathHack(dirname string, basedir string, gopath string) (string, error) {
// newdir = helloworld
// basedir = /home/jcarr/go/src/go.wit.com/apps
// giturl = https://gitea.wit.com/gui/helloworld
// func cloneActual(newdir, basedir, giturl string) error {
switch gopath {
case "golang.org/x/crypto":
return cloneActual(dirname, basedir, "https://"+"go.googlesource.com/crypto")
case "golang.org/x/mod":
return cloneActual(dirname, basedir, "https://"+"go.googlesource.com/mod")
case "golang.org/x/net":
return cloneActual(dirname, basedir, "https://"+"go.googlesource.com/net")
case "golang.org/x/sys":
return cloneActual(dirname, basedir, "https://"+"go.googlesource.com/sys")
case "golang.org/x/sync":
return cloneActual(dirname, basedir, "https://"+"go.googlesource.com/sync")
case "golang.org/x/term":
return cloneActual(dirname, basedir, "https://"+"go.googlesource.com/term")
case "golang.org/x/text":
return cloneActual(dirname, basedir, "https://"+"go.googlesource.com/text")
case "golang.org/x/tools":
return cloneActual(dirname, basedir, "https://"+"go.googlesource.com/tools")
case "golang.org/x/xerrors":
return cloneActual(dirname, basedir, "https://"+"go.googlesource.com/xerrors")
case "google.golang.org/protobuf":
return cloneActual(dirname, basedir, "https://"+"go.googlesource.com/protobuf")
case "google.golang.org/genproto":
return cloneActual(dirname, basedir, "https://"+"go.googlesource.com/genproto")
case "google.golang.org/api":
return cloneActual(dirname, basedir, "https://"+"go.googlesource.com/api")
case "google.golang.org/grpc":
return cloneActual(dirname, basedir, "https://"+"go.googlesource.com/grpc")
case "google.golang.org/appengine":
return cloneActual(dirname, basedir, "https://"+"go.googlesource.com/appengine")
}
return "", errors.New("no gopath override here")
}
// attempt to git clone if the go path doesn't exist
// does a git clone, if it works, returns true
// workdir = /home/jcarr/go/src/
// gopath = go.wit.com/apps/helloworld
func (f *Forge) Clone(gopath string) (*gitpb.Repo, error) {
var err error
pb, err := f.Repos.NewGoPath(f.goSrc, gopath, "")
if err == nil {
return pb, err
}
workdir := f.goSrc
fullpath := filepath.Join(workdir, gopath)
dirname := filepath.Base(fullpath)
basedir := strings.TrimSuffix(fullpath, dirname)
url := "https://" + gopath
log.Info("trying git clone")
log.Info("gopath =", gopath)
// try a direct git clone against the gopath
// cloneActual("helloworld", "/home/jcarr/go/src/go.wit.com/apps", "https://go.wit.com/apps/helloworld")
if finalurl, err := cloneActual(dirname, basedir, url); err == nil {
// git clone worked!
return f.Repos.NewGoPath(f.goSrc, gopath, finalurl)
}
log.Info("direct attempt at git clone failed", url)
// if direct git clone doesn't work, look for a redirect
// go directly to the URL as that is autoritive. If that failes
// try the go package system as maybe the git site no longer exists
if url, err = findGoImport(url); err != nil {
log.Info("findGoImport() DID NOT WORK", url)
log.Info("findGoImport() DID NOT WORK", err)
} else {
if finalurl, err := cloneActual(dirname, basedir, url); err == nil {
// git clone worked!
return f.Repos.NewGoPath(f.goSrc, gopath, finalurl)
}
}
log.Info("git clone from 'go-import' info failed", url)
// query the golang package system for the last known location
// NOTE: take time to thank the go developers and google for designing this wonderful system
if url, err = runGoList(gopath); err != nil {
log.Info("go list failed", err)
} else {
if finalurl, err := cloneActual(dirname, basedir, url); err == nil {
// git clone worked!
return f.Repos.NewGoPath(f.goSrc, gopath, finalurl)
}
}
log.Info("git clone from 'git list' info failed", url)
// try to parse a redirect
if finalurl, err := clonePathHack(dirname, basedir, gopath); err == nil {
// WTF didn't go-import or go list work?
return f.Repos.NewGoPath(f.goSrc, gopath, finalurl)
}
return nil, errors.New("can not find git sources for gopath " + gopath)
}
// newdir = helloworld
// basedir = /home/jcarr/go/src/go.wit.com/apps
// giturl = https://gitea.wit.com/gui/helloworld
func cloneActual(newdir, basedir, giturl string) (string, error) {
log.Info("cloneActual() newdir =", newdir)
log.Info("cloneActual() basedir =", basedir)
log.Info("cloneActual() giturl =", giturl)
if !IsDirectory(basedir) {
os.MkdirAll(basedir, 0750)
}
err := os.Chdir(basedir)
if err != nil {
log.Warn("chdir failed", basedir, err)
return giturl, err
}
cmd := []string{"git", "clone", "--verbose", "--progress", giturl, newdir}
log.Info("Running:", cmd)
r := shell.PathRunRealtime(basedir, cmd)
if r.Error != nil {
log.Warn("git clone error", r.Error)
return giturl, r.Error
}
fullpath := filepath.Join(basedir, newdir)
if !IsDirectory(fullpath) {
log.Info("git clone failed", giturl)
return giturl, errors.New("git clone failed " + giturl)
}
gitdir := filepath.Join(fullpath, ".git")
if IsDirectory(gitdir) {
log.Info("git cloned worked to", fullpath)
return giturl, nil
}
// git clone didn't really work but did make a directory
log.Info("fullpath is probably empty", fullpath)
return giturl, errors.New("crapnuts. rmdir fullpath here? " + fullpath)
}
// check the server for the current go path to git url mapping
// for example:
// This will check go.wit.com for "go.wit.com/apps/go-clone"
// and return where the URL to do git clone should be
func findGoImport(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
tmp := string(bodyBytes)
parts := strings.Split(tmp, "go-import")
if len(parts) < 2 {
return "", errors.New("missing go-import")
}
// this is terrible, it doesn't even look for 'content='
// but again, this is just a hack for my own code to be
// usuable after the removal in go v1.22 of the go get clone behavior that was in v1.21
parts = strings.Split(parts[1], "\"")
var newurl string
for {
if len(parts) == 0 {
break
}
tmp := strings.TrimSpace(parts[0])
fields := strings.Split(tmp, " ")
// log.Info("FIELDS:", fields)
if len(fields) == 3 {
newurl = fields[2]
break
}
parts = parts[1:]
}
if newurl == "" {
return "", errors.New("missing git content string")
}
return newurl, nil
}
func IsDirectory(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}
return info.IsDir()
}

View File

@ -1,76 +1,241 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb package forgepb
import ( import (
"os/user" "fmt"
"os"
"path/filepath"
"regexp"
"strings"
"go.wit.com/lib/protobuf/gitpb" "go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
) )
func (f *Forge) NewGoPathRepo(gopath string) (*gitpb.Repo, error) { func (f *Forge) NewGoRepo(gopath string, url string) (*gitpb.Repo, error) {
repo, err := f.Repos.NewGoPath(f.GetGoSrc(), gopath, "") fullpath := filepath.Join(f.GetGoSrc(), gopath)
test := f.Repos.FindByFullPath(fullpath)
if test != nil {
return test, nil
}
repo, err := f.Repos.NewGoRepo(fullpath, gopath)
if err != nil { if err != nil {
log.Info("WARNING. NEW FAILED", fullpath)
return nil, err return nil, err
} }
// slices.Reverse(f.Repos.Repos)
repo.URL = url
f.VerifyBranchNames(repo) f.VerifyBranchNames(repo)
repo.ParseGoSum() if f.Config.IsReadOnly(repo.GetGoPath()) {
repo.ReadOnly = true
}
repo.Reload()
return repo, nil return repo, nil
} }
func (f *Forge) VerifyBranchNames(newr *gitpb.Repo) { func isValidSemVer(version string) bool {
// log.Info("init worked for", newr.GoPath) // Regular expression for semantic versioning
regex := `^v(\d+)\.(\d+)\.(\d+)$`
matched, _ := regexp.MatchString(regex, version)
return matched
}
// golang versions MUST be vX.X.X
// it can not be vX.X and it also can not be v0.0.0
// verifies the version is format v3.2.1
// this is retardedly wrong. it needs to be done right
func (f *Forge) ValidGoVersion(ver string) (bool, error) {
return ValidGoVersion(ver)
}
func ValidGoVersion(ver string) (bool, error) {
if ver == "v0.0.0" {
return false, fmt.Errorf("golang does not allow version v0.0.0")
}
if !strings.HasPrefix(ver, "v") {
return false, fmt.Errorf("(%s) invalid. golang versions must start with a 'v'", ver)
}
xver := ver[1:]
parts := strings.Split(xver, ".")
if len(parts) != 3 {
return false, fmt.Errorf("(%s) invalid. golang versions must have exactly 3 numbers (v1.2.3)", ver)
}
if isValidSemVer(ver) {
return true, nil
}
return false, nil
}
// figure out what the name of the git master branch is
// also check the forge config
func (f *Forge) findMasterBranch(repo *gitpb.Repo) {
// check the forge config first
if bname := f.Config.FindMasterBranch(repo.GetGoPath()); bname != "" {
log.Info("Using master branch name from forge config:", bname)
repo.SetMasterBranchName(bname)
return
}
// todo: fix this after .git parsing is better or libgit2 is being used
headfile := filepath.Join(repo.GetFullPath(), ".git/refs/remotes/origin/HEAD")
if data, err := os.ReadFile(headfile); err == nil {
s := string(data)
if strings.HasPrefix(s, "ref: ") {
fields := strings.Fields(s)
_, bname := filepath.Split(fields[1])
// log.Info("Using master branch name from .git/ HEAD:", bname)
repo.SetMasterBranchName(bname)
return
}
}
if newr.GetMasterBranchName() == "" {
// try to guess what the 'master' branch is // try to guess what the 'master' branch is
if newr.IsBranch("guimaster") { if repo.IsBranch("master") {
newr.SetMasterBranchName("guimaster") repo.SetMasterBranchName("master")
} else if newr.IsBranch("master") {
newr.SetMasterBranchName("master")
} else if newr.IsBranch("main") {
newr.SetMasterBranchName("main")
} else {
// todo, figure out the name from git
newr.SetMasterBranchName("master")
if newr.CheckoutMaster() {
} else {
cmd := []string{"git", "branch", "master"}
newr.Run(cmd)
}
}
}
if f.Config.IsReadOnly(newr.GoPath) {
return return
} }
if newr.GetDevelBranchName() == "" { if repo.IsBranch("main") {
if newr.IsBranch("guidevel") { repo.SetMasterBranchName("main")
newr.SetDevelBranchName("guidevel") return
} else if newr.IsBranch("devel") {
newr.SetDevelBranchName("devel")
} else {
// forcing for now. todo: warn users
newr.SetDevelBranchName("devel")
if newr.CheckoutDevel() {
} else {
cmd := []string{"git", "branch", "devel"}
newr.Run(cmd)
}
}
} }
if newr.GetUserBranchName() == "" { // TODO: figure out the name from git
usr, _ := user.Current() repo.SetMasterBranchName("master")
uname := usr.Username /* no longer checkout on Init()
if newr.IsBranch(uname) { if repo.CheckoutMaster() {
newr.SetUserBranchName(uname) } else {
cmd := []string{"git", "branch", "master"}
repo.Run(cmd)
}
*/
}
// figure out what the name of the git devel branch is
// also check the forge config
func (f *Forge) findDevelBranch(repo *gitpb.Repo) {
// check the forge config first
if bname := f.Config.FindDevelBranch(repo.GetGoPath()); bname != "" {
repo.SetDevelBranchName(bname)
/* no longer checkout on Init()
if repo.CheckoutDevel() {
} else {
cmd := []string{"git", "branch", bname}
repo.Run(cmd)
}
*/
return
}
if repo.IsBranch("devel") {
repo.SetDevelBranchName("devel")
return
}
repo.SetDevelBranchName("devel")
/* no longer checkout on Init()
if repo.CheckoutDevel() {
} else {
cmd := []string{"git", "branch", "devel"}
repo.Run(cmd)
}
*/
}
// this is still in flux
func (f *Forge) VerifyBranchNames(repo *gitpb.Repo) {
// log.Info("init worked for", repo.GoPath)
if repo.GetMasterBranchName() == "" {
f.findMasterBranch(repo)
}
if repo.GetDevelBranchName() == "" {
f.findDevelBranch(repo)
}
if repo.GetUserBranchName() == "" {
uname := f.configUserBranchName(repo)
if repo.IsBranch(uname) {
repo.SetUserBranchName(uname)
} else { } else {
// forcing for now. todo: warn users // forcing for now. todo: warn users
newr.SetUserBranchName(uname) repo.SetUserBranchName(uname)
if newr.CheckoutUser() {
} else {
cmd := []string{"git", "branch", uname}
newr.Run(cmd)
}
} }
} }
} }
// what name should be used for the user branch?
func (f *Forge) configUserBranchName(repo *gitpb.Repo) string {
// look in the repo record for the username
uname := repo.GetUserBranchName()
if uname != "" {
return uname
}
// query the the forge config for a custom User Branch Name
uname = f.Config.FindUserBranch(repo.GetGoPath())
if uname != "" {
return uname
}
// use the os.Username
uname = f.Config.Username
return uname
}
// todo: check the forge config
func (f *Forge) configDevelBranchName(repo *gitpb.Repo) string {
if repo.GetDevelBranchName() != "" {
return repo.GetDevelBranchName()
}
if repo.IsBranch("guidevel") {
return "guidevel"
}
if repo.IsBranch("devel") {
return "devel"
}
return "devel"
}
func (f *Forge) AddFullPath(fulldir string) *gitpb.Repo {
repo := f.Repos.FindByFullPath(fulldir)
if repo != nil {
return repo
}
log.Info("don't know how to add full paths yet", fulldir)
return nil
}
func (f *Forge) FindByGoPath(gopath string) *gitpb.Repo {
fullpath := filepath.Join(f.GetGoSrc(), gopath)
return f.Repos.FindByFullPath(fullpath)
}
// tries any parent
func (f *Forge) FindAnyPath(dir string) *gitpb.Repo {
dir = filepath.Clean(dir)
repo := f.Repos.FindByFullPath(dir)
if repo != nil {
log.Info("FindAnyPath() worked = ", dir)
return repo
}
if dir == "" {
return nil
}
if dir == "/" {
return nil
}
basedir, newdir := filepath.Split(dir)
if newdir == "" {
// is this correct? not sure what stupid windows does
return nil
}
return f.FindAnyPath(basedir)
}
func (f *Forge) DeleteByGoPath(gopath string) bool {
fullpath := filepath.Join(f.GetGoSrc(), gopath)
return f.Repos.DeleteByFullPath(fullpath)
}

155
rill.go Normal file
View File

@ -0,0 +1,155 @@
package forgepb
import (
"sync"
"github.com/destel/rill"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
// rill is awesome. long live rill
// attempt scan with rill
func (f *Forge) rillUpdate(pool1 int, pool2 int) (int, error) {
var repos []*gitpb.Repo
all := f.Repos.SortByFullPath()
for all.Scan() {
repo := all.Next()
repos = append(repos, repo)
}
// Convert a slice of user IDs into a channel
ids := rill.FromSlice(repos, nil)
// Read users from the API.
// Concurrency = 20
rills := rill.Map(ids, pool1, func(repo *gitpb.Repo) (*gitpb.Repo, error) {
return repo, nil
})
var counter int
// Activate users.
// Concurrency = 10
err := rill.ForEach(rills, pool2, func(repo *gitpb.Repo) error {
counter += 1
// log.Info("rill.ForEach() gopath=", repo.GetGoPath())
return f.updateRepo(repo)
})
return counter, err
}
func (f *Forge) updateRepo(repo *gitpb.Repo) error {
if !repo.IsValidDir() {
log.Printf("%10s %-50s gopath=%s\n", "git dir is missing:", repo.FullPath, repo.GetGoPath())
f.Repos.DeleteByFullPath(repo.FullPath)
f.configSave = true
return nil
}
if repo.DidRepoChange() {
f.configSave = true
log.Info("repo changed ", repo.FullPath, repo.StateChange)
if err := repo.Reload(); err != nil {
return err
}
} else {
// log.Info("repo did not change", repo.FullPath, repo.StateChange)
}
if f.Config.IsReadOnly(repo.GetGoPath()) {
if repo.ReadOnly {
} else {
log.Info("readonly flag on repo is wrong", repo.GetGoPath())
repo.ReadOnly = true
f.configSave = true
}
}
return nil
}
var RillX int = 10
var RillY int = 10
// x is the size of the queued up pool (shouldn't matter here for this I think)
// y is how many simultanous functions will run
// todo: tune and compute x,y by # of CPUs and disk io
// todo: store x,y in forge config ? (or compute them. notsure)
func (f *Forge) RillReload() int {
var all []*gitpb.Repo
tmp := f.Repos.All()
for tmp.Scan() {
repo := tmp.Next()
if !repo.IsValidDir() {
log.Printf("%s %-50s", "got an invalid repo in forgepb.RillFuncError()", repo.GetGoPath())
continue
}
all = append(all, repo)
}
// Convert a slice of user IDs into a channel
ids := rill.FromSlice(all, nil)
var counter int
// Read users from the API.
// Concurrency = 20
dirs := rill.Map(ids, RillX, func(repo *gitpb.Repo) (*gitpb.Repo, error) {
return repo, nil
})
rill.ForEach(dirs, RillY, func(repo *gitpb.Repo) error {
if !repo.DidRepoChange() {
return nil
}
f.configSave = true
repo.Reload()
counter += 1
return nil
})
return counter
}
// x is the size of the queued up pool (shouldn't matter here for this I think)
// y is how many simultanous functions will run
// todo: tune and compute x,y by # of CPUs and disk io
// todo: store x,y in forge config ? (or compute them. notsure)
func (f *Forge) RillFuncError(rillf func(*gitpb.Repo) error) int {
var all []*gitpb.Repo
tmp := f.Repos.All()
for tmp.Scan() {
repo := tmp.Next()
if !repo.IsValidDir() {
log.Printf("%s %-50s", "got an invalid repo in forgepb.RillFuncError()", repo.GetGoPath())
continue
}
all = append(all, repo)
}
// Convert a slice of user IDs into a channel
ids := rill.FromSlice(all, nil)
var counter int
var watch int = 10
var meMu sync.Mutex
// Read users from the API.
// Concurrency = 20
dirs := rill.Map(ids, RillX, func(id *gitpb.Repo) (*gitpb.Repo, error) {
return id, nil
})
err := rill.ForEach(dirs, RillY, func(repo *gitpb.Repo) error {
meMu.Lock()
counter += 1
if counter > watch {
// log.Info("Processed", watch, "repos") // this doesn't work
watch += 50
}
meMu.Unlock()
return rillf(repo)
})
if err != nil {
log.Info("rill.ForEach() error:", err)
}
return counter
}

54
run.go Normal file
View File

@ -0,0 +1,54 @@
package forgepb
import (
"errors"
"fmt"
"os"
"path/filepath"
"go.wit.com/lib/gui/shell"
"go.wit.com/log"
)
// git clone (also downloads git notes)
// newdir = helloworld
// basedir = /home/jcarr/go/src/go.wit.com/apps
// giturl = https://gitea.wit.com/gui/helloworld
func RunGitClone(newdir, basedir, giturl string) error {
log.Info("runGitClone() newdir =", newdir)
log.Info("runGitClone() basedir =", basedir)
log.Info("runGitClone() giturl =", giturl)
if !shell.IsDir(basedir) {
os.MkdirAll(basedir, os.ModePerm)
}
err := os.Chdir(basedir)
if err != nil {
// log.Warn("chdir failed", basedir, err)
return err
}
cmd := []string{"git", "clone", "--verbose", "--progress", giturl, newdir}
log.Info("Running:", basedir, cmd)
r := shell.PathRunRealtime(basedir, cmd)
if r.Error != nil {
// log.Warn("git clone error", r.Error)
return r.Error
}
fullpath := filepath.Join(basedir, newdir)
if !shell.IsDir(fullpath) {
// log.Info("git clone failed", giturl)
return fmt.Errorf("git clone %s failed to create a directory", giturl)
}
gitdir := filepath.Join(fullpath, ".git")
if shell.IsDir(gitdir) {
// log.Info("git cloned worked to", fullpath)
// also clone notes -- this can store the go.mod and go.sum files
cmd := []string{"git", "fetch", "origin", "refs/notes/*:refs/notes/*"}
shell.PathRunRealtime(fullpath, cmd)
return nil
}
// git clone didn't really work but did make a directory
log.Info("fullpath is probably empty", fullpath)
return errors.New("crapnuts. rmdir fullpath here? " + fullpath)
}

View File

@ -2,9 +2,9 @@ package forgepb
import ( import (
sync "sync" sync "sync"
"time"
"go.wit.com/lib/protobuf/gitpb" "go.wit.com/lib/protobuf/gitpb"
"go.wit.com/lib/protobuf/zoopb"
) )
// maybe an interface someday? // maybe an interface someday?
@ -12,14 +12,38 @@ type Forge struct {
// one-time initialized data // one-time initialized data
initOnce sync.Once initOnce sync.Once
initErr error // init error, if any initErr error // init error, if any
goSrc string // the path to go/src goSrc string // the path to go/src
configDir string // normally ~/.config/forge
goWork bool // means the user is currently using a go.work file goWork bool // means the user is currently using a go.work file
Config *ForgeConfigs // config repos for readonly, private, etc Config *ForgeConfigs // config repos for readonly, private, etc
Repos *gitpb.Repos Repos *gitpb.Repos // the repo protobufs
Machine *zoopb.Machine // Machine *zoopb.Machine // things for virtigo to track vm's
configSave bool // if you need to save the config because things changed
hasFullScan bool // track last scan so it can be throttled
fullscan time.Time // time of the last scan so it can be throttled
} }
func (f *Forge) GetGoSrc() string { func (f *Forge) GetGoSrc() string {
return f.goSrc return f.goSrc
} }
func (f *Forge) GetConfigDir() string {
return f.configDir
}
func (f *Forge) IsGoWork() bool {
return f.goWork
}
func (f *Forge) HasFullScan() bool {
return f.Repos.HasFullScan
}
func (f *Forge) FullScanRan() {
f.fullscan = time.Now()
}
func (f *Forge) FullScanAge() time.Duration {
fs := f.Repos.FullScan.AsTime()
return time.Since(fs)
}