Compare commits

..

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

29 changed files with 1345 additions and 906 deletions

View File

@ -5,7 +5,7 @@
# go install
all: forgeConfig.pb.go patch.pb.go set.pb.go goimports vet
all: forgeConfig.pb.go patchset.pb.go goimports vet
generate: clean
autogenpb --proto patchset.proto
@ -27,19 +27,5 @@ clean:
forgeConfig.pb.go: forgeConfig.proto
autogenpb --proto forgeConfig.proto
# patchset.pb.go: patchset.proto
# autogenpb --proto patchset.proto
patch.pb.go: patch.proto
autogenpb --proto patch.proto
set.pb.go: set.proto
autogenpb --proto set.proto
protoc-test:
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
patchset.pb.go: patchset.proto
autogenpb --proto patchset.proto

View File

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

View File

@ -129,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())
log.Info("GO111MODULE=off", "f.goWork =", f.IsGoWork(), "f.gosrc =", f.GetGoSrc())
} else {
log.Info("GO111MODULE=", testenv, "f.goWork =", f.IsGoWork())
log.Info("GO111MODULE=", testenv, "f.goWork =", f.IsGoWork(), "f.gosrc =", f.GetGoSrc())
}
log.Info("running:", repo.FullPath)
log.Info("running:", cmd)
@ -202,10 +202,10 @@ func (f *Forge) runAutogenpb(repo *gitpb.Repo) error {
return nil
}
// used by guireleaser for now
// sortcut to find
func (f *Forge) FindWorkingDirRepo() *gitpb.Repo {
pwd, _ := os.Getwd()
basedir := strings.TrimPrefix(pwd, f.Config.ReposDir)
basedir := strings.TrimPrefix(pwd, f.GetGoSrc())
basedir = strings.Trim(basedir, "/")
return f.FindByGoPath(basedir)
}

View File

@ -29,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.Config.ReposDir, gopath)
fullpath := filepath.Join(f.goSrc, gopath)
if pb := f.FindAnyPath(fullpath); pb != nil {
// repo already exists
return pb, nil
@ -156,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.Config.ReposDir, gopath)
fullpath := filepath.Join(f.goSrc, gopath)
basedir, newdir := filepath.Split(fullpath)
// clone the URL directly

202
config.go
View File

@ -2,85 +2,175 @@
package forgepb
import (
"os"
// functions to import and export the protobuf
// data to and from config files
import (
"errors"
"fmt"
"os"
"path/filepath"
"time"
"go.wit.com/lib/config"
"go.wit.com/lib/gui/prep"
"go.wit.com/log"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
)
// returns err1 || err2
func (f *Forge) ConfigSave() error {
var err error
if f.Config == nil {
return log.Errorf("forge.Config == nil")
// backup the current config files
if e := f.backupConfig(); e != nil {
log.Info("forge.BackupConfig() error", e)
err = e
// continue here? notsure. could be bad either way
// out of disk space?
}
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 f.Config != nil {
if e := f.Config.ConfigSave(); e != nil {
log.Info("forge.Config.ConfigSave() error", e)
err = e
}
}
if f.Repos != nil {
if f.HasFullScan() {
f.Repos.HasFullScan = true
t := time.Now()
f.Repos.FullScan = timestamppb.New(t)
}
if e := f.Repos.ConfigSave(); e != nil {
log.Info("forge.Repos.ConfigSave() error", e)
err = e
}
}
return err
}
// functions to import and export the protobuf
// data to and from config files
// write to ~/.config/forge/
func (cfg *ForgeConfigs) ConfigSave() error {
var header string
header += "\n"
header += "# the forge config file\n"
header += "# You can customize things like:\n"
header += "#\n"
header += "# * which repos you have write access to\n"
header += "# * custom branch names for 'master', 'devel' and 'user'\n"
header += "# * 'favorites' so you can remember which things you like\n"
header += "# * sometimes protobuf TEXT can fail so as a backup this also creates a .json file\n"
header += "#\n"
header += "\n"
return config.ConfigSaveWithHeader(cfg, header)
func (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
}
func (cfg *ForgeConfigs) DumpENV() {
log.Infof("CfgPB.Filename = %s\n", cfg.Filename)
log.Infof("CfgPB.ReposPB = %s\n", cfg.ReposPB)
log.Infof("CfgPB.ReposDir = %s\n", cfg.ReposDir)
log.Infof("CfgPB.PatchDir = %s\n", cfg.PatchDir)
log.Infof("CfgPB.ForgeURL = %s\n", cfg.ForgeURL)
if cfg.GoWork {
log.Infof("CfgPB.GoWork = %v\n", cfg.GoWork)
// load the ~/.config/forge/ files
func (c *ForgeConfigs) ConfigLoad(fullpath string) error {
// 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")
}
log.Infof("CfgPB.Mode = %s\n", cfg.Mode)
// log.Infof("CfgPB.Hostname=%s\n", cfg.Hostname)
if cfg.ReposPB != os.Getenv("FORGE_REPOSPB") {
log.Infof("CfgPB file problem: cfg.ReposPB=%s != FORGE_REPOSPB=%s\n", cfg.ReposPB, os.Getenv("FORGE_REPOSPB"))
if 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
}
/*
if f.Config.Xterm == "" {
f.Config.Xterm = "xterm"
f.Config.XtermArgv = append(f.Config.XtermArgv, "-bg")
f.Config.XtermArgv = append(f.Config.XtermArgv, "black")
f.Config.XtermArgv = append(f.Config.XtermArgv, "-fg")
f.Config.XtermArgv = append(f.Config.XtermArgv, "white")
f.SetConfigSave(true)
func (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")
}
*/
func (cfg *ForgeConfigs) InitDefaults() {
// attempt to marshal forge.text
if err := c.UnmarshalTEXT(data); err != nil {
return err
}
log.Log(INFO, "forge.loadText()", 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
}

74
configBackup.go Normal file
View File

@ -0,0 +1,74 @@
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"
"os"
"path/filepath"
"time"
)
func (f *Forge) backupConfig() error {
// make a new dir to backup the files
srcDir := filepath.Join(f.configDir)
destDir := filepath.Join(f.configDir, "backup")
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()
now := time.Now()
timestamp := now.Format("2006.01.02.150405") // bummer. other date doesn't work?
dest = dest + timestamp
destFile, err := os.Create(dest)
if err != nil {
return err
}
defer destFile.Close()
// Copy the content
_, err = io.Copy(destFile, srcFile)
return err
}

15
configDefault.go Normal file
View File

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

View File

@ -6,7 +6,6 @@ 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"
@ -23,7 +22,8 @@ func (f *Forge) CheckDirtyQuiet() {
for _, s := range stats {
if s.Err == nil {
} else {
config.SetChanged("repos", true)
// log.Info(i, s.Err)
f.SetConfigSave(true)
changed = true
}
}
@ -45,7 +45,7 @@ func (f *Forge) CheckDirty() *gitpb.Repos {
if s.Err == nil {
} else {
log.Info(i, s.Err)
config.SetChanged("repos", true)
f.SetConfigSave(true)
}
}
@ -73,12 +73,13 @@ func doCheckDirty(repo *gitpb.Repo) error {
// nothing changed
} else {
log.Info("Repo changed to clean", repo.FullPath)
return log.Errorf("%s repo changed to clean", repo.FullPath)
// f.SetConfigSave(true)
}
} else {
if repo.CheckDirty() {
log.Info("Repo changed to dirty", repo.FullPath)
return log.Errorf("%s repo changed to dirty", repo.FullPath)
// f.SetConfigSave(true)
} else {
// nothing changed
}

View File

@ -42,8 +42,6 @@ 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`
@ -55,17 +53,9 @@ message ForgeConfigs { // `autogenpb:mar
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
bool goWork = 9; // true if there is a go.work file
string goSrc = 9; // is ~/go/src unless a go.work file is found
bool pathLock = 10; // the path is locked
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`

90
goSrcFind.go Normal file
View File

@ -0,0 +1,90 @@
package forgepb
// returns whatever your golang source dir is
// If there is a go.work file in your parent, that directory will be returned
// otherwise, return ~/go/src
import (
"fmt"
"os"
"path/filepath"
"go.wit.com/log"
)
func (f *Forge) GetHome() string {
return f.goSrc
}
// 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")
}

202
goSrcScan.go Normal file
View File

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

View File

@ -18,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.Config.ReposDir, "go.work")
filename := filepath.Join(f.GetGoSrc(), "go.work")
workf, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
@ -32,10 +32,26 @@ 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

33
http.go
View File

@ -7,7 +7,6 @@ import (
"io/ioutil"
"net"
"net/http"
"net/url"
"os/user"
"strings"
@ -15,36 +14,29 @@ import (
"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
func (f *Forge) HttpPost(url string, data []byte) ([]byte, error) {
var err error
var req *http.Request
req, err = http.NewRequest(http.MethodPost, finalURL.String(), bytes.NewBuffer(data))
if req == nil {
return nil, err
}
req, err = http.NewRequest(http.MethodPost, url, bytes.NewBuffer(data))
// log.Info("httpPost() with len", len(data), "url", url)
usr, _ := user.Current()
req.Header.Set("author", usr.Username)
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 {
log.Error(err)
return []byte("client.Do(req) error"), err
}
defer resp.Body.Close()
// log.Info("httpPost() with len", len(data))
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error(err)
return body, err
}
@ -52,7 +44,7 @@ func rawHttpPost(req *http.Request) ([]byte, error) {
}
func (f *Forge) LookupPBorig(check *gitpb.Repos) (*gitpb.Repos, error) {
url := f.Config.ForgeURL + "lookup"
url := f.forgeURL + "lookup"
for repo := range check.IterByFullPath() {
if repo.Namespace == "" {
@ -63,7 +55,6 @@ func (f *Forge) LookupPBorig(check *gitpb.Repos) (*gitpb.Repos, error) {
return check.SubmitReposPB(url)
}
/*
func (f *Forge) LookupPB(check *gitpb.Repos) (*gitpb.Repos, error) {
url := f.forgeURL + "lookup"
@ -103,9 +94,7 @@ func (f *Forge) UpdatePB(check *gitpb.Repos) (*gitpb.Repos, error) {
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 {
@ -120,19 +109,17 @@ func (pb *Patches) AddHttpToPB(r *http.Request) error {
}
}
pb.HttpRequest = &httppb.HttpRequest{
pb.HttpRequest = &Patches_HttpRequest{
Method: r.Method,
URL: r.URL.String(),
Url: r.URL.String(),
Proto: r.Proto,
Headers: headers,
IP: getClientIP(r),
RemoteAddr: 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)

View File

@ -11,8 +11,29 @@ import (
"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
// a console with a fixed with font. AKA: a typerwriter. Which is exactly
// what COBOL did in the 1970's (60s? notsure) And the 80s.
// So, you want to dump out stuff on the console. Let's see. Something like
/*
forge --favorites
go.wit.com/apps/myapp v0.2.0 (installed)
go.wit.com/lib/somethingfun v0.0.7 (not downloaded)
*/
// anyway, you get the idea. This is also called COBOL because it does on
// thing and truncates every line output to the columns you see with stty -a
// my monitor is huge, so it's not going to work at 80x24. 160x48 is better
// actually, I'd predict some of these will probably end up 240 wide
// long live good eyesight and 4K monitors!
func (f *Forge) PrintHumanTable(allr *gitpb.Repos) {
log.DaemonMode(true)
@ -20,8 +41,8 @@ func (f *Forge) PrintHumanTable(allr *gitpb.Repos) {
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}
args := []string{"namespace", "cur br", "age", "master", "devel", "user", "", "lasttag", "next", "repo type"}
sizes := []int{40, 9, 6, 16, 16, 16, 1, 12, 12, 8}
log.Info(cobol.TerminalChomp(cobol.StandardTableSize10(sizes, args)))
all := allr.SortByFullPath()
@ -39,8 +60,8 @@ func (f *Forge) PrintForgedTable(allr *gitpb.Repos) {
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}
args := []string{"namespace", "cur br", "age", "master", "devel", "last tag", "", "", "", ""}
sizes := []int{40, 9, 6, 12, 16, 16, 16, 12, 12, 8}
log.Info(cobol.TerminalChomp(cobol.StandardTableSize10(sizes, args)))
all := allr.SortByFullPath()
@ -49,7 +70,7 @@ func (f *Forge) PrintForgedTable(allr *gitpb.Repos) {
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)
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) PrintHumanTableFull(allr *gitpb.Repos) {
@ -58,8 +79,8 @@ func (f *Forge) PrintHumanTableFull(allr *gitpb.Repos) {
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}
args := []string{"cur br", "age", "master", "devel", "user", "", "lasttag", "next", "repo type", "namespace"}
sizes := []int{9, 6, 16, 16, 16, 1, 12, 12, 8, 0}
log.Info(cobol.TerminalChomp(cobol.StandardTableSize10(sizes, args)))
all := allr.SortByFullPath()
@ -68,7 +89,7 @@ func (f *Forge) PrintHumanTableFull(allr *gitpb.Repos) {
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)
log.Infof("Total repositories: %d (%d master) (%d devel) (%d user) (%d unknown)\n", t.total, t.master, t.devel, t.user, t.unknown)
}
// also shows which files are dirty
@ -78,8 +99,8 @@ func (f *Forge) PrintHumanTableDirty(allr *gitpb.Repos) {
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}
args := []string{"namespace", "cur br", "age", "master", "devel", "user", "", "lasttag", "next", "repo type"}
sizes := []int{40, 9, 6, 16, 16, 16, 1, 12, 12, 8}
log.Info(cobol.TerminalChomp(cobol.StandardTableSize10(sizes, args)))
for repo := range allr.IterAll() {
@ -94,7 +115,7 @@ func (f *Forge) PrintHumanTableDirty(allr *gitpb.Repos) {
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)
log.Infof("Total repositories: %d (%d master) (%d devel) (%d user) (%d unknown)\n", t.total, t.master, t.devel, t.user, t.unknown)
}
// used to count which repos are on which branches (master/main, devel, user)
@ -121,20 +142,82 @@ func tallyBranchTotals(t *tally, repo *gitpb.Repo) {
t.user += 1
return
}
log.Printf("unknown curr=%s user=%s devel=%s master=%s\n", repo.GetCurrentBranchName(), repo.GetUserBranchName(), repo.GetDevelBranchName(), repo.GetMasterBranchName())
log.Printf("unknown curr=%s master=%s devel=%s user=%s\n", repo.GetCurrentBranchName(), repo.GetMasterBranchName(), repo.GetDevelBranchName(), repo.GetUserBranchName())
t.unknown += 1
}
/*
func standardTable5(arg1, arg2, arg3, arg4, arg5 string) string {
len1 := 40
len2 := 12
len3 := 12
len4 := 16
len5 := 8
var s string
if len(arg1) > len1 {
arg1 = arg1[:len1]
}
s = "%-" + fmt.Sprintf("%d", len1) + "s "
if len(arg2) > len2 {
arg2 = arg2[:len2]
}
s += "%-" + fmt.Sprintf("%d", len2) + "s "
if len(arg3) > len3 {
arg3 = arg3[:len3]
}
s += "%-" + fmt.Sprintf("%d", len3) + "s "
if len(arg4) > len4 {
arg4 = arg4[:len4]
}
s += "%-" + fmt.Sprintf("%d", len4) + "s "
if len(arg5) > len5 {
arg5 = arg5[:len5]
}
s += "%-" + fmt.Sprintf("%d", len5) + "s"
return fmt.Sprintf(s, arg1, arg2, arg3, arg4, arg5)
}
*/
/*
func standardTableSize10(sizes []int, args []string) string {
var s string
for i, si := range sizes {
if si == 0 {
s += "%-s "
} else {
s += "%-" + fmt.Sprintf("%d", si) + "s "
if len(args[i]) > sizes[i] {
args[i] = args[i][:sizes[i]]
}
}
}
// there must be a better syntax for this
arg1 := args[0]
arg2 := args[1]
arg3 := args[2]
arg4 := args[3]
arg5 := args[4]
arg6 := args[5]
arg7 := args[6]
arg8 := args[7]
arg9 := args[8]
arg10 := args[9]
return fmt.Sprintf(s, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)
// return fmt.Sprintf(s, args)
}
*/
func (f *Forge) printRepoToTable(repo *gitpb.Repo, sizes []int, full bool) {
var end string
// shortened version numbers
var mhort string = repo.GetMasterVersion()
var dhort string = repo.GetDevelVersion()
var uver string = repo.GetUserVersion()
if uver == "uerr" {
var uhort string = repo.GetUserVersion()
if uhort == "uerr" {
// blank these out
uver = ""
uhort = ""
}
var lasttag string = repo.GetLastTag()
var thort string = repo.GetTargetVersion()
@ -169,16 +252,15 @@ func (f *Forge) printRepoToTable(repo *gitpb.Repo, sizes []int, full bool) {
}
if repo.IsDirty() {
age = ""
uver = "* " + uver
age = "#"
end += "(dirty) "
}
var args []string
if full {
args = []string{cname, age, uver, dhort, mhort, chort, lasttag, thort, rtype, gopath}
args = []string{cname, age, mhort, dhort, uhort, chort, lasttag, thort, rtype, gopath}
} else {
args = []string{gopath, cname, age, uver, dhort, mhort, chort, lasttag, thort, rtype}
args = []string{gopath, cname, age, mhort, dhort, uhort, chort, lasttag, thort, rtype}
}
start := cobol.StandardTableSize10(sizes, args)
@ -188,6 +270,23 @@ func (f *Forge) printRepoToTable(repo *gitpb.Repo, sizes []int, full bool) {
}
}
if repo.GetMasterBranchName() != "master" && repo.GetMasterBranchName() != "main" {
end += "(m:" + repo.GetMasterBranchName() + ") "
}
if repo.GetDevelBranchName() != "devel" {
end += "(d:" + repo.GetDevelBranchName() + ") "
}
if repo.GetUserBranchName() != f.Config.Username {
end += "(u:" + repo.GetUserBranchName() + ") "
}
debname := f.Config.DebName(repo.GetGoPath())
if debname != filepath.Base(gopath) {
end += "(deb:" + debname + ") "
}
switch repo.GetState() {
case "PERFECT":
case "unchanged":
@ -203,23 +302,6 @@ func (f *Forge) printRepoToTable(repo *gitpb.Repo, sizes []int, full bool) {
end += "(" + repo.GetState() + ") "
}
if repo.GetMasterBranchName() != "master" && repo.GetMasterBranchName() != "main" {
end += "(m:" + repo.GetMasterBranchName() + ") "
}
if repo.GetDevelBranchName() != "devel" {
end += "(d:" + repo.GetDevelBranchName() + ") "
}
if repo.GetUserBranchName() != f.Config.Username {
end += "(u:" + repo.GetUserBranchName() + ") "
}
debname := f.Config.DebName(repo.GetNamespace())
if debname != filepath.Base(gopath) {
end += "(deb:" + debname + ") "
}
log.Info(cobol.TerminalChomp(start + " " + end))
}
@ -246,7 +328,7 @@ func (f *Forge) printForgedToTable(repo *gitpb.Repo, sizes []int) {
log.Info(cobol.TerminalChomp(start + " " + end))
}
func (psets *Sets) PrintTable() {
func (psets *Patchsets) PrintTable() {
if psets == nil {
return
}
@ -257,11 +339,10 @@ func (psets *Sets) PrintTable() {
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.GetSets() {
log.Info(pset.Patches.Uuid, pset.Patches.Name, pset.Patches.State)
for x, pset := range psets.GetPatchsets() {
log.Info(pset.Uuid, pset.Name, pset.State)
if pset.State == "DONE" {
// old patchset
continue
@ -281,7 +362,6 @@ func (psets *Sets) PrintTable() {
}
}
log.Infof("Total Contents (%d) Parts (%d)\n", countCONTENTS, countPARTS)
*/
}
func (patches *Patches) PrintTable() {
@ -295,7 +375,6 @@ func (patches *Patches) PrintTable() {
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
@ -309,5 +388,4 @@ func (patches *Patches) PrintTable() {
countPARTS += 1
}
log.Infof("Total Patches (%d)\n", countPARTS)
*/
}

266
init.go
View File

@ -3,161 +3,213 @@
package forgepb
import (
"errors"
"os"
"os/user"
"path/filepath"
"time"
"go.wit.com/lib/config"
"go.wit.com/lib/fhelp"
"go.wit.com/lib/gui/shell"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
/* better syntax from gin
Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default(opts ...OptionFunc) *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine.With(opts...)
}
*/
func Init() *Forge {
f := new(Forge)
cfg := new(ForgeConfigs)
err := config.ConfigLoad(cfg, "forge", "forge")
f.Config = cfg
if err != nil {
// fhelp.DumpENV("finit:")
f.setenv()
if !fhelp.QuestionUser("This is your first time using forge, use these default values?") {
os.Exit(-1)
}
f.Config.InitDefaults()
f.Config.ConfigSave()
f.initFromConfig()
f.Config.DumpENV()
return f
f := InitPB()
/*
f.Machine = new(zoopb.Machine)
if err := f.Machine.ConfigLoad(); err != nil {
log.Log(WARN, "zoopb.ConfigLoad() failed", err)
}
*/
if f.Config.Username == "" {
usr, _ := user.Current()
f.Config.Username = usr.Username
f.Config.ConfigSave()
f.SetConfigSave(true)
}
f.initFromConfig()
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()
if f.hasFullScan {
// duplicate time checking below. which one to keep?
if f.FullScanAge() > time.Minute {
log.Log(INFO, "forgepb.Scan() skipping scan. been run a minute ago", f.FullScanAge())
return f
}
}
now := time.Now()
start := f.Repos.Len()
f.ScanGoSrc()
f.FullScanRan()
end := f.Repos.Len()
if (end - start) == 0 {
log.Log(INFO, "forgepb.Scan() Scan did not find new git repositories. Total =", end)
if f.FullScanAge() > time.Minute {
f.rillUpdate(20, 10)
}
} else {
log.Log(INFO, "forgepb.Scan() Scan found", end-start, "new git repositories. Total =", end)
f.rillUpdate(20, 10)
}
if f.configSave {
// taking this out to debug Marshal() panic
f.ConfigSave()
f.configSave = false
}
log.Log(INFO, "update() check took", shell.FormatDuration(time.Since(now)))
return f
}
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)
func FirstTimeUser() bool {
if checkenv() {
return false
}
// setup the env
f := new(Forge)
f.Config = cfg
f.initFromConfig()
log.Printf("forge.Init() %s len()=%d\n", f.Config.Filename, f.Repos.Len())
return f
f.setenv()
fullname := filepath.Join(os.Getenv("FORGE_CONFIG"), "forge.text")
_, err := os.ReadFile(fullname)
if errors.Is(err, os.ErrNotExist) {
return true
}
return false
}
func (f *Forge) initFromConfig() {
if f.configENV() {
log.Info("ENV changed config. todo: save config here")
f.Config.ConfigSave()
}
if f.Config.ReposPB != os.Getenv("FORGE_REPOSPB") {
// if different, use the ENV var
// this probably means that it gets saved as the default in the config
// we probably want that (?)
f.Config.ReposPB = os.Getenv("FORGE_REPOSPB")
}
if _, s := filepath.Split(f.Config.ReposPB); s != "repos.pb" {
fhelp.DumpENV("forge:")
f.Config.DumpENV()
log.Infof("ReposPB invalid filename '%s'\n", f.Config.ReposPB)
os.Exit(-1)
func (f *Forge) InitPB() {
f.setenv()
// load the ~/.config/forge/ config
f.Config = new(ForgeConfigs)
if err := f.Config.ConfigLoad(f.configDir); err != nil {
log.Log(WARN, "forgepb.ConfigLoad() failed", err)
}
f.Repos = gitpb.NewRepos()
f.Repos.ConfigLoad(f.Config.ReposPB)
f.Repos.ConfigLoad()
if f.Repos.HasFullScan {
f.hasFullScan = true
}
// init the Patchsets
f.Patchsets = NewSets()
f.Patchsets = NewPatchsets()
// todo: play with these / determine good values based on user's machine
if f.Config.RillX == 0 {
f.Config.RillX = 10
}
if f.Config.RillY == 0 {
f.Config.RillY = 20
f.rillX = 10
f.rillY = 20
}
func (f *Forge) InitMachine() {
if f.Config.Username == "" {
usr, _ := user.Current()
f.Config.Username = usr.Username
}
f.hostname, _ = os.Hostname()
// log.Info(hostname, err)
}
// only init's the protobuf. intended to not scan or change anything
func InitPB() *Forge {
f := new(Forge)
f.setenv()
f.InitPB()
return f
}
func (f *Forge) SetConfigSave(b bool) {
config.SetChanged("forge", b)
f.configSave = b
}
// saves the config if there have been changes
func (f *Forge) Exit() {
// log.Info("forge.configSave =", f.configSave)
if f.Config.Mode == ForgeMode_MASTER {
// fhelp.DumpENV("forge:")
// f.Config.DumpENV()
// todo: tell the user to switch to NORMAL mode
}
if f.configSave {
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)
}
func RawInitPB() *Forge {
f := new(Forge)
f.RawInitPB()
return f
}
func (f *Forge) RawInitPB() {
f.InitPB()
}
// 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() {
log.Info("doing setenv()")
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.configDir = os.Getenv("FORGE_CONFIG")
f.goSrc = os.Getenv("FORGE_GOSRC")
f.repoPB = os.Getenv("FORGE_REPOPB")
f.forgeURL = os.Getenv("FORGE_URL")
f.patchDir = os.Getenv("FORGE_PATCHDIR")
f.hostname = os.Getenv("HOSTNAME")
if os.Getenv("FORGE_GOWORK") == "true" {
f.goWork = true
}
f.Config.ReposPB = os.Getenv("FORGE_REPOSPB")
f.Config.ReposDir = os.Getenv("FORGE_REPOSDIR")
f.Config.PatchDir = os.Getenv("FORGE_PATCHDIR")
f.Config.ForgeURL = os.Getenv("FORGE_URL")
fhelp.DumpENV("setenv end()")
})
}
// if the env vars are set, this is probably not a new user
func checkenv() bool {
if os.Getenv("FORGE_CONFIG") != "" {
return true
}
if os.Getenv("FORGE_GOSRC") != "" {
return true
}
if os.Getenv("FORGE_REPOPB") != "" {
return true
}
if os.Getenv("FORGE_URL") != "" {
return true
}
if os.Getenv("FORGE_PATCHDIR") != "" {
return true
}
if os.Getenv("FORGE_VERBOSE") != "" {
return true
}
return false
}
func (f *Forge) GetForgeURL() string {
return f.Config.ForgeURL
return f.forgeURL
}
// set the URL for forge otherwise fallback to ENV or to forge.wit.com
@ -168,47 +220,7 @@ func (f *Forge) SetForgeURL(url string) {
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
f.forgeURL = url
os.Setenv("FORGE_URL", f.forgeURL)
log.Info("Forge URL has been set to", f.forgeURL)
}

View File

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

86
patchset.Get.go Normal file
View File

@ -0,0 +1,86 @@
// Copyright 1994-2025 WIT.COM Inc Licensed GPL 3.0
package forgepb
import (
"go.wit.com/log"
)
// retrieves current patches from forge
func (f *Forge) GetPatches() error {
url := f.forgeURL + "GetPatchsets"
log.Info("GetPatchsets() url", url)
body, err := f.HttpPost(url, nil)
if err != nil {
log.Info("httpPost() failed:", err)
return err
}
log.Info("GetPatchets() len(body)", len(body))
var psets *Patchsets
psets = new(Patchsets)
err = psets.Unmarshal(body)
if err != nil {
log.Info("Unmarshal failed", err)
return err
}
psets.PrintTable()
f.loadUpstreamPatchsets(psets)
return nil
}
func (f *Forge) expireAllPatchsets() {
all := f.Patchsets.All()
for all.Scan() {
pset := all.Next()
pset.State = "DONE"
}
}
func (f *Forge) loadUpstreamPatchsets(psets *Patchsets) {
var foundnew bool
all := psets.All()
for all.Scan() {
pset := all.Next()
found := f.Patchsets.FindByUuid(pset.Uuid)
if found == nil {
f.expireAllPatchsets()
log.Info("new patchset", pset.Name, pset.Uuid)
pset.State = "new"
foundnew = true
if pset == nil {
log.Warn("OH NO! pset == nil")
continue
}
if f.Patchsets == nil {
log.Warn("OH NO! f.Patchsets == nil")
continue
}
log.Warn("appending pset", pset.Uuid, pset.State, pset.Name)
f.Patchsets.Append(pset)
continue
}
author := log.Sprintf("Author: %s <%s>", found.GitAuthorName, found.GitAuthorEmail)
log.Info("EXAMINE PSET", pset.Name, pset.Uuid, pset.Patches.Len())
for _, patch := range pset.Patches.Patches {
// log.Info("\tnew patch:", i, patch.CommitHash, patch.Namespace)
if f.findPatch(patch) {
// log.Info("\talready found!!!!!!!", pset.Uuid, patch.Namespace)
continue
}
if err := f.addRandomPatch(patch); err == nil {
log.Info("\tnew patch added:", patch.CommitHash, found.Name, found.Comment, author)
foundnew = true
} else {
log.Info("\tnew patch failed:", patch.CommitHash, found.Name, found.Comment, author, err)
}
}
pset.State = found.State
if pset.State == "" {
pset.State = "new"
}
}
if foundnew {
f.SavePatchsets()
}
}

View File

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

119
patchset.Send.go Normal file
View File

@ -0,0 +1,119 @@
package forgepb
// functions to import and export the protobuf
// data to and from config files
import (
"errors"
"strings"
"time"
"go.wit.com/log"
)
// 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
}
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")
}
func (f *Forge) SubmitDevelPatchSet(name string) (*Patchset, error) {
pset, err := f.MakeDevelPatchSet(name)
if err != nil {
return nil, err
}
if err := f.submitPatchset(pset); err != nil {
return nil, err
}
return pset, nil
}
func (f *Forge) submitPatchset(pset *Patchset) error {
var url string
url = f.forgeURL + "patchset"
msg, err := pset.Marshal()
if err != nil {
log.Info("proto.Marshal() failed:", err)
return err
}
log.Info("proto.Marshal() msg len", len(msg))
body, err := f.HttpPost(url, msg)
if err != nil {
log.Info("httpPost() failed:", err)
return err
}
newpb := NewPatches()
if err := newpb.Unmarshal(body); err != nil {
cfcheck := string(body[0:100])
if strings.Contains(cfcheck, "<title>Just a moment...</title>") {
return log.Errorf("Cloudflare throttled this attempt to submit. TODO: fix this")
} else {
log.Infof("forged DID NOT SEND BACK PROTOBUF len(body)=%d %s (TODO: look for failure on cloudflare 'is human' check here)\n", len(body), body[0:100])
// log.Infof("TODO: try to identify data here len(body)=%d body[0:40]=%s\n", len(body), body[0:40])
// log.Info("BODY START:", body[0:10], string(body[0:10]))
// log.Info(string(body))
// if err := newpb.UnmarshalTEXT(body[2:]); err == nil {
// log.Info("wow, that did work. newpb.Len() =", newpb.Len())
// }
}
return err
}
log.Info("Total patches sent ok:", newpb.Len())
return nil
}
func (f *Forge) SubmitPatchesNew(pset *Patches, urlpath string) (*Patches, error) {
var url string
url = f.forgeURL + "oldpatchset"
msg, err := pset.Marshal()
if err != nil {
log.Info("proto.Marshal() failed:", err)
return nil, err
}
log.Info("proto.Marshal() msg len", len(msg))
body, err := f.HttpPost(url, msg)
if err != nil {
log.Info("httpPost() failed:", err)
return nil, err
}
newpb := NewPatches()
if err := newpb.Unmarshal(body); err != nil {
cfcheck := string(body[0:100])
if strings.Contains(cfcheck, "<title>Just a moment...</title>") {
return nil, log.Errorf("Cloudflare throttled this attempt to submit. TODO: fix this")
} else {
log.Infof("forged DID NOT SEND BACK PROTOBUF len(body)=%d %s (TODO: look for failure on cloudflare 'is human' check here)\n", len(body), body[0:100])
// log.Infof("TODO: try to identify data here len(body)=%d body[0:40]=%s\n", len(body), body[0:40])
// log.Info("BODY START:", body[0:10], string(body[0:10]))
// log.Info(string(body))
// if err := newpb.UnmarshalTEXT(body[2:]); err == nil {
// log.Info("wow, that did work. newpb.Len() =", newpb.Len())
// }
}
return nil, err
}
log.Info("Total patches sent ok:", newpb.Len())
return newpb, nil
}

View File

@ -14,9 +14,9 @@ import (
)
func (f *Forge) LoadPatchsets() error {
f.Patchsets = NewSets()
f.Patchsets = NewPatchsets()
filename := filepath.Join(f.Config.PatchDir, "all-patches.pb")
filename := filepath.Join(f.patchDir, "all-patches.pb")
data, err := os.ReadFile(filename)
if err != nil {
@ -28,7 +28,7 @@ func (f *Forge) LoadPatchsets() error {
log.Infof("LoadPatchsets() proto.Marshal() error %v\n", err)
return err
}
// log.Infof("LoadPatchsets() found %d patches.\n", f.Patchsets.Len())
log.Infof("LoadPatchsets() found %d patches.\n", f.Patchsets.Len())
return nil
}
@ -39,13 +39,13 @@ func (f *Forge) InitPatchsets() error {
log.Info("LoadPatchsets() failed", err)
}
// TODO: check if Unmarshal failed here
f.Patchsets = NewSets()
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")
filename := filepath.Join(f.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)
@ -54,7 +54,7 @@ func (f *Forge) SavePatchsets() error {
}
defer regfile.Close()
newpb := proto.Clone(f.Patchsets).(*Sets)
newpb := proto.Clone(f.Patchsets).(*Patchsets)
if newpb == nil {
for pset := range f.Patchsets.IterAll() {
pset.ShowPatchsets()
@ -79,7 +79,7 @@ func cleanSubject(line string) string {
return strings.TrimSpace(cleaned)
}
func (pb *Set) ShowPatchsets() error {
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)
@ -87,29 +87,17 @@ func (pb *Set) ShowPatchsets() error {
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)
// }
/*
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 *Set) bool {
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
@ -123,11 +111,11 @@ func (f *Forge) AddPatchset(pb *Set) bool {
if f.findPatch(patch) {
// log.Info("\talready found!!!!!!!", pset.Uuid, patch.Namespace)
} else {
if f.AddNewPatch(patch) {
if err := f.addRandomPatch(patch); err == nil {
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)
log.Info("\tnew patch failed:", patch.CommitHash, pb.Name, pb.Comment, author, err)
}
}
@ -146,49 +134,52 @@ func (f *Forge) AddPatchset(pb *Set) bool {
}
}
f.Patchsets.Append(pb)
// Clone() this protobuf into me.forge.Patchsets
var newpb *Patchset
newpb = proto.Clone(pb).(*Patchset)
if newpb != nil {
f.Patchsets.Patchsets = append(f.Patchsets.Patchsets, newpb)
}
return true
}
func (f *Forge) findAutoPatchset() *Set {
func (f *Forge) findAutoPatchset() *Patchset {
for pset := range f.Patchsets.IterAll() {
if pset.Name == "forge auto commit" {
return pset
}
}
var fauto *Set
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() *Set {
fauto := new(Set)
fauto = new(Patchset)
fauto.Name = "forge auto commit"
fauto.Patches = NewPatches()
fauto.Uuid = uuid.New().String()
f.Patchsets.Patchsets = append(f.Patchsets.Patchsets, fauto)
}
return fauto
}
// adds submitted patches not
// If the patch was actually new, return true
func (f *Forge) AddNewPatch(patch *Patch) bool {
// adds submitted patches not specifically assigned to a patchset
// to the generic patchset called "forge auto commit"
func (f *Forge) addRandomPatch(patch *Patch) error {
// ignore patch if it's already here
if f.findPatch(patch) {
log.Info("already found patch", patch.CommitHash, patch.Namespace)
return false
return nil
}
fauto := f.findAutoPatchset()
if fauto == nil {
// should have made the default patchset
return false
return log.Errorf("no default place yet")
}
fauto.Patches.Append(patch)
return true
newpb := proto.Clone(patch).(*Patch)
if newpb == nil {
return log.Errorf("proto.Clone returned nil")
}
fauto.Patches.Patches = append(fauto.Patches.Patches, newpb)
return nil
}
// returns true if the patch already exists in the protobuf

View File

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

102
patchset.proto Normal file
View File

@ -0,0 +1,102 @@
// Copyright 2025 WIT.COM Inc Licensed GPL 3.0
syntax = "proto3";
package forgepb;
import "google/protobuf/timestamp.proto"; // Import the well-known type for Timestamp
// Forge doesn't need this kind of specificity
// but this is what the patch files contain and how git sees them
// message Blob {
// string hunkLine = 1;
// bytes data = 2;
// }
//
// message File {
// string filename = 1;
// string hashLine = 2;
// repeated Blob Blobs = 3;
// }
//
// message Patch {
// repeated File Files = 1;
// string repoNamespace = 2;
// string gH = 3;
// string gaI = 4;
// string gcI = 5;
// }
// git log -1 --format="%H %aI %cI %an %ae %cn %ce"
message Patch {
string namespace = 1; // the base repo git namespace
bytes data = 2; // the raw data of the whole patch
string gH = 3; // after some deliberation, I think I'll just try variable names
string gT = 4;
string gP = 5;
string gs = 6;
string gaI = 7; // that exactly match what git uses.
string gan = 8;
string gae = 9;
string gcI = 10;
string gcn = 11;
string gce = 12;
string gN = 13;
string gGG = 14;
string gGS = 15;
string gGK = 16;
string newHash = 17; // new hash
string state = 18; // the 'state' of the patch
string filename = 19; // `autogenpb:unique` `autogenpb:sort`
string startHash = 20; // the start commit hash
string commitHash = 21; // the git commit hash of this patch `autogenpb:sort` `autogenpb:unique`
string comment = 22; // the git commit message (in patch form)
repeated string Files = 23; // the filenames this patch changes
google.protobuf.Timestamp ctime = 24; // create time of the patch
bool applied = 25; // have you applied this patch?
bool upstream = 26; // has this patch been applied upstream?
}
// this is a "PATCH: [1/x]" series
message Patches { // `autogenpb:marshal` `autogenpb:gui:Patch`
message HttpRequest { // HttpRequest represents the essential fields of an incoming HTTP request.
string method = 1; // The request method, e.g., "GET", "POST".
string url = 2; // The full URL of the request, including scheme, host, path, and query string.
string route = 3; // just the route: "/add/" or "/find/"
string proto = 4; // The protocol version, e.g., "HTTP/1.1", "HTTP/2.0".
map<string, string> headers = 5; // The map of request headers. Header names are case-insensitive,
string remoteAddr = 6; // The remote IP address of the client, after resolving proxies.
string host = 7; // The host on which the URL is sought (www.wit.com)
string hostname = 8; // The hostname of the client if passed from the client (mylaptop.fun.me)
bytes body = 9; // The request body as raw bytes.
string namespace = 10; // When the body is a pb (always!). This is the pb namespace ("go.wit.com/lib/protobuf/virtpb")
}
string uuid = 1; // `autogenpb:uuid:2679065e-c81d-4a00-aca4-03c158a834fb`
string version = 2; // `autogenpb:version:v2.0.0`
repeated Patch Patches = 3;
HttpRequest httpRequest = 4; // who connected // rename httpRequest? This might make sense in our case
string Error = 5; // when passing these around, if there is an error, store it here
}
message Patchset { // `autogenpb:marshal`
Patches patches = 1; //
string name = 2; // `autogenpb:sort`
string comment = 3; //
string gitAuthorName = 4; // `autogenpb:sort`
string gitAuthorEmail = 5; //
google.protobuf.Timestamp ctime = 6; // create time of the patchset
string tmpDir = 7; // temp dir
string startBranchName = 8; //
string endBranchName = 9; //
string startBranchHash = 10; //
string endBranchHash = 11; //
string state = 12; // the state of the patch
string uuid = 13; // `autogenpb:sort` `autogenpb:unique`
string hostname = 14; //
}
message Patchsets { // `autogenpb:marshal` `autogenpb:gui` `autogenpb:nomutex`
string uuid = 1; // `autogenpb:uuid:be926ad9-f07f-484c-adf2-d96eeabf3079`
string version = 2; // `autogenpb:version:v0.0.45`
repeated Patchset Patchsets = 3;
}

View File

@ -9,13 +9,12 @@ 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.Config.ReposDir, gopath)
fullpath := filepath.Join(f.GetGoSrc(), gopath)
test := f.Repos.FindByFullPath(fullpath)
if test != nil {
return test, nil
@ -36,7 +35,7 @@ func (f *Forge) NewGoRepo(gopath string, url string) (*gitpb.Repo, error) {
if f.Config.IsReadOnly(repo.GetGoPath()) {
repo.ReadOnly = true
}
repo.ReloadCheck()
repo.Reload()
return repo, nil
}
@ -53,13 +52,13 @@ func (f *Forge) AddNamespaceDir(ns string, fullpath string) (*gitpb.Repo, error)
}
f.VerifyBranchNames(repo)
repo.ReloadCheck()
repo.Reload()
// set the readonly flag based on the users' forge config
if f.Config.IsReadOnly(repo.GetGoPath()) {
repo.ReadOnly = true
}
config.SetChanged("repos", true)
f.configSave = true
return repo, nil
}
@ -164,9 +163,6 @@ func (f *Forge) VerifyBranchNames(repo *gitpb.Repo) {
if repo.GetUserBranchName() == "" {
uname := f.configUserBranchName(repo)
if uname == "" {
log.Info("configUserBranchName() ERROR: failed with blank")
}
if repo.IsBranch(uname) {
repo.SetUserBranchName(uname)
} else {
@ -174,9 +170,6 @@ func (f *Forge) VerifyBranchNames(repo *gitpb.Repo) {
repo.SetUserBranchName(uname)
}
}
if repo.GetUserBranchName() == "" || repo.GetDevelBranchName() == "" {
log.Infof("VerifyBranchNames() failed m=%s d=%s u=%s\n", repo.GetMasterBranchName(), repo.GetDevelBranchName(), repo.GetUserBranchName())
}
}
// what name should be used for the user branch?
@ -192,9 +185,6 @@ func (f *Forge) configUserBranchName(repo *gitpb.Repo) string {
if uname != "" {
return uname
}
if f.Config.Username == "" {
// something is wrong!
}
// use the os.Username
uname = f.Config.Username
@ -225,7 +215,7 @@ func (f *Forge) AddFullPath(fulldir string) *gitpb.Repo {
}
func (f *Forge) FindByGoPath(gopath string) *gitpb.Repo {
fullpath := filepath.Join(f.Config.ReposDir, gopath)
fullpath := filepath.Join(f.GetGoSrc(), gopath)
return f.Repos.FindByFullPath(fullpath)
}
@ -252,6 +242,6 @@ func (f *Forge) FindAnyPath(dir string) *gitpb.Repo {
}
func (f *Forge) DeleteByGoPath(gopath string) bool {
fullpath := filepath.Join(f.Config.ReposDir, gopath)
fullpath := filepath.Join(f.GetGoSrc(), gopath)
return f.Repos.DeleteByFullPath(fullpath)
}

37
rill.go
View File

@ -5,7 +5,6 @@ import (
"time"
"github.com/destel/rill"
"go.wit.com/lib/config"
"go.wit.com/lib/protobuf/gitpb"
"go.wit.com/log"
)
@ -34,6 +33,7 @@ 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)
})
@ -42,14 +42,16 @@ 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\n", repo.FullPath, repo.GetNamespace())
log.Printf("%10s %-50s gopath=%s\n", "git dir is missing\n", repo.FullPath, repo.GetGoPath())
f.Repos.DeleteByFullPath(repo.FullPath)
f.configSave = true
return nil
}
if repo.HasChanged() {
if repo.DidRepoChange() {
f.configSave = true
// log.Info("repo changed ", repo.FullPath, repo.StateChange)
if err := repo.ReloadCheck(); err != nil {
if err := repo.Reload(); err != nil {
return err
}
} else {
@ -60,6 +62,7 @@ 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
@ -76,10 +79,7 @@ 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)
log.Printf("%s %-50s\n", "got an invalid repo in forgepb.RillReload()", repo.GetGoPath())
continue
}
all = append(all, repo)
@ -95,10 +95,11 @@ func (f *Forge) RillReload() int {
})
rill.ForEach(dirs, RillY, func(repo *gitpb.Repo) error {
if !repo.HasChanged() {
if !repo.DidRepoChange() {
return nil
}
repo.ReloadCheck()
f.configSave = true
repo.Reload()
counter += 1
return nil
})
@ -115,9 +116,9 @@ func (f *Forge) RillFuncError(rillf func(*gitpb.Repo) error) map[string]*RillSta
}
func (f *Forge) ConfigRill(rillX int, rillY int) {
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)
f.rillX = rillX
f.rillY = rillY
log.Infof("Setting rill values to %d,%d\n", f.rillX, f.rillY)
}
type RillStats struct {
@ -140,15 +141,11 @@ func (f *Forge) RillRepos(rillf func(*gitpb.Repo) error) 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)
log.Printf("got an invalid repo in forgepb.RillRepos() %-50s\n", repo.GetGoPath())
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)
@ -157,11 +154,11 @@ func (f *Forge) RillRepos(rillf func(*gitpb.Repo) error) map[string]*RillStats {
// Read users from the API.
// Concurrency = 20
dirs := rill.Map(ids, int(f.Config.RillX), func(id *gitpb.Repo) (*gitpb.Repo, error) {
dirs := rill.Map(ids, f.rillX, func(id *gitpb.Repo) (*gitpb.Repo, error) {
return id, nil
})
rill.ForEach(dirs, int(f.Config.RillY), func(repo *gitpb.Repo) error {
rill.ForEach(dirs, f.rillY, func(repo *gitpb.Repo) error {
// todo: make this a goroutine to show stats to the user
rillMu.Lock()
counter += 1

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package forgepb
import (
sync "sync"
"time"
"go.wit.com/lib/protobuf/gitpb"
)
@ -10,13 +11,45 @@ import (
type Forge struct {
// one-time initialized data
once sync.Once
initErr error // init error, if any
goSrc string // the path to go/src
repoPB string // the path to the repos.pb cache file
configDir string // normally ~/.config/forge
goWork bool // means the user is currently using a go.work file
Config *ForgeConfigs // config repos for readonly, private, etc
Repos *gitpb.Repos // the repo protobufs
Patchsets *Sets // patches that are in progress
configSave bool // if you need to save the config because things changed
hasFullScan bool // track last scan so it can be throttled
fullscan time.Time // time of the last scan so it can be throttled
hostname string // your hostname
goWork bool // means the user is currently using a go.work file
forgeURL string // URL to use to forge.wit.com
rillX int // used for Rill()
rillY int // used for Rill()
Patchsets *Patchsets // patches that are in progress
patchDir string // where patches are stored
}
func (f *Forge) GetGoSrc() string {
return f.goSrc
}
func (f *Forge) GetConfigDir() string {
return f.configDir
}
func (f *Forge) IsGoWork() bool {
return f.goWork
}
func (f *Forge) HasFullScan() bool {
return f.Repos.HasFullScan
}
func (f *Forge) FullScanRan() {
f.fullscan = time.Now()
}
func (f *Forge) FullScanAge() time.Duration {
fs := f.Repos.FullScan.AsTime()
return time.Since(fs)
}