Compare commits

...

191 Commits

Author SHA1 Message Date
Jeff Carr 98ecc249c0 quiet output 2025-09-13 08:39:20 -05:00
Jeff Carr cb6394f34a finally dump this old code 2025-09-13 08:31:54 -05:00
Jeff Carr 2fccf60335 other stuff 2025-09-13 07:45:13 -05:00
Jeff Carr 2b3bd66ef2 rill in forge.Config 2025-09-13 07:09:40 -05:00
Jeff Carr 27a539b879 finally a rewrite of the old junky scanner 2025-09-13 06:26:55 -05:00
Jeff Carr 0f895e83e6 use config GO library 2025-09-13 05:33:11 -05:00
Jeff Carr ce4af38e8b track changes 2025-09-13 01:31:15 -05:00
Jeff Carr d406ee5f21 finally a smarter forge.ConfigSave() 2025-09-13 00:56:29 -05:00
Jeff Carr bc9509e43b work on a better/faster Reload() 2025-09-13 00:52:44 -05:00
Jeff Carr b21a47434e smarter behavior when in 'Normal' mode 2025-09-12 14:52:45 -05:00
Jeff Carr 07b0fcf3b6 experiment with restricting writing to these 2025-09-12 10:45:09 -05:00
Jeff Carr be1cd7372c typo 2025-09-12 10:21:25 -05:00
Jeff Carr c99f9faf3c only with VERBOSE on 2025-09-12 10:18:55 -05:00
Jeff Carr 692264d1f1 reorder the 'end' var 2025-09-12 10:17:03 -05:00
Jeff Carr 7431308823 only let forge save the config files 2025-09-12 10:13:35 -05:00
Jeff Carr b77555e9fa sure 2025-09-12 02:03:22 -05:00
Jeff Carr e12a1fb496 rm GOSRC 2025-09-11 23:10:43 -05:00
Jeff Carr eda91e3af6 stop using GoSrc() 2025-09-11 22:14:57 -05:00
Jeff Carr 6c3162f7ce rm full scan stuff 2025-09-11 21:47:19 -05:00
Jeff Carr ef00352a0b rm old code 2025-09-11 21:40:15 -05:00
Jeff Carr 235fe57beb keep removing non-protobuf fields 2025-09-11 21:19:54 -05:00
Jeff Carr 81cac13284 start removing old code 2025-09-11 20:54:49 -05:00
Jeff Carr 98407ed8b7 better config output 2025-09-11 20:03:38 -05:00
Jeff Carr 8b41d89ab2 minor 2025-09-11 07:54:18 -05:00
Jeff Carr f2ec2e74ee check mtimes here 2025-09-11 06:57:38 -05:00
Jeff Carr 74313bd867 common forge.Init() 2025-09-11 04:46:47 -05:00
Jeff Carr 15d545f389 back to a common forge.Init() 2025-09-11 04:38:52 -05:00
Jeff Carr c62f48ec16 new init() 2025-09-11 03:05:25 -05:00
Jeff Carr de30c30005 s/namespace/Namespace/ 2025-09-11 02:54:05 -05:00
Jeff Carr b89a5571e9 change config file notes 2025-09-11 02:36:28 -05:00
Jeff Carr b231a2144d trim Init() code 2025-09-11 02:32:00 -05:00
Jeff Carr 98467dec72 using new config package 2025-09-11 02:20:06 -05:00
Jeff Carr dbde2f51b8 cleanup config file handling 2025-09-11 01:53:53 -05:00
Jeff Carr ac107331fc fixes for standard forge ENV values 2025-09-10 22:36:03 -05:00
Jeff Carr e2e30d02d1 code is autogen'd now 2025-09-08 04:35:10 -05:00
Jeff Carr 556c549265 autogen some http functions 2025-09-08 04:03:30 -05:00
Jeff Carr 18796d1a47 http autogen funcs 2025-09-08 03:37:37 -05:00
Jeff Carr da9221ce34 use the common httppb 2025-09-08 01:34:33 -05:00
Jeff Carr a3007ba1b5 correct protoc syntax 2025-09-08 01:05:18 -05:00
Jeff Carr 43281eea6f added logging to the http PB 2025-09-07 21:41:31 -05:00
Jeff Carr 16b0cad836 this is working out so far 2025-09-07 21:41:27 -05:00
Jeff Carr ecf4049947 start using a standard http PB 2025-09-07 21:41:20 -05:00
Jeff Carr e3c8669be4 works at 80x24. I'm rather proud of this app. 2025-09-07 12:05:51 -05:00
Jeff Carr 964aaf823d if this works well, then I am happier 2025-09-06 21:50:57 -05:00
Jeff Carr 9dbc7c6615 add a "normal" state 2025-09-06 19:21:21 -05:00
Jeff Carr 867a4f973e moved to httppb 2025-09-06 18:49:02 -05:00
Jeff Carr 3729df67a5 patches are starting to work 2025-09-06 16:05:26 -05:00
Jeff Carr bf031595e3 patchset http routines 2025-09-06 15:22:31 -05:00
Jeff Carr c0b9518c0d set route in PB 2025-09-05 14:17:30 -05:00
Jeff Carr a9dbfb9201 more work on patches 2025-09-05 12:59:38 -05:00
Jeff Carr 2015f84fb4 add http headers to protobuf 2025-09-05 12:54:27 -05:00
Jeff Carr 822fe38eee work on patch tracking 2025-09-05 12:53:19 -05:00
Jeff Carr 817107dc16 more patch stuff 2025-09-05 01:24:34 -05:00
Jeff Carr 4a27e7702b better Send() function 2025-09-04 22:38:07 -05:00
Jeff Carr 0e05b773cf make a test checkout devel function 2025-09-04 21:34:50 -05:00
Jeff Carr 625dfe2f68 tracking down inconsistancies 2025-09-04 20:39:32 -05:00
Jeff Carr f54f189bcd check if a config file exists for new users 2025-09-04 20:19:03 -05:00
Jeff Carr dbb0d9867d Is the user new? 2025-09-04 20:02:38 -05:00
Jeff Carr 8202a75eea cleaned up init.go more perfectly 2025-09-04 19:17:58 -05:00
Jeff Carr 4cac4386f5 smarter implementation of CheckDirty() 2025-09-04 18:32:10 -05:00
Jeff Carr 89e6fd0805 fix 'nil' panic 2025-09-04 15:10:35 -05:00
Jeff Carr 076bdfb9c6 use standard table code. start labeling private repos 2025-09-04 10:25:26 -05:00
Jeff Carr 72e8d3659f use fullpath if namespace is blank 2025-09-04 09:58:50 -05:00
Jeff Carr 18aad5f1b5 common branch handling code 2025-09-04 09:15:45 -05:00
Jeff Carr d44e359d70 moved to fhelp 2025-09-04 08:35:56 -05:00
Jeff Carr 15fbb15da8 fix terminal table that prints to STDOUT 2025-09-04 08:12:33 -05:00
Jeff Carr e506a73349 use cobol.TerminalChomp() for readability 2025-09-04 08:05:19 -05:00
Jeff Carr 111c67e975 debugging panic() 2025-09-04 00:14:44 -05:00
Jeff Carr 961ca8f41e this is intersting maybe what I'm doing. I'm not current AI. that is for sure 2025-09-03 21:38:06 -05:00
Jeff Carr d6cd7fc88f append patches to local patches file 2025-09-03 20:50:54 -05:00
Jeff Carr 50351298b6 a fast CheckDirty() function for forge
30GB in 1200 repos in under a second on my fast laptop
2025-09-03 19:06:22 -05:00
Jeff Carr 3ec4b29886 make config settings for locking the path 2025-09-03 17:32:11 -05:00
Jeff Carr d981b93728 finally a proper use for sync.Once 2025-09-03 14:04:47 -05:00
Jeff Carr 98cc06ab8f lots of Init() cleanups 2025-09-03 13:54:36 -05:00
Jeff Carr 888442708c making patch handling better 2025-09-03 05:50:30 -05:00
Jeff Carr f82c9ee088 missing \n in Printf() 2025-08-31 16:25:08 -05:00
Jeff Carr 7e2caa9290 standard rill() with stats 2025-08-31 15:54:32 -05:00
Jeff Carr 5147aa73de stats for rill 2025-08-31 15:05:40 -05:00
Jeff Carr 46c11e7d33 make stats for rill 2025-08-31 14:15:51 -05:00
Jeff Carr eee996ed59 speed up finding UTF-8 errors 2025-08-31 13:27:49 -05:00
Jeff Carr 4fcc1e0349 fixes for Append() 2025-08-31 12:24:56 -05:00
Jeff Carr a1bab32121 ignore version checks on cloud.google.com/go embedded GO packages 2025-08-31 10:22:32 -05:00
Jeff Carr c8339bec60 notes for the future 2025-08-30 21:49:35 -05:00
Jeff Carr f7fe365ed1 path overrides 2025-08-29 17:45:24 -05:00
Jeff Carr 7fdb7ee788 minor summary change 2025-08-29 10:29:19 -05:00
Jeff Carr ba3846b5f2 common funcs between forge & forged 2025-08-28 19:30:59 -05:00
Jeff Carr c4146f9fbe read in patches in Init() 2025-08-28 19:30:59 -05:00
Jeff Carr 0539e2feee detect cloudflare human check 2025-08-22 01:44:16 -05:00
Jeff Carr 473cf6f9ad maybe fixed protobuf submit process for forge patches 2025-08-22 01:34:51 -05:00
Jeff Carr f020b6f828 so dumb I am sometimes 2025-08-22 01:29:55 -05:00
Jeff Carr bab3fcd479 try offset 2025-08-22 01:24:35 -05:00
Jeff Carr 624fd71c3d more debugging 2025-08-22 01:20:22 -05:00
Jeff Carr 2c74e3cde8 oops 2025-08-22 00:54:06 -05:00
Jeff Carr 5c0ed4ef8c more debugging 2025-08-22 00:53:40 -05:00
Jeff Carr 6647a3db23 increase output 2025-08-22 00:52:20 -05:00
Jeff Carr e1eafe87c4 was using wrong protobuf 2025-08-21 22:43:40 -05:00
Jeff Carr 6a1541c132 switch to namespace fix send patches 2025-08-21 22:38:37 -05:00
Jeff Carr 6db458b2df hopefully works? 2025-08-21 14:40:41 -05:00
Jeff Carr fd4b2aa6c9 output some info on the returned http body text 2025-08-21 14:31:02 -05:00
Jeff Carr 9392657f76 check if forge daemon returned a protobuf 2025-08-21 14:14:08 -05:00
Jeff Carr a433ef6309 minor 2025-08-20 12:59:18 -05:00
Jeff Carr 04983dc7ce use the better string: 'namespace' 2025-08-20 11:44:42 -05:00
Jeff Carr 9027d88e50 debugging to improve sumit 2025-08-19 22:25:12 -05:00
Jeff Carr 554adc9f0f experiement with ~/.cache/forge/ 2025-08-18 06:10:58 -05:00
Jeff Carr 25cee2b013 add a check for all repos on master 2025-08-17 23:44:46 -05:00
Jeff Carr 0de26aee43 totals by branch type 2025-08-17 23:00:14 -05:00
Jeff Carr 8b3f289eae more generic format table 2025-08-16 21:59:57 -05:00
Jeff Carr 8e8d9e9a69 return a map of the repos & errors 2025-07-21 22:25:15 -05:00
Jeff Carr b6a7af773d exposed for forged 2025-07-09 23:24:06 -05:00
Jeff Carr bd05270a83 expose these the forge daemon 2025-07-09 23:24:06 -05:00
Jeff Carr b60226c818 start UpdatePB() 2025-07-08 15:48:37 -05:00
Jeff Carr 34a10367c5 work on generic Clone() 2025-07-07 18:52:02 -05:00
Jeff Carr dea10e2150 send a repos PB 2025-07-02 12:51:22 -05:00
Jeff Carr 90ee9f6f63 autogenpb formatting change 2025-06-30 07:44:57 -05:00
Jeff Carr dbb8f23f18 switch patches to forge.wit.com. finally! 2025-06-30 04:56:12 -05:00
Jeff Carr a0525067a0 minor fix 2025-05-29 19:18:31 -05:00
Jeff Carr f9331ea548 also ignore this file GO creates when using go.work 2025-05-23 10:33:54 -05:00
Jeff Carr 521857cd53 add RillRepo() 2025-04-20 21:45:20 -05:00
Jeff Carr 050d93d401 smarter human table code 2025-04-04 06:07:23 -05:00
Jeff Carr d27ad541f1 new autogenpb autoformat 2025-03-26 23:36:20 -05:00
Jeff Carr 4b6ec6443e sort and append unique on this field 2025-03-23 09:30:58 -05:00
Jeff Carr 88e87f5c1a use hostname.Get() 2025-03-22 08:30:27 -05:00
Jeff Carr b2ed410276 work on setting the hostname in the patchset 2025-03-22 05:22:04 -05:00
Jeff Carr 68127e4356 now using the awesome golang 1.24 'iter' 2025-03-19 06:40:27 -05:00
Jeff Carr b4a15ef947 better syntax ideas 2025-03-18 15:14:50 -05:00
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
35 changed files with 2250 additions and 1151 deletions

6
.gitignore vendored
View File

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

View File

@ -20,12 +20,20 @@ goimports:
goimports -w *.go
clean:
rm -f *.pb.go
rm -f *.pb.go *.patch
-rm -f go.*
go-mod-clean --purge
go-mod-clean purge
forgeConfig.pb.go: forgeConfig.proto
autogenpb --proto forgeConfig.proto
patchset.pb.go: patchset.proto
autogenpb --proto patchset.proto
protoc-test:
cd ~/go/src && protoc \
--proto_path=. \
--go_out=. \
--go_opt=Mgo.wit.com/lib/protobuf/forgepb/patchset.proto=go.wit.com/lib/protobuf/forgepb \
--go_opt=Mgo.wit.com/lib/protobuf/httppb/httpRequest.proto=go.wit.com/lib/protobuf/httppb \
go.wit.com/lib/protobuf/forgepb/patchset.proto

316
branches.go Normal file
View File

@ -0,0 +1,316 @@
package forgepb
import (
"path/filepath"
"time"
"go.wit.com/lib/config"
"go.wit.com/lib/gui/shell"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
var ErrorNotAllReposOnMaster error = log.Errorf("not all repos on are on the master branch")
var ErrorNotAllReposOnDevel error = log.Errorf("not all repos on are on the devel branch")
var ErrorNotAllReposOnUser error = log.Errorf("not all repos on are on the user branch")
func (f *Forge) IsEverythingOnMaster() (int, int, int, error) {
var total int
var count int
var nope int
// first make sure every repo is on the master branch
for repo := range f.Repos.IterAll() {
total += 1
if repo.GetMasterBranchName() == repo.GetCurrentBranchName() {
count += 1
} else {
nope += 1
}
}
if total != count {
// log.Info(ErrorNotAllReposOnMaster)
return total, count, nope, ErrorNotAllReposOnMaster
}
return total, count, nope, nil
}
func (f *Forge) IsEverythingOnDevel() (int, int, int, error) {
var total int
var count int
var nope int
// first make sure every repo is on the master branch
for repo := range f.Repos.IterAll() {
total += 1
if repo.GetDevelBranchName() == repo.GetCurrentBranchName() {
count += 1
} else {
nope += 1
}
}
if total != count {
return total, count, nope, ErrorNotAllReposOnDevel
}
return total, count, nope, nil
}
func (f *Forge) IsEverythingOnUser() (int, int, int, error) {
var total int
var count int
var nope int
// first make sure every repo is on the master branch
for repo := range f.Repos.IterAll() {
total += 1
if repo.GetCurrentBranchName() == repo.GetUserBranchName() {
count += 1
} else {
nope += 1
}
}
if total != count {
return total, count, nope, ErrorNotAllReposOnUser
}
return total, count, nope, nil
}
// trys to figure out if there is still something to update
func (f *Forge) DoAllCheckoutMaster() error {
now := time.Now()
f.RillFuncError(rillCheckoutMaster)
count := f.RillReload()
if count != 0 {
config.SetChanged("repos", true)
}
total, count, nope, err := f.IsEverythingOnMaster()
log.Printf("Master branch check. %d total repos. (%d ok) (%d not on master branch) (%s)\n", total, count, nope, shell.FormatDuration(time.Since(now)))
if err != nil {
// display all repos not on master
found := new(gitpb.Repos)
all := f.Repos.SortByFullPath()
for all.Scan() {
repo := all.Next()
if repo.GetCurrentBranchName() != repo.GetMasterBranchName() {
found.Append(repo)
}
}
f.PrintHumanTable(found)
log.Printf("There are %d repos that are NOT on the master branch\n", found.Len())
return err
}
return nil
}
func rillCheckoutMaster(repo *gitpb.Repo) error {
if repo.IsDirty() {
// never do dirty repos
return nil
}
// 'giterr' means something is very wrong with this repo
if repo.GetMasterVersion() == "giterr" {
repo.CheckoutMaster()
log.Info(repo.GetFullPath(), "master == giterr. BAD REPO")
log.Info(repo.GetFullPath(), "master == giterr. BAD REPO git describe --tags master --always")
log.Info(repo.GetFullPath(), "master == giterr. BAD REPO. todo: figure this out in rillCheckoutMaster()")
// cmd := []string{"git", "checkout", "main"} // todo: figure out main
// repo.RunVerbose(cmd)
return log.Errorf("master version can not be determined")
}
if repo.GetCurrentBranchName() == repo.GetMasterBranchName() {
// repo is already on master
return nil
}
repo.CheckoutMaster()
return nil
}
func rillCheckoutUser(repo *gitpb.Repo) error {
if repo.IsDirty() {
// never do dirty repos
return nil
}
if repo.GetCurrentBranchName() == repo.GetMasterBranchName() {
// repo is already on devel branch. have to move them there first for now
// return repo.CheckoutDevel()
}
if repo.GetCurrentBranchName() == repo.GetUserBranchName() {
// repo is already on user branch
return nil
}
if err := repo.CheckoutUser(); err != nil {
log.Info(repo.GetFullPath(), err)
return err
}
return nil
}
// trys to figure out if there is still something to update
func (f *Forge) DoAllCheckoutUser(force bool) error {
now := time.Now()
if force {
log.Info("going to force create user branches")
if err := f.makeUserBranches(); err != nil {
return err
}
}
f.RillFuncError(rillCheckoutUser)
count := f.RillReload()
if count != 0 {
config.SetChanged("repos", true)
}
total, count, nope, err := f.IsEverythingOnUser()
log.Printf("User branch check. %d total repos. (%d ok) (%d not on user branch) (%s)\n", total, count, nope, shell.FormatDuration(time.Since(now)))
if err != nil {
// display all repos not on user
found := new(gitpb.Repos)
all := f.Repos.SortByFullPath()
for all.Scan() {
repo := all.Next()
if repo.GetCurrentBranchName() != repo.GetUserBranchName() {
found.Append(repo)
}
}
f.PrintHumanTable(found)
log.Printf("There are %d repos that are NOT on the user branch\n", found.Len())
return err
}
return nil
}
func (f *Forge) makeUserBranches() error {
all := f.Repos.SortByFullPath()
for all.Scan() {
repo := all.Next()
branch := repo.GetUserBranchName()
if repo.Exists(filepath.Join(".git/refs/heads", branch)) {
continue
}
if repo.Exists(filepath.Join(".git/refs/remotes/origin", branch)) {
cmd := []string{"git", "checkout", branch}
repo.RunVerbose(cmd)
continue
}
cmd := []string{"git", "branch", branch}
repo.RunVerbose(cmd)
cmd = []string{"git", "checkout", branch}
repo.RunVerbose(cmd)
}
return nil
}
func testReload(repo *gitpb.Repo) error {
if !repo.HasChanged() {
return nil
}
repo.ReloadCheck()
return log.Errorf("repo changed")
}
func (f *Forge) DoAllCheckoutDevelNew(force bool) error {
f.makeDevelBranches()
// first run git checkout
stats := f.RillFuncError(rillCheckoutDevel)
for path, stat := range stats {
dur := stat.End.Sub(stat.Start)
if dur > 1*time.Second {
log.Infof("%s git checkout took a long time (%s)\n", path, shell.FormatDuration(dur))
}
if stat.Err == nil {
// there was no error
continue
}
// didn't change to devel
}
var counter int
// recreate the repo protobuf
stats = f.RillFuncError(testReload)
for path, stat := range stats {
dur := stat.End.Sub(stat.Start)
if dur > 1*time.Second {
log.Infof("%s # Reload took a long time (%s)\n", path, shell.FormatDuration(dur))
}
if stat.Err == nil {
// repo didn't reload
continue
}
// repo reloaded
counter += 1
}
log.Info("reloaded", counter, "repos")
config.SetChanged("repos", true)
return nil
}
// is every repo on the devel branch?
func (f *Forge) DoAllCheckoutDevel(force bool) error {
now := time.Now()
if force {
log.Info("going to force create devel branches")
f.makeDevelBranches()
}
log.Info("going to rill:")
f.RillFuncError(rillCheckoutDevel)
count := f.RillReload()
if count != 0 {
config.SetChanged("repos", true)
}
total, count, nope, err := f.IsEverythingOnDevel()
log.Printf("Devel branch check. %d total repos. (%d ok) (%d not on devel branch) (%s)\n", total, count, nope, shell.FormatDuration(time.Since(now)))
if err != nil {
// display all repos not on user
found := new(gitpb.Repos)
all := f.Repos.SortByFullPath()
for all.Scan() {
repo := all.Next()
if repo.GetCurrentBranchName() != repo.GetDevelBranchName() {
found.Append(repo)
}
}
f.PrintHumanTable(found)
log.Printf("There are %d repos that are NOT on the devel branch\n", found.Len())
return err
}
return nil
}
func (f *Forge) makeDevelBranches() error {
all := f.Repos.SortByFullPath()
for all.Scan() {
repo := all.Next()
branch := repo.GetDevelBranchName()
if repo.Exists(filepath.Join(".git/refs/heads", branch)) {
continue
}
if repo.Exists(filepath.Join(".git/refs/remotes/origin", branch)) {
cmd := []string{"git", "checkout", branch}
repo.RunVerbose(cmd)
continue
}
cmd := []string{"git", "branch", branch}
repo.RunVerbose(cmd)
cmd = []string{"git", "checkout", branch}
repo.RunVerbose(cmd)
}
return nil
}
func rillCheckoutDevel(repo *gitpb.Repo) error {
if repo.IsDirty() {
// never do dirty repos
return nil
}
if repo.GetCurrentBranchName() == repo.GetDevelBranchName() {
// repo is already on devel branch
return nil
}
repo.CheckoutDevel()
return nil
}

View File

@ -58,6 +58,9 @@ func (f *Forge) doBuild(repo *gitpb.Repo, userFlags []string, goWhat string) err
data, _ := repo.ReadFile(".forge")
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
}
@ -115,7 +118,8 @@ func (f *Forge) doBuild(repo *gitpb.Repo, userFlags []string, goWhat string) err
// set standard ldflag options
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)
// add some standard golang flags
ldflags := "-X main.VERSION=" + version + " "
@ -125,9 +129,9 @@ func (f *Forge) doBuild(repo *gitpb.Repo, userFlags []string, goWhat string) err
testenv := os.Getenv("GO111MODULE")
if testenv == "off" {
log.Info("GO111MODULE=off", "f.goWork =", f.IsGoWork(), "f.gosrc =", f.GetGoSrc())
log.Info("GO111MODULE=off", "f.goWork =", f.IsGoWork())
} else {
log.Info("GO111MODULE=", testenv, "f.goWork =", f.IsGoWork(), "f.gosrc =", f.GetGoSrc())
log.Info("GO111MODULE=", testenv, "f.goWork =", f.IsGoWork())
}
log.Info("running:", repo.FullPath)
log.Info("running:", cmd)
@ -198,10 +202,28 @@ func (f *Forge) runAutogenpb(repo *gitpb.Repo) error {
return nil
}
// sortcut to find
// used by guireleaser for now
func (f *Forge) FindWorkingDirRepo() *gitpb.Repo {
pwd, _ := os.Getwd()
basedir := strings.TrimPrefix(pwd, f.GetGoSrc())
basedir := strings.TrimPrefix(pwd, f.Config.ReposDir)
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
}

162
cleanGoSum.go Normal file
View File

@ -0,0 +1,162 @@
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("%s repo go dependancy count: %d\n", 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\n", 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\n", 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\n", 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
}

View File

@ -1,3 +1,5 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
@ -27,7 +29,7 @@ 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)
fullpath := filepath.Join(f.Config.ReposDir, gopath)
if pb := f.FindAnyPath(fullpath); pb != nil {
// repo already exists
return pb, nil
@ -99,6 +101,12 @@ func overridePath(gopath string) string {
if strings.HasPrefix(gopath, "github.com/go-gl/glfw") {
return "https://github.com/go-gl/glfw"
}
if strings.HasPrefix(gopath, "cloud.google.com/go") {
return "https://github.com/googleapis/google-cloud-go"
}
if strings.HasPrefix(gopath, "go.opentelemetry.io/contrib") {
return "https://github.com/open-telemetry/opentelemetry-go-contrib"
}
return ""
}
@ -148,7 +156,7 @@ func (f *Forge) goClonePop(gopath string) (*gitpb.Repo, error) {
func (f *Forge) urlClone(gopath, giturl string) (*gitpb.Repo, error) {
var err error
fullpath := filepath.Join(f.goSrc, gopath)
fullpath := filepath.Join(f.Config.ReposDir, gopath)
basedir, newdir := filepath.Split(fullpath)
// clone the URL directly
@ -171,6 +179,10 @@ func (f *Forge) urlClone(gopath, giturl string) (*gitpb.Repo, error) {
// 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) {
return findGoImport(url)
}
func findGoImport(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
@ -212,3 +224,38 @@ func findGoImport(url string) (string, error) {
return newurl, nil
}
// GetNamesapce removes http://, https://, and .git suffix from the given URL if present.
func GetNamespace(url string) string {
// Trim protocol prefix
if strings.HasPrefix(url, "http://") {
url = strings.TrimPrefix(url, "http://")
} else if strings.HasPrefix(url, "https://") {
url = strings.TrimPrefix(url, "https://")
}
// Trim trailing .git
url = strings.TrimSuffix(url, ".git")
return url
}
func (f *Forge) Clone(url string) (*gitpb.Repo, error) {
ns := GetNamespace(url)
if ns == url {
return nil, errors.New("todo: forgepb.Clone() fix url parsing")
}
//
// returns repo if namespace already exists
if repo := f.Repos.FindByNamespace(ns); repo != nil {
log.Info("FindByNamespace() worked = ", ns)
return repo, nil
}
if repo, _ := f.urlClone(ns, url); repo != nil {
return repo, nil
}
return nil, errors.New("todo: forgepb.Clone() url failed " + url)
}

175
config.go
View File

@ -1,172 +1,35 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
// functions to import and export the protobuf
// data to and from config files
import (
"errors"
"fmt"
"os"
"path/filepath"
"go.wit.com/lib/config"
"go.wit.com/lib/gui/prep"
"go.wit.com/log"
)
// returns err1 || err2
func (f *Forge) ConfigSave() error {
var err error
// backup the current config files
if e := backupConfig(); e != nil {
log.Info("forge.BackupConfig() error", e)
err = e
// continue here? notsure. could be bad either way
// out of disk space?
if f.Config == nil {
return log.Errorf("forge.Config == nil")
}
if f.Config != nil {
if config.HasChanged("forge") {
// only let forge save the config files (?)
if prep.AppName() == "forge" || prep.AppName() == "guireleaser" {
log.Info("Okay, this is", prep.AppName())
} else {
log.Info("This is not forge")
return log.Errorf("Only forge can save the config files")
}
if e := f.Config.ConfigSave(); e != nil {
log.Info("forge.Config.ConfigSave() error", e)
err = e
}
}
if f.Repos != nil {
if e := f.Repos.ConfigSave(); e != nil {
log.Info("forge.Repos.ConfigSave() error", e)
err = e
}
}
return err
}
// write to ~/.config/forge/ unless ENV{FORGE_CONFIG} is set
func (f *ForgeConfigs) ConfigSave() error {
data, err := f.Marshal()
if err != nil {
log.Info("proto.Marshal() failed len", len(data), err)
return err
}
log.Info("forgepb.ConfigSave() proto.Marshal() worked len", len(data))
s := f.FormatTEXT()
configWrite("forge.text", []byte(s))
s = f.FormatJSON()
configWrite("forge.json", []byte(s))
return nil
}
// load the ~/.config/forge/ files
func (c *ForgeConfigs) ConfigLoad() error {
if os.Getenv("FORGE_CONFIG") == "" {
homeDir, _ := os.UserHomeDir()
fullpath := filepath.Join(homeDir, ".config/forge")
os.Setenv("FORGE_CONFIG", fullpath)
}
// var data []byte
// var err error
if c == nil {
// 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")
}
if err := c.loadText(); err == nil {
return nil
}
// forge.text doesn't exist. try forge.json
// this lets the user hand edit the JSON config
// probably just deprecate this
if data, err := loadFile("forge.json"); err != nil {
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
}
}
}
}
cpath := filepath.Join(os.Getenv("FORGE_CONFIG"), ".")
if _, err := os.Stat(cpath); err == nil {
log.Info("Something has gone wrong. Your", os.Getenv("FORGE_CONFIG"), "directory exists")
log.Info("However, the config files could not be loaded")
}
// first time user. make a template config file
c.sampleConfig()
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) {
fullname := filepath.Join(os.Getenv("FORGE_CONFIG"), filename)
data, err := os.ReadFile(fullname)
if errors.Is(err, os.ErrNotExist) {
// if file does not exist, just return nil. this
// will cause ConfigLoad() to try the next config file like "forge.text"
// because the user might want to edit the .config by hand
return nil, nil
}
if err != nil {
// log.Info("open config file :", err)
return nil, err
}
return data, nil
}
func configWrite(filename string, data []byte) error {
fullname := filepath.Join(os.Getenv("FORGE_CONFIG"), filename)
cfgfile, err := os.OpenFile(fullname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
defer cfgfile.Close()
if err != nil {
log.Warn("open config file :", err)
return err
}
if filename == "forge.text" {
// 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"))
}
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)
return nil
}

View File

@ -1,76 +0,0 @@
package forgepb
// thank chatgpt for this because why. why write this if you can have it
// kick this out in 30 seconds
import (
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"time"
)
func backupConfig() error {
// make a new dir to backup the files
now := time.Now()
// timestamp := now.Format("2022.07.18.190545") // 50yr shout out to K&R
timestamp := now.Format("2006.01.02.150405") // bummer. other date doesn't work?
srcDir := filepath.Join(os.Getenv("FORGE_CONFIG"))
destDir := filepath.Join(os.Getenv("FORGE_CONFIG"), timestamp)
return backupFiles(srcDir, destDir)
}
func backupFiles(srcDir string, destDir string) error {
// Create the destination directory
err := os.MkdirAll(destDir, os.ModePerm)
if err != nil {
return errors.New(fmt.Sprintf("Failed to create directory: %v", err))
}
// Read the contents of the source directory
entries, err := os.ReadDir(srcDir)
if err != nil {
return errors.New(fmt.Sprintf("Failed to read directory: %v", err))
}
// Iterate over the entries in the source directory
for _, entry := range entries {
// Skip directories and files that do not have the .test extension
if entry.IsDir() {
continue
}
log.Println("backing up file", entry.Name())
srcPath := filepath.Join(srcDir, entry.Name())
destPath := filepath.Join(destDir, entry.Name())
// Copy the file
if err := copyFile(srcPath, destPath); err != nil {
return errors.New(fmt.Sprintf("Failed to copy file %s: %v", entry.Name(), err))
}
}
return nil
}
// copyFile copies a file from src to dest
func copyFile(src, dest string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
destFile, err := os.Create(dest)
if err != nil {
return err
}
defer destFile.Close()
// Copy the content
_, err = io.Copy(destFile, srcFile)
return err
}

View File

@ -1,15 +0,0 @@
package forgepb
import (
"fmt"
)
func (all *ForgeConfigs) sampleConfig() {
new1 := new(ForgeConfig)
new1.GoPath = "go.wit.com"
new1.Writable = true
new1.Directory = true
all.Append(new1)
fmt.Println("first time user. adding an example config file with", len(all.ForgeConfigs), "repos")
}

99
doDirty.go Normal file
View File

@ -0,0 +1,99 @@
// Copyright 2017-2025 WIT.COM Inc. All rights reserved.
// Use of this source code is governed by the GPL 3.0
package forgepb
import (
"time"
"go.wit.com/lib/config"
"go.wit.com/lib/gui/shell"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
func (f *Forge) CheckDirtyQuiet() {
start := f.straightCheckDirty()
now := time.Now()
stats := f.RillRepos(doCheckDirty)
end := f.straightCheckDirty()
diff := end - start
var changed bool
for _, s := range stats {
if s.Err == nil {
} else {
config.SetChanged("repos", true)
changed = true
}
}
if changed {
log.Printf("dirty check (%d dirty repos) (%d total repos) (%d changed) took:%s\n", end, f.Repos.Len(), diff, shell.FormatDuration(time.Since(now)))
}
}
func (f *Forge) CheckDirty() *gitpb.Repos {
start := f.straightCheckDirty()
now := time.Now()
stats := f.RillRepos(doCheckDirty)
end := f.straightCheckDirty()
diff := end - start
log.Printf("dirty check (%d dirty repos) (%d total repos) (%d changed) took:%s\n", end, f.Repos.Len(), diff, shell.FormatDuration(time.Since(now)))
for i, s := range stats {
if s.Err == nil {
} else {
log.Info(i, s.Err)
config.SetChanged("repos", true)
}
}
return f.FindDirty()
}
func (f *Forge) straightCheckDirty() int {
var count int
for repo := range f.Repos.IterAll() {
if repo.IsDirty() {
count += 1
}
}
return count
}
func doCheckDirty(repo *gitpb.Repo) error {
// reset these in here for now
if repo.GetTargetVersion() != "" {
repo.TargetVersion = ""
}
if repo.IsDirty() {
if repo.CheckDirty() {
// nothing changed
} else {
log.Info("Repo changed to clean", repo.FullPath)
return log.Errorf("%s repo changed to clean", repo.FullPath)
}
} else {
if repo.CheckDirty() {
log.Info("Repo changed to dirty", repo.FullPath)
return log.Errorf("%s repo changed to dirty", repo.FullPath)
} else {
// nothing changed
}
}
return nil
}
func (f *Forge) FindDirty() *gitpb.Repos {
found := gitpb.NewRepos()
for repo := range f.Repos.IterByFullPath() {
if repo.IsDirty() {
found.AppendByFullPath(repo)
}
}
return found
}

View File

@ -1,6 +1,8 @@
package forgepb
import (
"errors"
"fmt"
"strings"
"go.wit.com/lib/protobuf/gitpb"
@ -16,21 +18,18 @@ import (
// 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) bool {
var good bool = true
func (f *Forge) FinalGoDepsCheckOk(check *gitpb.Repo, verbose bool) error {
if check == nil {
log.Info("boo, check == nil")
return false
return errors.New("FinalGoDepsCheckOk() boo, check == nil")
}
// parse the go.mod and go.sum files
if !check.ParseGoSum() {
log.Info("forge.FinalGoDepsCheckOk() failed")
return false
return fmt.Errorf("forge.ParseGoSum() failed. go.mod & go.sum are broken")
}
if check.GetGoPrimitive() {
return true
return nil
}
deps := check.GoDeps.SortByGoPath()
@ -42,8 +41,10 @@ func (f *Forge) FinalGoDepsCheckOk(check *gitpb.Repo, verbose bool) bool {
// skip this gopath because it's probably broken forever
continue
}
log.Info("not found:", depRepo.GetGoPath())
good = false
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())
@ -61,51 +62,94 @@ func (f *Forge) FinalGoDepsCheckOk(check *gitpb.Repo, verbose bool) bool {
// skip this gopath because it's probably broken forever
continue
} else {
log.Printf("%-48s error %10s vs %10s\n", depRepo.GetGoPath(), depRepo.GetVersion(), found.GetTargetVersion())
good = false
// 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())
}
}
}
}
if good {
log.Printf("current repo %s go dependancy count: %d\n", check.GetGoPath(), check.GoDepsLen())
}
return good
return nil
}
// TODO: this is a dumb hack & needs to be updated to talk to forged
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)
log.Info("CheckOverride() is bypassing", gopath)
return true
}
if gopath == "github.com/posener/complete/v2" {
log.Info("CheckOverride() is ignoring", gopath)
log.Info("CheckOverride() is bypassing", gopath)
return true
}
if strings.HasPrefix(gopath, "github.com/go-gl") {
log.Info("CheckOverride() is ignoring", gopath)
log.Info("CheckOverride() is bypassing", gopath)
return true
}
if strings.HasPrefix(gopath, "google.golang.org") {
log.Info("CheckOverride() is ignoring", gopath)
log.Info("CheckOverride() is bypassing", gopath)
return true
}
if strings.HasPrefix(gopath, "cloud.google.com/go") {
log.Info("CheckOverride() is bypassing", gopath)
return true
}
if strings.HasPrefix(gopath, "go.opencensus.io") {
log.Info("CheckOverride() is ignoring", gopath)
log.Info("CheckOverride() is bypassing", gopath)
return true
}
if strings.HasPrefix(gopath, "github.com/nicksnyder/go-i18n") {
log.Info("CheckOverride() is ignoring", gopath)
log.Info("CheckOverride() is bypassing", gopath)
return true
}
// fuckit for now. just blacklist github.com
if strings.HasPrefix(gopath, "github.com/") {
log.Info("CheckOverride() is ignoring", gopath)
log.Info("CheckOverride() is ignoring everything at github.com", 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

@ -18,6 +18,8 @@ package forgepb
import (
"path/filepath"
"strings"
"go.wit.com/lib/protobuf/gitpb"
)
/*
@ -116,6 +118,17 @@ func (fc *ForgeConfigs) DebName(gopath string) string {
return normalBase
}
// a work in progress
func (f *Forge) IsPrivate(repo *gitpb.Repo) bool {
namespace := repo.GetNamespace()
if namespace == "" {
// assume true
return true
}
return f.Config.IsPrivate(namespace)
}
// is this a non-publishable repo?
// matches package names from apt
//

56
forgeConfig.config.go Normal file
View File

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

View File

@ -1,3 +1,5 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
syntax = "proto3";
package forgepb;
@ -12,33 +14,61 @@ import "google/protobuf/timestamp.proto"; // Import the well-known type for Time
// package names sometimes must be different than the binary name
// for example 'zookeeper' is packaged as 'zookeeper-go'
// due to the prior apache foundation project. This happens and is ok!
message ForgeConfig { // `autogenpb:nomutex`
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 readOnly = 3; // the opposite, but needed for now because I don't know what I'm doing
bool private = 4; // if the repo can be published
bool directory = 5; // everything in this directory should use these writable & private values
bool favorite = 6; // you like this. always git clone/go clone this repo
bool interesting = 7; // this is something interesting you found and want to remember it
message ForgeConfig { // `autogenpb:nomutex`
string goPath = 1; // `autogenpb:unique` `autogenpb:sort` // Examples: 'go.wit.com/apps/go-clone' or "~/mythings" or "/home/src/foo"
string masterBranchName = 8; // git 'main' or 'master' branch name
string develBranchName = 9; // whatever the git 'devel' branch name is
string userBranchName = 10; // whatever your username branch is
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 private = 4; // if the repo can be published
bool directory = 5; // everything in this directory should use these writable & private values
bool favorite = 6; // you like this. always git clone/go clone this repo
bool interesting = 7; // this is something interesting you found and want to remember it
string debName = 11; // the actual name used with 'apt install' (or distro apt equivalent.
// 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
string masterBranchName = 8; // git 'main' or 'master' branch name
string develBranchName = 9; // whatever the git 'devel' branch name is
string userBranchName = 10; // whatever your username branch is
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
string debName = 11; // the actual name used with 'apt install' (or distro apt equivalent.
// 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
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
string namespace = 14; // `autogenpb:unique` `autogenpb:sort`
}
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 = 14; // what xterm the user wants as the default
repeated string xtermArgv = 15; // the argv line for xterm
enum ForgeMode {
MASTER = 0; // "release mode"
DEVEL = 1; // "patch mode"
USER = 2; // "work mode"
NORMAL = 3; // "normal mode" // use this when you are developing code
CLEAN = 4; // indicates "clean" was run
}
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
string goSrc = 9; // is ~/go/src unless a go.work file is found
bool pathLock = 10; // the path is locked
string ReposPB = 11; // where the repos.pb is
string ReposDir = 12; // where the repos are
string PatchDir = 13; // patch dir
string ForgeURL = 14; // forge URL
string Filename = 15; // filename of the config file
int32 rillX = 16; // used by rill
int32 rillY = 17; // used by rill
}
// this generic message is used by autogen to identify and
// then dump the uuid and version from any arbitrary .pb file
message Identify { // `autogenpb:marshal`
string uuid = 1; //
string version = 2; //
}

View File

@ -1,173 +0,0 @@
package forgepb
import (
"errors"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
func (f *Forge) GitPull() bool {
f.Repos.RillGitPull(5, 5)
/*
var localonly int
var badmap int
log.Log(WARN, "running git pull everywhere")
var failed int = 0
for all.Scan() {
repo := all.Next()
if out, err := repo.GitPull(); err == nil {
log.Log(WARN, "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(WARN, "bad unknown git error", repo.GetGoPath(), out, err)
}
}
log.Log(WARN, "Ran git pull in all repos. failure count =", failed)
log.Log(WARN, "Ran git pull in all repos. bad errors =", badmap)
if localonly != 0 {
log.Log(WARN, "Ran git pull in all repos. ignored local only branches =", localonly)
}
*/
return true
}
func (f *Forge) CheckoutDevel() bool {
log.Log(WARN, "running git checkout devel everwhere")
var failed int = 0
var count int = 0
all := f.Repos.SortByFullPath()
for all.Scan() {
repo := all.Next()
count += 1
if repo.CheckoutDevel() {
// checkout ok
} else {
dname := repo.GetDevelBranchName()
if err := f.makeBranch(repo, dname); err != nil {
log.Info(repo.GetGoPath(), "can not make devel branch", dname)
failed += 1
}
}
}
log.Log(WARN, "Ran git checkout in", count, "repos. failure count =", failed)
return true
}
func (f *Forge) MakeDevelBranch(repo *gitpb.Repo) error {
dname := repo.GetDevelBranchName()
if dname == "" {
dname = f.configDevelBranchName(repo)
}
if err := f.makeBranch(repo, dname); err != nil {
return err
}
repo.DevelBranchName = dname
return nil
}
func (f *Forge) MakeUserBranch(repo *gitpb.Repo) error {
uname := repo.GetUserBranchName()
if uname == "" {
uname = f.configUserBranchName(repo)
}
if err := f.makeBranch(repo, uname); err != nil {
return err
}
repo.UserBranchName = uname
return nil
}
func (f *Forge) makeBranch(repo *gitpb.Repo, bname string) error {
if repo.IsLocalBranch(bname) {
// branch already exists in refs/heads/
return nil
}
if repo.IsBranch(bname) {
// branch already exists refs/remotes/
return nil
} else {
log.Info("makeBranch() says", bname, "does not exist")
loop := repo.Tags.All()
for loop.Scan() {
t := loop.Next()
log.Info("LocalTagExists() tag:", t.Refname)
}
}
mname := repo.GetMasterBranchName()
cname := repo.GetCurrentBranchName()
if mname != cname {
return errors.New("can only make branches from master branch")
}
cmd := []string{"git", "branch", bname}
if err := repo.StrictRun(cmd); err != nil {
return err
}
return nil
}
func (f *Forge) CheckoutMaster() bool {
log.Log(WARN, "running git checkout master everwhere")
var failed int = 0
var count int = 0
all := f.Repos.SortByFullPath()
for all.Scan() {
repo := all.Next()
count += 1
if repo.CheckoutMaster() {
// checkout ok
} else {
failed += 1
}
}
log.Log(WARN, "Ran git checkout in", count, "repos. failure count =", failed)
return true
}
func (f *Forge) CheckoutUser() bool {
log.Log(WARN, "running git checkout user everwhere")
var failed int = 0
var count int = 0
all := f.Repos.SortByFullPath()
for all.Scan() {
repo := all.Next()
if repo.GetCurrentBranchName() == repo.GetUserBranchName() {
// already on the user branch
continue
}
count += 1
if repo.CheckoutUser() {
// checkout ok
} else {
failed += 1
}
}
log.Log(WARN, "Ran git checkout in", count, "repos. failure count =", failed)
return true
}
func (f *Forge) CheckoutUserForce() bool {
log.Log(WARN, "running git checkout user everwhere")
var failed int = 0
var count int = 0
all := f.Repos.SortByFullPath()
for all.Scan() {
repo := all.Next()
count += 1
if repo.CheckoutUser() {
// checkout ok
} else {
failed += 1
}
}
log.Log(WARN, "Ran git checkout in", count, "repos. failure count =", failed)
return true
}

View File

@ -84,6 +84,10 @@ type Origin struct {
// A Tags describes the available tags in a code repository.
func RunGoList(url string) (string, error) {
return runGoList(url)
}
func runGoList(url string) (string, error) {
ver, err := getLatestVersion(url)
if err != nil {

View File

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

View File

@ -1,203 +0,0 @@
package forgepb
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/destel/rill"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
func (f *Forge) ScanGoSrc() (bool, error) {
dirs, err := gitDirectoriesNew(f.goSrc)
if err != nil {
return false, err
}
var gopaths []string
for _, dir := range dirs {
// log.Info("forge.ScanGoSrc()", dir)
if strings.HasPrefix(dir, f.goSrc) {
gopath := strings.TrimPrefix(dir, f.goSrc)
gopath = strings.Trim(gopath, "/")
if r := f.FindByGoPath(gopath); r != nil {
// log.Info("already have", gopath)
continue
}
gopaths = append(gopaths, gopath)
} else {
log.Log(WARN, "ScanGoSrc() bad:", dir)
return false, errors.New("forgepb.ScanGoSrc() bad dir: " + dir)
}
}
newcount, err := f.rillScanDirs(gopaths)
if err != nil {
log.Info("go src dir problem. exit for now?", err)
return false, err
}
if newcount != 0 {
log.Info("forge go src scan found", newcount, "repos")
f.SetConfigSave(true)
}
return true, err
}
// doesn't enter the directory any further when it finds a .git/
// not stupid like my old version
func gitDirectoriesNew(srcDir string) ([]string, error) {
var all []string
var trip bool
err := filepath.WalkDir(srcDir, func(path string, d os.DirEntry, err error) error {
if err != nil {
// Handle possible errors, like permission issues
fmt.Fprintf(os.Stderr, "error accessing path %q: %v\n", path, err)
return err
}
if d.IsDir() {
// log.Info("path is dir", path)
} else {
_, fname := filepath.Split(path)
switch fname {
case "repos.pb":
case "go.work":
case "go.work.last":
default:
// todo: figure out a way to do padding for init()
if trip == false {
log.Info("WARNING:")
}
log.Info("WARNING: you have an untracked file outside of any .git repository:", path)
trip = true
}
return nil
}
gitdir := filepath.Join(path, ".git")
_, err2 := os.Stat(gitdir)
if !os.IsNotExist(err2) {
all = append(all, path)
return filepath.SkipDir
}
return nil
})
//
// probably always leave this here forever
// this check, along with CheckDirty() makes sure you can safely delete ~/go/src or the go.work directory
// because everything is either checked in or deleted. An important thing to know!
if trip {
log.Info("WARNING:")
log.Info("WARNING: there isn't a way to disable this warning yet")
log.Info("WARNING: probably this is a good thing however. you don't want to leave files outside of git repos here")
log.Info("WARNING: so this warning should probably stay")
log.Info("WARNING:")
log.Info("WARNING: this also might mean you put these files here because you are actively working on them")
log.Info("WARNING: and you don't want to forget about them")
log.Info("WARNING:")
}
return all, err
}
func gitDirectoriesOld(srcDir string) ([]string, error) {
var all []string
err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Log(WARN, "Error accessing path:", path, err)
return nil
}
// Check if the path is a directory and has a .git subdirectory
if info.IsDir() && IsGitDir(path) {
all = append(all, path)
}
return nil
})
if err != nil {
log.Log(WARN, "Error walking the path:", srcDir, err)
}
return all, err
}
// IsGitDir checks if a .git directory exists inside the given directory
func IsGitDir(dir string) bool {
gitDir := filepath.Join(dir, ".git")
info, err := os.Stat(gitDir)
if os.IsNotExist(err) {
return false
}
return info.IsDir()
}
// rill is awesome. long live rill
// attempt scan with rill
func (f *Forge) rillScanDirs(gopaths []string) (int, error) {
// Convert a slice of user IDs into a channel
ids := rill.FromSlice(gopaths, nil)
// Read users from the API.
// Concurrency = 20
dirs := rill.Map(ids, 20, func(id string) (*gitpb.Repo, error) {
return f.checkpath(id, "")
})
var counter int
// Activate users.
// Concurrency = 10
err := rill.ForEach(dirs, 10, func(repo *gitpb.Repo) error {
counter += 1
return nil
})
return counter, err
}
func (f *Forge) checkpath(gopath string, url string) (*gitpb.Repo, error) {
fullpath := filepath.Join(f.GetGoSrc(), gopath)
log.Info("checkpath()", gopath, fullpath)
repo, err := f.NewGoRepo(gopath, "")
if err != nil {
log.Info("checkpath()", gopath, err)
}
return repo, err
}
func (f *Forge) RillRedoGoMod() int {
var all []*gitpb.Repo
tmp := f.Repos.SortByFullPath()
for tmp.Scan() {
repo := tmp.Next()
if !repo.IsValidDir() {
log.Printf("%10s %-50s", "why am I in RillRedoGoMod? 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 {
log.Info("rill.ForEach() error:", err)
}
return counter
}

View File

@ -1,3 +1,5 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
@ -16,7 +18,7 @@ func (f *Forge) MakeGoWork() error {
// 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")
filename := filepath.Join(f.Config.ReposDir, "go.work")
workf, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
@ -30,26 +32,10 @@ func (f *Forge) MakeGoWork() error {
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

161
http.go Normal file
View File

@ -0,0 +1,161 @@
// Copyright 1994-2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
"bytes"
"io/ioutil"
"net"
"net/http"
"net/url"
"os/user"
"strings"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
func (f *Forge) HttpPost(base string, route string, data []byte) ([]byte, error) {
// Fix using url.JoinPath (Best Practice)
baseURL, _ := url.Parse(f.Config.ForgeURL) // "http://forge.grid.wit.com:2520")
finalURL := baseURL.JoinPath(route) // Correctly produces ...:2520/patches
var err error
var req *http.Request
req, err = http.NewRequest(http.MethodPost, finalURL.String(), bytes.NewBuffer(data))
if req == nil {
return nil, err
}
usr, _ := user.Current()
req.Header.Set("author", usr.Username)
req.Header.Set("hostname", f.hostname)
return rawHttpPost(req)
}
func rawHttpPost(req *http.Request) ([]byte, error) {
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return []byte("client.Do(req) error"), err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return body, err
}
return body, nil
}
func (f *Forge) LookupPBorig(check *gitpb.Repos) (*gitpb.Repos, error) {
url := f.Config.ForgeURL + "lookup"
for repo := range check.IterByFullPath() {
if repo.Namespace == "" {
repo.Namespace = repo.GoInfo.GoPath
}
}
return check.SubmitReposPB(url)
}
/*
func (f *Forge) LookupPB(check *gitpb.Repos) (*gitpb.Repos, error) {
url := f.forgeURL + "lookup"
queryPB := gitpb.NewRepos()
for repo := range check.IterByFullPath() {
ns := repo.Namespace
if ns == "" {
ns = repo.GoInfo.GoPath
}
newr := new(gitpb.Repo)
newr.Namespace = ns
queryPB.AppendByNamespace(newr)
}
return queryPB.SubmitReposPB(url)
}
func (f *Forge) UpdatePB(check *gitpb.Repos) (*gitpb.Repos, error) {
url := f.forgeURL + "update"
queryPB := gitpb.NewRepos()
for repo := range check.IterByFullPath() {
ns := repo.Namespace
if ns == "" {
ns = repo.GoInfo.GoPath
}
newr := new(gitpb.Repo)
newr.Namespace = ns
queryPB.AppendByNamespace(newr)
}
return queryPB.SubmitReposPB(url)
}
*/
/*
// HTTPRequestToProto converts an *http.Request to our custom HttpRequest protobuf message.
func (pb *Patches) AddHttpToPB(r *http.Request) error {
if pb == nil {
return log.Errorf("AddHttpToPB() pb was nil")
}
// Convert the header map. http.Header is a map[string][]string.
// We'll just take the first value for each header for simplicity.
headers := make(map[string]string)
for name, values := range r.Header {
if len(values) > 0 {
headers[name] = strings.Join(values, "\n")
}
}
pb.HttpRequest = &httppb.HttpRequest{
Method: r.Method,
URL: r.URL.String(),
Proto: r.Proto,
Headers: headers,
IP: getClientIP(r),
Host: r.Host,
Hostname: r.Header.Get("hostname"),
}
// pb.HttpRequest.Route = cleanURL(r.URL.Path)
return nil
}
*/
func getIpSimple(r *http.Request) string {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
log.Printf("could not split host port: %v", err)
return r.RemoteAddr // Fallback
}
return host
}
// getClientIP inspects the request for common headers to find the true client IP.
func getClientIP(r *http.Request) string {
// Caddy sets the X-Forwarded-For header.
if forwardedFor := r.Header.Get("X-Forwarded-For"); forwardedFor != "" {
// The header can be a comma-separated list of IPs. The first one is the original client.
ips := strings.Split(forwardedFor, ",")
return strings.TrimSpace(ips[0])
}
// Fallback to RemoteAddr if the header is not present.
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}

View File

@ -1,3 +1,5 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (

View File

@ -1,3 +1,5 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
@ -59,14 +61,7 @@ func (f *Forge) HumanPrintRepo(check *gitpb.Repo) {
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)
// f.PrintHumanTable(check)
printTime("Last Pull", check.Times.LastPull.AsTime())
printTime("Last Dirty", check.Times.LastDirty.AsTime())
@ -86,7 +81,7 @@ func (f *Forge) testGoRepo(check *gitpb.Repo) {
data, _ := os.ReadFile(filepath.Join(check.FullPath, "go.mod"))
log.Info(string(data))
if f.FinalGoDepsCheckOk(check, true) {
if err := f.FinalGoDepsCheckOk(check, true); err == nil {
log.Info("forge.FinalGoDepsCheck(check) worked!")
} else {
log.Info("forge.FinalGoDepsCheck(check) failed. boo.")

View File

@ -1,186 +1,208 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
"fmt"
"path/filepath"
"go.wit.com/lib/cobol"
"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!
// a console with a fixed with font. AKA: a typerwriter
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(standardTable8("repopath", "cur br", "age", "target", "master", "devel", "user", "curver", "repo type"))
t := new(tally)
// print the header
args := []string{"Namespace", "branch", "age", "user", "devel", "master", "", "lasttag", "next", "repo type"}
sizes := []int{35, 9, 4, 13, 13, 13, 1, 12, 12, 8}
log.Info(cobol.TerminalChomp(cobol.StandardTableSize10(sizes, args)))
all := allr.SortByFullPath()
for all.Scan() {
repo := all.Next()
f.printRepoToTable(repo)
count += 1
f.printRepoToTable(repo, sizes, false)
tallyBranchTotals(t, repo)
}
log.Info("Total git repositories:", count)
log.Infof("Total repositories: %d (%d master) (%d devel) (%d user) (%d unknown)\n", t.total, t.master, t.devel, t.user, t.unknown)
}
func (f *Forge) PrintForgedTable(allr *gitpb.Repos) {
log.DaemonMode(true)
t := new(tally)
// print the header
args := []string{"Namespace", "branch", "age", "master", "devel", "last tag", "", "", "", ""}
sizes := []int{35, 9, 4, 13, 13, 13, 13, 12, 12, 8}
log.Info(cobol.TerminalChomp(cobol.StandardTableSize10(sizes, args)))
all := allr.SortByFullPath()
for all.Scan() {
repo := all.Next()
f.printForgedToTable(repo, sizes)
tallyBranchTotals(t, repo)
}
log.Infof("Total repositories: %d (%d user) (%d devel) (%d master) (%d unknown)\n", t.total, t.user, t.devel, t.master, t.unknown)
}
func (f *Forge) PrintHumanTableFull(allr *gitpb.Repos) {
log.DaemonMode(true)
t := new(tally)
// print the header
args := []string{"branch", "age", "user", "devel", "master", "", "lasttag", "next", "repo type", "Namespace"}
sizes := []int{9, 4, 13, 13, 13, 1, 12, 12, 8, 0}
log.Info(cobol.TerminalChomp(cobol.StandardTableSize10(sizes, args)))
all := allr.SortByFullPath()
for all.Scan() {
repo := all.Next()
f.printRepoToTable(repo, sizes, true)
tallyBranchTotals(t, repo)
}
log.Infof("Total repositories: %d (%d user) (%d devel) (%d master) (%d unknown)\n", t.total, t.user, t.devel, t.master, t.unknown)
}
// 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(standardTable8("repopath", "cur br", "age", "target", "master", "devel", "user", "curver", "repo type"))
all := allr.SortByFullPath()
for all.Scan() {
repo := all.Next()
f.printRepoToTable(repo)
t := new(tally)
// print the header
args := []string{"Namespace", "branch", "age", "user", "devel", "master", "", "lasttag", "next", "repo type"}
sizes := []int{35, 9, 4, 13, 13, 13, 1, 12, 12, 8}
log.Info(cobol.TerminalChomp(cobol.StandardTableSize10(sizes, args)))
for repo := range allr.IterAll() {
f.printRepoToTable(repo, sizes, false)
if len(repo.DirtyList) != 0 {
for _, line := range repo.DirtyList {
log.Info("\t", line)
log.Info(cobol.TerminalChomp("\t" + line))
}
}
var mver string = repo.GetMasterVersion()
repo.Tags.GetAge(mver)
count += 1
tallyBranchTotals(t, repo)
}
log.Info("Total git repositories:", count)
log.Infof("Total repositories: %d (%d user) (%d devel) (%d master) (%d unknown)\n", t.total, t.user, t.devel, t.master, t.unknown)
}
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)
// used to count which repos are on which branches (master/main, devel, user)
type tally struct {
total int
master int
devel int
user int
unknown int
}
func standardTable8(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 string) string {
len1 := 40
len2 := 12
len3 := 6
len4 := 12
len5 := 16
len6 := 16
len7 := 16
len8 := 16
len9 := 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 "
func tallyBranchTotals(t *tally, repo *gitpb.Repo) {
t.total += 1
if len(arg5) > len5 {
arg5 = arg5[:len5]
if repo.GetCurrentBranchName() == repo.GetMasterBranchName() {
t.master += 1
return
}
s += "%-" + fmt.Sprintf("%d", len5) + "s "
if len(arg6) > len6 {
arg6 = arg6[:len6]
if repo.GetCurrentBranchName() == repo.GetDevelBranchName() {
t.devel += 1
return
}
s += "%-" + fmt.Sprintf("%d", len6) + "s "
if len(arg7) > len7 {
arg7 = arg7[:len7]
if repo.GetCurrentBranchName() == repo.GetUserBranchName() {
t.user += 1
return
}
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 "
return fmt.Sprintf(s, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
log.Printf("unknown curr=%s user=%s devel=%s master=%s\n", repo.GetCurrentBranchName(), repo.GetUserBranchName(), repo.GetDevelBranchName(), repo.GetMasterBranchName())
t.unknown += 1
}
func (f *Forge) printRepoToTable(repo *gitpb.Repo) {
func (f *Forge) printRepoToTable(repo *gitpb.Repo, sizes []int, full bool) {
var end string
if repo.IsDirty() {
end += "(dirty) "
}
// shortened version numbers
var mhort string = repo.GetMasterVersion()
var dhort string = repo.GetDevelVersion()
var uhort string = repo.GetUserVersion()
var uver string = repo.GetUserVersion()
if uver == "uerr" {
// blank these out
uver = ""
}
var lasttag string = repo.GetLastTag()
var thort string = repo.GetTargetVersion()
var chort string = repo.GetCurrentBranchVersion()
var chort string = "" // repo.GetCurrentBranchVersion()
var cname string = repo.GetCurrentBranchName()
var gopath string = repo.GetGoPath()
var gopath string = repo.GetNamespace()
if gopath == "" {
gopath = repo.GetFullPath()
}
var rtype string = repo.GetRepoType()
switch rtype {
case "binary":
rtype = "GO bin"
case "library":
rtype = "GO lib"
case "protobuf":
rtype = "GO pb"
}
if f.IsPrivate(repo) {
rtype = "priv"
}
// ctime := repo.Tags.GetAge(mhort)
// age := shell.FormatDuration(time.Since(ctime))
age := shell.FormatDuration(repo.NewestAge())
start := standardTable8(gopath, cname, age, thort, mhort, dhort, uhort, chort, rtype)
age := shell.FormatDuration(repo.BranchAge(cname))
if f.Config.IsReadOnly(repo.GetGoPath()) {
end += "(readonly) "
// end += "(readonly) "
} else {
end += "(rw) "
}
if repo.IsDirty() {
age = ""
uver = "* " + uver
end += "(dirty) "
}
var args []string
if full {
args = []string{cname, age, uver, dhort, mhort, chort, lasttag, thort, rtype, gopath}
} else {
args = []string{gopath, cname, age, uver, dhort, mhort, chort, lasttag, thort, rtype}
}
start := cobol.StandardTableSize10(sizes, args)
if rtype == "protobuf" {
if repo.GoInfo.GoBinary {
end += "(binary) "
}
}
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() + ") "
}
if repo.GetMasterBranchName() != "master" && repo.GetMasterBranchName() != "main" {
end += "(m:" + repo.GetMasterBranchName() + ") "
}
@ -193,13 +215,95 @@ func (f *Forge) printRepoToTable(repo *gitpb.Repo) {
end += "(u:" + repo.GetUserBranchName() + ") "
}
switch repo.GetState() {
case "PERFECT":
case "unchanged":
// end += "(invalid tag) "
default:
end += "(" + repo.GetState() + ") "
debname := f.Config.DebName(repo.GetNamespace())
if debname != filepath.Base(gopath) {
end += "(deb:" + debname + ") "
}
log.Info(start, end)
log.Info(cobol.TerminalChomp(start + " " + end))
}
func (f *Forge) printForgedToTable(repo *gitpb.Repo, sizes []int) {
var end string
// shortened version numbers
var mhort string = repo.GetMasterVersion()
var dhort string = repo.GetDevelVersion()
var lasttag string = repo.GetLastTag()
var cname string = repo.GetCurrentBranchName()
var ns string = repo.GetNamespace()
age := shell.FormatDuration(repo.BranchAge(cname))
var args []string
args = []string{ns, cname, age, mhort, dhort, lasttag, "", "", "", ""}
start := cobol.StandardTableSize10(sizes, args)
end += "todo"
log.Info(cobol.TerminalChomp(start + " " + end))
}
func (psets *Patchsets) PrintTable() {
if psets == nil {
return
}
log.DaemonMode(true)
// print the header
args := []string{"commit hash", "new hash", "", "", "name", "Repo Namespace", "", "", "", "", ""}
sizes := []int{12, 12, 3, 3, 40, 80, 2, 2, 2, 2}
log.Info(cobol.TerminalChomp(cobol.StandardTableSize10(sizes, args)))
var countCONTENTS int
var countPARTS int
for x, pset := range psets.GetPatchsets() {
log.Info(pset.Uuid, pset.Name, pset.State)
if pset.State == "DONE" {
// old patchset
continue
}
cId := log.Sprintf("%d", x)
countCONTENTS += 1
for i, p := range pset.Patches.GetPatches() {
var args []string
partId := log.Sprintf("%d", i)
_, fname := filepath.Split(p.GetFilename())
args = []string{p.CommitHash, p.NewHash, cId, partId, fname, p.GetNamespace(), "", "", "", "", ""}
start := cobol.StandardTableSize10(sizes, args)
log.Info(cobol.TerminalChomp(start))
countPARTS += 1
}
}
log.Infof("Total Contents (%d) Parts (%d)\n", countCONTENTS, countPARTS)
}
func (patches *Patches) PrintTable() {
if patches == nil {
return
}
log.DaemonMode(true)
// print the header
args := []string{"commit hash", "new hash", "", "", "name", "Repo Namespace", "", "", "", "", ""}
sizes := []int{12, 12, 3, 3, 40, 80, 2, 2, 2, 2}
log.Info(cobol.TerminalChomp(cobol.StandardTableSize10(sizes, args)))
var countPARTS int
for x, p := range patches.GetPatches() {
var args []string
partId := log.Sprintf("%d", x)
_, fname := filepath.Split(p.GetFilename())
args = []string{p.CommitHash, p.NewHash, partId, fname, p.GetNamespace(), "", "", "", "", ""}
start := cobol.StandardTableSize10(sizes, args)
log.Info(cobol.TerminalChomp(start))
countPARTS += 1
}
log.Infof("Total Patches (%d)\n", countPARTS)
}

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
}

254
init.go
View File

@ -1,132 +1,192 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
"os"
"os/user"
"path/filepath"
"time"
"go.wit.com/lib/gui/shell"
"go.wit.com/lib/config"
"go.wit.com/lib/fhelp"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/lib/protobuf/zoopb"
"go.wit.com/log"
)
// todo: use initOnce
// cache.go has Do()
// f.initOnce.Do(f.initWork)
/* better syntax from gin
Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default(opts ...OptionFunc) *Engine {
engine := New()
engine.Use(Logger(), Recovery())
return engine.With(opts...)
}
*/
func Init() *Forge {
f := InitPB()
f.Machine = new(zoopb.Machine)
if err := f.Machine.ConfigLoad(); err != nil {
log.Log(WARN, "zoopb.ConfigLoad() failed", err)
cfg := new(ForgeConfigs)
err := config.ConfigLoad(cfg, "forge", "forge")
if err != nil {
log.Info("forge has not been configured yet filename =", cfg.Filename)
log.Info("go install go.wit.com/apps/forge@latest")
os.Exit(-1)
}
if f.Config.Username == "" {
usr, _ := user.Current()
f.Config.Username = usr.Username
f.SetConfigSave(true)
f := initFromConfig(cfg)
if f.Config.Mode == ForgeMode_MASTER {
log.Printf("forge.Init() %s len()=%d\n", f.Config.Filename, f.Repos.Len())
fhelp.DumpENV("finit:")
f.Config.DumpENV()
}
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()
now := time.Now()
start := f.Repos.Len()
f.ScanGoSrc()
end := f.Repos.Len()
if (end - start) == 0 {
log.Log(INFO, "forgepb.Scan() Scan did not find new git repositories. Total =", end)
} 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 {
func InitByAppname(argname string) *Forge {
cfg := new(ForgeConfigs)
err := config.ConfigLoad(cfg, argname, "forge")
if err != nil {
log.Info("forge has not been configured yet", cfg.Filename)
log.Info("go install go.wit.com/apps/forge@latest")
os.Exit(-1)
}
f := initFromConfig(cfg)
log.Printf("forge.Init() %s len()=%d\n", f.Config.Filename, f.Repos.Len())
return f
}
func initFromConfig(cfg *ForgeConfigs) *Forge {
f := new(Forge)
// TODO: rethink this but it works for now
gosrc := os.Getenv("FORGE_GOSRC")
if gosrc == "" {
goSrcDir, err := f.findGoSrc()
if err != nil {
log.Log(WARN, "forge init() findGoSrc()", err)
}
os.Setenv("FORGE_GOSRC", goSrcDir)
f.Config = cfg
if f.configENV() {
log.Info("ENV changed config. todo: save config here")
f.Config.ConfigSave()
}
f.goSrc = os.Getenv("FORGE_GOSRC")
// also rethink this, but maybe this is the right thing to do
if os.Getenv("FORGE_CONFIG") == "" {
homeDir, _ := os.UserHomeDir()
fullpath := filepath.Join(homeDir, ".config/forge")
os.Setenv("FORGE_CONFIG", fullpath)
if f.Config.ReposPB != os.Getenv("FORGE_REPOSPB") {
// if different, use the ENV var
// this probably means that it gets saved as the default in the config
// we probably want that (?)
f.Config.ReposPB = os.Getenv("FORGE_REPOSPB")
}
if _, s := filepath.Split(f.Config.ReposPB); s != "repos.pb" {
fhelp.DumpENV("forge:")
f.Config.DumpENV()
log.Infof("ReposPB invalid filename '%s'\n", f.Config.ReposPB)
os.Exit(-1)
}
// check again for go.work // user could have a go.work file in ~/go/src
if f.goWorkExists() {
f.goWork = true
}
f.Repos = gitpb.NewRepos()
f.Repos.ConfigLoad(f.Config.ReposPB)
// print out the settings that will be used
log.Log(INFO, "forgepb.Init() FORGE_CONFIG", os.Getenv("FORGE_CONFIG"))
// init the Patchsets
f.Patchsets = NewPatchsets()
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 = new(gitpb.Repos)
f.Repos.ConfigLoad()
}
// only init's the protobuf. intended to not scan or change anything
func InitPB() *Forge {
f := DetermineGoPath()
f.InitPB()
// todo: play with these / determine good values based on user's machine
f.Config.RillX = 10
f.Config.RillY = 20
return f
}
func (f *Forge) SetConfigSave(b bool) {
f.configSave = b
config.SetChanged("forge", b)
}
// saves the config if there have been changes
func (f *Forge) Exit() {
// log.Info("forge.configSave =", f.configSave)
if f.configSave {
f.ConfigSave()
if f.Config.Mode == ForgeMode_MASTER {
fhelp.DumpENV("forge:")
f.Config.DumpENV()
}
f.ConfigSave()
if f.Repos != nil {
if config.HasChanged("repos") {
if err := f.Repos.ConfigSave(f.Config.ReposPB); err != nil {
log.Info("forge.Repos.ConfigSave() error", err)
}
}
}
// log.Info("forge.Exit() ok")
os.Exit(0)
}
// the first thing done is process any ENV settings
// try to NOT use the ENV settings anywhere but here
// all initial ENV settings should be stored in the forge struct
func (f *Forge) setenv() {
f.once.Do(func() {
if err := fhelp.ConfigureENV(); err != nil {
log.Info("forge ConfigureENV() failed", err)
os.Exit(-1)
}
if f.Config == nil {
log.Info("forge.Config() was nil")
os.Exit(-1)
}
// f.forgeURL = os.Getenv("FORGE_URL")
f.hostname = os.Getenv("HOSTNAME")
if os.Getenv("FORGE_GOWORK") == "true" {
f.goWork = true
}
f.Config.ReposPB = os.Getenv("FORGE_REPOPB")
f.Config.PatchDir = os.Getenv("FORGE_PATCHDIR")
f.Config.ForgeURL = os.Getenv("FORGE_URL")
})
}
func (f *Forge) GetForgeURL() string {
return f.Config.ForgeURL
}
// set the URL for forge otherwise fallback to ENV or to forge.wit.com
func (f *Forge) SetForgeURL(url string) {
if url == "" {
url = os.Getenv("FORGE_URL")
}
if url == "" {
url = "https://forge.wit.com/"
}
f.Config.ForgeURL = url
os.Setenv("FORGE_URL", f.Config.ForgeURL)
log.Info("Forge URL has been set to", f.Config.ForgeURL)
}
// the first thing done is process any ENV settings
// try to NOT use the ENV settings anywhere but here
// all initial ENV settings should be stored in the forge struct
func (f *Forge) configENV() bool {
var changed bool
f.once.Do(func() {
if err := fhelp.ConfigureENV(); err != nil {
log.Info("forge ConfigureENV() failed", err)
os.Exit(-1)
}
if os.Getenv("FORGE_REPOPB") != "" && f.Config.ReposPB != os.Getenv("FORGE_REPOPB") {
log.Info("ENV: updating FORGE_REPOSPB from", f.Config.ReposPB, "to", os.Getenv("FORGE_REPOPB"))
f.Config.ReposPB = os.Getenv("FORGE_REPOPB")
changed = true
}
if os.Getenv("FORGE_GOSRC") != "" && f.Config.ReposDir != os.Getenv("FORGE_GOSRC") {
log.Info("ENV: updating FORGE_GOSRC from", f.Config.ReposDir, "to", os.Getenv("FORGE_GOSRC"))
f.Config.ReposDir = os.Getenv("FORGE_GOSRC")
changed = true
}
if os.Getenv("FORGE_PATCHDIR") != "" && f.Config.PatchDir != os.Getenv("FORGE_PATCHDIR") {
log.Info("ENV: updating FORGE_PATCHDIR from", f.Config.PatchDir, "to", os.Getenv("FORGE_PATCHDIRC"))
f.Config.PatchDir = os.Getenv("FORGE_PATCHDIR")
changed = true
}
f.Config.ForgeURL = os.Getenv("FORGE_URL")
f.hostname = os.Getenv("HOSTNAME")
if os.Getenv("FORGE_GOWORK") == "true" {
f.goWork = true
}
})
if changed {
// save config here
}
return changed
}

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()
}
}

View File

@ -6,13 +6,50 @@ import (
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/lib/protobuf/httppb"
"go.wit.com/log"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
)
func (f *Forge) MakeDevelPatchSet() (*Patchset, error) {
func (p *Patches) HttpPostVerbose(baseURL string, route string) (*Patches, *httppb.HttpRequest, error) {
p.PrintTable()
return p.HttpPost(baseURL, route)
}
func (p *Patchsets) HttpPostVerbose(baseURL string, route string) (*Patchsets, *httppb.HttpRequest, error) {
p.PrintTable()
return p.HttpPost(baseURL, route)
}
func newPatchset(name string) *Patchset {
pset := new(Patchset)
pset.Name = name
pset.Ctime = timestamppb.New(time.Now())
pset.Uuid = uuid.New().String()
pset.Hostname, _ = os.Hostname()
return pset
}
// creates a patchset
// works from the user branches against the devel branches
func (f *Forge) MakeDevelPatchSet(name string) (*Patchset, error) {
pset := newPatchset(name)
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
@ -23,17 +60,21 @@ func (f *Forge) MakeDevelPatchSet() (*Patchset, error) {
all := f.Repos.SortByFullPath()
for all.Scan() {
repo := all.Next()
userb := repo.GetUserBranchName()
develb := repo.GetDevelBranchName()
if develb == "" {
if !repo.IsLocalBranch(repo.GetUserBranchName()) {
// log.Info("repo doesn't have user branch", repo.GetGoPath())
continue
}
if userb == "" {
if !repo.IsLocalBranch(repo.GetDevelBranchName()) {
// log.Info("repo doesn't have devel branch", repo.GetGoPath())
continue
}
pset.StartBranchName = develb
pset.EndBranchName = userb
// 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
@ -42,8 +83,9 @@ func (f *Forge) MakeDevelPatchSet() (*Patchset, error) {
return pset, nil
}
/*
func (f *Forge) MakeMasterPatchSet() (*Patchset, error) {
pset := new(Patchset)
pset := newPatchset("masterBranchPS")
dir, err := os.MkdirTemp("", "forge")
if err != nil {
return nil, err
@ -73,6 +115,7 @@ func (f *Forge) MakeMasterPatchSet() (*Patchset, error) {
}
return pset, nil
}
*/
func (pset *Patchset) makePatchSetNew(repo *gitpb.Repo) error {
startBranch := pset.StartBranchName
@ -83,7 +126,19 @@ func (pset *Patchset) makePatchSetNew(repo *gitpb.Repo) error {
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 {
@ -103,14 +158,16 @@ func (pset *Patchset) makePatchSetNew(repo *gitpb.Repo) error {
return nil
}
return pset.addPatchFiles(repo)
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)
// 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 {
@ -124,10 +181,10 @@ func (p *Patchset) addPatchFiles(repo *gitpb.Repo) error {
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)
// 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)
@ -138,9 +195,15 @@ func (p *Patchset) addPatchFiles(repo *gitpb.Repo) error {
patch.Filename, _ = filepath.Rel(p.TmpDir, path)
patch.Data = data
patch.parseData()
patch.StartHash = repo.DevelHash()
patch.StartHash = repo.ActualDevelHash()
patch.NewHash = "na"
patch.Namespace = repo.GetGoPath()
if p.Patches == nil {
p.Patches = new(Patches)
}
p.Patches.Append(patch)
log.Info("ADDED PATCH FILE", path)
p.Patches.Uuid = uuid.New().String()
// log.Info("ADDED PATCH FILE", path)
return nil
})
return baderr
@ -159,6 +222,8 @@ func (p *Patch) parseData() string {
switch fields[0] {
case "From":
p.CommitHash = fields[1]
case "Subject:":
p.Comment = line
case "diff":
p.Files = append(p.Files, line)
}

View File

@ -1,27 +0,0 @@
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")
}

224
patchset.config.go Normal file
View File

@ -0,0 +1,224 @@
package forgepb
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/google/uuid"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
"google.golang.org/protobuf/proto"
)
func (f *Forge) LoadPatchsets() error {
f.Patchsets = NewPatchsets()
filename := filepath.Join(f.Config.PatchDir, "all-patches.pb")
data, err := os.ReadFile(filename)
if err != nil {
return err
}
err = f.Patchsets.Unmarshal(data)
if err != nil {
log.Infof("LoadPatchsets() proto.Marshal() error %v\n", err)
return err
}
// log.Infof("LoadPatchsets() found %d patches.\n", f.Patchsets.Len())
return nil
}
func (f *Forge) InitPatchsets() error {
if err := f.LoadPatchsets(); err == nil {
return nil
} else {
log.Info("LoadPatchsets() failed", err)
}
// TODO: check if Unmarshal failed here
f.Patchsets = NewPatchsets()
f.findAutoPatchset() // adds the default values
return f.SavePatchsets()
}
func (f *Forge) SavePatchsets() error {
filename := filepath.Join(f.Config.PatchDir, "all-patches.pb")
regfile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
log.Info("SavePatchsets() filename open error:", filename, err)
// fmt.Fprintln(w, "filename open error:", filename, err)
return err
}
defer regfile.Close()
newpb := proto.Clone(f.Patchsets).(*Patchsets)
if newpb == nil {
for pset := range f.Patchsets.IterAll() {
pset.ShowPatchsets()
}
return log.Errorf("SavePatchsets() Clone failed!")
}
data, err := newpb.Marshal()
if err != nil {
log.Infof("SavePatchset() proto.Marshal() error %v\n", err)
return err
}
log.Infof("SavePatchset() worked (%d) bytes on %d patches\n", len(data), f.Patchsets.Len())
regfile.Write(data)
return nil
}
func cleanSubject(line string) string {
// Regular expression to remove "Subject:" and "[PATCH...]" patterns
re := regexp.MustCompile(`(?i)^Subject:\s*(\[\s*PATCH[^\]]*\]\s*)?`)
cleaned := re.ReplaceAllString(line, "")
return strings.TrimSpace(cleaned)
}
func (pb *Patchset) ShowPatchsets() error {
author := "Author: " + pb.GitAuthorName
author += " <" + pb.GitAuthorEmail + ">"
log.Printf("%-16s %s %s %s\n", string(pb.Uuid)[0:8], pb.Name, pb.Comment, author)
for _, patch := range pb.Patches.Patches {
comment := cleanSubject(patch.Comment)
log.Printf("\t%-8s %-50s %-50s\n", string(patch.CommitHash)[0:8], patch.Namespace, comment)
}
// for patch := range pb.IterAll() {
// comment := cleanSubject(patch.Comment)
// log.Info("\tnew patch:", patch.NewHash, "commithash:", patch.CommitHash, patch.Namespace, comment)
// }
return nil
}
// adds a patch. returns true if patch is new
func (f *Forge) AddPatch(patch *Patch) bool {
if f.findPatch(patch) {
// log.Info("\talready found!!!!!!!", pset.Uuid, patch.Namespace)
return false
}
if f.AddNewPatch(patch) {
log.Info("\tnew patch added:", patch.CommitHash, patch.Gs, patch.Gae, patch.Gan)
return true
}
log.Info("\tnew patch failed:", patch.CommitHash, patch.Gs)
return false
}
// adds a patchset or just the patches
func (f *Forge) AddPatchset(pb *Patchset) bool {
var changed bool
// if the name of the patchset is "forge auto commit"
// then just add all the patches
if pb.Name == "forge auto commit" {
author := "Author: " + pb.GitAuthorName
author += " <" + pb.GitAuthorEmail + ">"
fmt.Println(pb.Name, pb.Comment, author)
for _, patch := range pb.Patches.Patches {
// log.Info("\tnew patch:", i, patch.CommitHash, patch.Namespace)
if f.findPatch(patch) {
// log.Info("\talready found!!!!!!!", pset.Uuid, patch.Namespace)
} else {
if f.AddNewPatch(patch) {
log.Info("\tnew patch added:", patch.CommitHash, pb.Name, pb.Comment, author)
changed = true
} else {
log.Info("\tnew patch failed:", patch.CommitHash, pb.Name, pb.Comment, author)
}
}
}
return changed
}
// if you got here, this patchset was submitted with a name
// Has this patchset already been submitted?
for pset := range f.Patchsets.IterAll() {
if pset.Uuid == pb.Uuid {
log.Info("ALREADY ADDED", pset.Uuid, pset.Name)
return false
} else {
}
}
// Clone() this protobuf into me.forge.Patchsets
var newpb *Patchset
newpb = proto.Clone(pb).(*Patchset)
if newpb != nil {
f.Patchsets.Patchsets = append(f.Patchsets.Patchsets, newpb)
}
return true
}
func (f *Forge) findAutoPatchset() *Patchset {
for pset := range f.Patchsets.IterAll() {
if pset.Name == "forge auto commit" {
return pset
}
}
var fauto *Patchset
log.Warn("findAutoPatchset() had to create 'forge auto commit'")
if fauto == nil {
fauto = makeDefaultPatchset()
f.Patchsets.Append(fauto)
}
return fauto
}
func makeDefaultPatchset() *Patchset {
fauto := new(Patchset)
fauto.Name = "forge auto commit"
fauto.Patches = NewPatches()
fauto.Uuid = uuid.New().String()
return fauto
}
// adds submitted patches not
// If the patch was actually new, return true
func (f *Forge) AddNewPatch(patch *Patch) bool {
// ignore patch if it's already here
if f.findPatch(patch) {
log.Info("already found patch", patch.CommitHash, patch.Namespace)
return false
}
fauto := f.findAutoPatchset()
if fauto == nil {
// should have made the default patchset
return false
}
fauto.Patches.Append(patch)
return true
}
// returns true if the patch already exists in the protobuf
func (f *Forge) findPatch(newpatch *Patch) bool {
// log.Info("\tlook for patch:", newpatch.CommitHash, newpatch.Namespace)
for pset := range f.Patchsets.IterAll() {
for _, patch := range pset.Patches.Patches {
if patch.CommitHash == newpatch.CommitHash {
// log.Info("\tfound pset!!!!!!", pset.Uuid, patch.Namespace)
return true
}
}
}
return false
}
func (f *Forge) IsPatchApplied(newpatch *Patch) (*gitpb.Repo, bool) {
log.Info("todo: find namespace and repo for patch", newpatch.Filename)
if f.findPatch(newpatch) {
log.Info("\tfindPatch() patch was found")
} else {
log.Info("\tfindPatch() patch was not found")
}
return nil, false
}

9
patchset.new.go Normal file
View File

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

View File

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

View File

@ -1,3 +1,5 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
@ -7,23 +9,57 @@ import (
"regexp"
"strings"
"go.wit.com/lib/config"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
func (f *Forge) NewGoRepo(gopath string, url string) (*gitpb.Repo, error) {
fullpath := filepath.Join(f.GetGoSrc(), gopath)
fullpath := filepath.Join(f.Config.ReposDir, gopath)
test := f.Repos.FindByFullPath(fullpath)
if test != nil {
return test, nil
}
if gopath == "" {
log.Info("TODO: gopath was blank. I'm not sure what is happening here, but this needs to determine the repo Namespace. all calls to NewGoRepo() need to be deprecated. Someone will have to do this when I have funding for WIT. If you are the one looking at this, track me down and I will help do this correctly.")
return nil, log.Errorf("gopath was blank")
}
repo, err := f.Repos.NewGoRepo(fullpath, gopath)
if err != nil {
log.Info("WARNING. NEW FAILED", fullpath)
log.Info("WARNING. NEW FAILED", fullpath, err)
return nil, err
}
// slices.Reverse(f.Repos.Repos)
repo.URL = url
f.VerifyBranchNames(repo)
if f.Config.IsReadOnly(repo.GetGoPath()) {
repo.ReadOnly = true
}
repo.Reload()
repo.ReloadCheck()
return repo, nil
}
// used by the forge daemon
func (f *Forge) AddNamespaceDir(ns string, fullpath string) (*gitpb.Repo, error) {
test := f.Repos.FindByNamespace(ns)
if test != nil {
return test, fmt.Errorf("already have namespace")
}
repo, err := f.Repos.NewGoRepo(fullpath, ns)
if err != nil {
log.Info("WARNING. NEW FAILED", fullpath)
return nil, err
}
f.VerifyBranchNames(repo)
repo.ReloadCheck()
// set the readonly flag based on the users' forge config
if f.Config.IsReadOnly(repo.GetGoPath()) {
repo.ReadOnly = true
}
config.SetChanged("repos", true)
return repo, nil
}
@ -76,7 +112,7 @@ func (f *Forge) findMasterBranch(repo *gitpb.Repo) {
if strings.HasPrefix(s, "ref: ") {
fields := strings.Fields(s)
_, bname := filepath.Split(fields[1])
log.Info("Using master branch name from .git/ HEAD:", bname)
// log.Info("Using master branch name from .git/ HEAD:", bname)
repo.SetMasterBranchName(bname)
return
}
@ -95,11 +131,6 @@ func (f *Forge) findMasterBranch(repo *gitpb.Repo) {
// TODO: figure out the name from git
repo.SetMasterBranchName("master")
if repo.CheckoutMaster() {
} else {
cmd := []string{"git", "branch", "master"}
repo.Run(cmd)
}
}
// figure out what the name of the git devel branch is
@ -108,11 +139,6 @@ func (f *Forge) findDevelBranch(repo *gitpb.Repo) {
// check the forge config first
if bname := f.Config.FindDevelBranch(repo.GetGoPath()); bname != "" {
repo.SetDevelBranchName(bname)
if repo.CheckoutDevel() {
} else {
cmd := []string{"git", "branch", bname}
repo.Run(cmd)
}
return
}
@ -121,13 +147,7 @@ func (f *Forge) findDevelBranch(repo *gitpb.Repo) {
return
}
// TODO: figure out the name from git
repo.SetDevelBranchName("devel")
if repo.CheckoutDevel() {
} else {
cmd := []string{"git", "branch", "devel"}
repo.Run(cmd)
}
}
// this is still in flux
@ -149,13 +169,9 @@ func (f *Forge) VerifyBranchNames(repo *gitpb.Repo) {
} else {
// forcing for now. todo: warn users
repo.SetUserBranchName(uname)
if repo.CheckoutUser() {
} else {
cmd := []string{"git", "branch", uname}
repo.Run(cmd)
}
}
}
// log.Info("VerifyBranchNames", repo.GetMasterBranchName(), repo.GetDevelBranchName(), repo.GetUserBranchName())
}
// what name should be used for the user branch?
@ -201,7 +217,7 @@ func (f *Forge) AddFullPath(fulldir string) *gitpb.Repo {
}
func (f *Forge) FindByGoPath(gopath string) *gitpb.Repo {
fullpath := filepath.Join(f.GetGoSrc(), gopath)
fullpath := filepath.Join(f.Config.ReposDir, gopath)
return f.Repos.FindByFullPath(fullpath)
}
@ -228,6 +244,6 @@ func (f *Forge) FindAnyPath(dir string) *gitpb.Repo {
}
func (f *Forge) DeleteByGoPath(gopath string) bool {
fullpath := filepath.Join(f.GetGoSrc(), gopath)
fullpath := filepath.Join(f.Config.ReposDir, gopath)
return f.Repos.DeleteByFullPath(fullpath)
}

166
rill.go
View File

@ -1,7 +1,11 @@
package forgepb
import (
"sync"
"time"
"github.com/destel/rill"
"go.wit.com/lib/config"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
@ -30,7 +34,6 @@ func (f *Forge) rillUpdate(pool1 int, pool2 int) (int, error) {
// 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)
})
@ -39,16 +42,14 @@ func (f *Forge) rillUpdate(pool1 int, pool2 int) (int, error) {
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())
log.Printf("%10s %-50s gopath=%s\n", "git dir is missing\n", repo.FullPath, repo.GetNamespace())
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 {
if repo.HasChanged() {
// log.Info("repo changed ", repo.FullPath, repo.StateChange)
if err := repo.ReloadCheck(); err != nil {
return err
}
} else {
@ -59,8 +60,157 @@ func (f *Forge) updateRepo(repo *gitpb.Repo) error {
} 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 = 20
// 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
for repo := range f.Repos.IterAll() {
if !repo.IsValidDir() {
log.Printf("%s %-50s\n", "got an invalid repo in forgepb.RillReload()", repo.GetFullPath())
f.Repos.Delete(repo)
log.Info("reposSave = true")
config.SetChanged("repos", true)
continue
}
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.HasChanged() {
return nil
}
repo.ReloadCheck()
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) map[string]*RillStats {
return f.RillRepos(rillf)
}
func (f *Forge) ConfigRill(rillX int, rillY int) {
f.Config.RillX = int32(rillX)
f.Config.RillY = int32(rillY)
log.Infof("Setting rill values to %d,%d\n", f.Config.RillX, f.Config.RillY)
}
type RillStats struct {
Err error
Start time.Time
End time.Time
}
var rillMu sync.Mutex
// 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) RillRepos(rillf func(*gitpb.Repo) error) map[string]*RillStats {
var all []*gitpb.Repo
var stats map[string]*RillStats
stats = make(map[string]*RillStats)
for repo := range f.Repos.IterAll() {
if !repo.IsValidDir() {
log.Printf("got an invalid repo in forgepb.RillRepos() %-50s\n", repo.GetFullPath())
f.Repos.Delete(repo)
log.Info("reposSave = true")
config.SetChanged("repos", true)
continue
}
all = append(all, repo)
}
// log.Info("Rill Repos len =", len(all))
// Convert a slice of user IDs into a channel
ids := rill.FromSlice(all, nil)
var counter int
var watch int = 10
// Read users from the API.
// Concurrency = 20
dirs := rill.Map(ids, int(f.Config.RillX), func(id *gitpb.Repo) (*gitpb.Repo, error) {
return id, nil
})
rill.ForEach(dirs, int(f.Config.RillY), func(repo *gitpb.Repo) error {
// todo: make this a goroutine to show stats to the user
rillMu.Lock()
counter += 1
if counter > watch {
// log.Info("Processed", watch, "repos") // this doesn't work
watch += 50
}
rillMu.Unlock()
rillSetStartTime(stats, repo.GetFullPath())
if err := rillf(repo); err != nil {
rillSetError(stats, repo.GetFullPath(), err)
}
rillSetEndTime(stats, repo.GetFullPath())
return nil
})
return stats
}
func rillSetError(stats map[string]*RillStats, fullpath string, err error) {
rillMu.Lock()
defer rillMu.Unlock()
if s, ok := stats[fullpath]; ok {
s.Err = err
return
}
log.Info("WHAT THE FUCK STATS ERROR", fullpath)
}
func rillSetStartTime(stats map[string]*RillStats, fullpath string) {
rillMu.Lock()
defer rillMu.Unlock()
if s, ok := stats[fullpath]; ok {
s.Start = time.Now()
return
}
var s *RillStats
s = new(RillStats)
s.Start = time.Now()
stats[fullpath] = s
}
func rillSetEndTime(stats map[string]*RillStats, fullpath string) {
rillMu.Lock()
defer rillMu.Unlock()
if s, ok := stats[fullpath]; ok {
s.End = time.Now()
return
}
log.Info("WHAT THE FUCK STATS END TIME", fullpath)
}

171
scanRepoDir.go Normal file
View File

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

View File

@ -4,25 +4,17 @@ import (
sync "sync"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/lib/protobuf/zoopb"
)
// maybe an interface someday?
type Forge struct {
// one-time initialized data
initOnce sync.Once
initErr error // init error, if any
goSrc string // the path to go/src
goWork bool // means the user is currently using a go.work file
Config *ForgeConfigs // config repos for readonly, private, etc
Repos *gitpb.Repos
Machine *zoopb.Machine
configSave bool
}
func (f *Forge) GetGoSrc() string {
return f.goSrc
once sync.Once
Config *ForgeConfigs // config repos for readonly, private, etc
Repos *gitpb.Repos // the repo protobufs
Patchsets *Patchsets // patches that are in progress
hostname string // your hostname
goWork bool // means the user is currently using a go.work file
}
func (f *Forge) IsGoWork() bool {