diff --git a/Makefile b/Makefile index 9a32973..202004e 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ # go install -all: repo.pb.go +all: repo.pb.go forgeConfig.pb.go make -C forgeConfig vet: lint @@ -34,3 +34,9 @@ repo.pb.go: repo.proto cd ~/go/src && protoc --go_out=. --proto_path=go.wit.com/lib/protobuf/forgepb \ --go_opt=Mrepo.proto=go.wit.com/lib/protobuf/forgepb \ repo.proto + +forgeConfig.pb.go: forgeConfig.proto + # I'm using version v1.35.x from google.golang.org/protobuf/cmd/protoc-gen-go + cd ~/go/src && protoc --go_out=. --proto_path=go.wit.com/lib/protobuf/forgepb \ + --go_opt=MforgeConfig.proto=go.wit.com/lib/protobuf/forgepb \ + forgeConfig.proto diff --git a/config.go b/config.go index 672d974..0866ae3 100644 --- a/config.go +++ b/config.go @@ -12,7 +12,7 @@ import ( ) // write to ~/.config/forge/ unless ENV{FORGE_HOME} is set -func (m *Repos) ConfigSave() error { +func (m *ForgeConfigs) ConfigSave() error { if os.Getenv("FORGE_HOME") == "" { homeDir, _ := os.UserHomeDir() fullpath := filepath.Join(homeDir, ".config/forge") @@ -39,7 +39,7 @@ func (m *Repos) ConfigSave() error { } // load the ~/.config/forge/ files -func (c *Repos) ConfigLoad() error { +func (c *ForgeConfigs) ConfigLoad() error { if os.Getenv("FORGE_HOME") == "" { homeDir, _ := os.UserHomeDir() fullpath := filepath.Join(homeDir, ".config/forge") @@ -66,7 +66,7 @@ func (c *Repos) ConfigLoad() error { log.Warn("broken forge.pb config file") return err } - log.Info("config load found", len(c.Repos), "repos") + log.Info("config load found", len(c.ForgeConfigs), "repos") return nil } @@ -86,7 +86,7 @@ func (c *Repos) ConfigLoad() error { log.Warn("broken forge.text config file") return err } - log.Info("config load found", len(c.Repos), "repos") + log.Info("config load found", len(c.ForgeConfigs), "repos") return nil } @@ -106,7 +106,7 @@ func (c *Repos) ConfigLoad() error { log.Warn("broken forge.json config file") return err } - log.Info("config load found", len(c.Repos), "repos") + log.Info("config load found", len(c.ForgeConfigs), "repos") return nil } diff --git a/forgeConfig.marshal.go b/forgeConfig.marshal.go new file mode 100644 index 0000000..63fa744 --- /dev/null +++ b/forgeConfig.marshal.go @@ -0,0 +1,50 @@ +package forgepb + +// TODO: autogen this +// functions to import and export the protobuf +// data to and from config files + +import ( + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/proto" + // "google.golang.org/protobuf/proto" +) + +// human readable JSON +func (p *ForgeConfigs) FormatJSON() string { + return protojson.Format(p) +} + +// apparently this isn't supposed to be used? +// https://protobuf.dev/reference/go/faq/#unstable-text +// this is a shame because this is much nicer output than JSON Format() +// TODO: fix things so this is the default +func (p *ForgeConfigs) FormatTEXT() string { + return prototext.Format(p) +} + +// unmarshalTEXT +func (p *ForgeConfigs) UnmarshalTEXT(data []byte) error { + return prototext.Unmarshal(data, p) +} + +// marshal json +func (p *ForgeConfigs) MarshalJSON() ([]byte, error) { + return protojson.Marshal(p) +} + +// unmarshal +func (p *ForgeConfigs) UnmarshalJSON(data []byte) error { + return protojson.Unmarshal(data, p) +} + +// marshal to wire +func (m *ForgeConfigs) Marshal() ([]byte, error) { + return proto.Marshal(m) +} + +// unmarshal from wire +func (m *ForgeConfigs) Unmarshal(data []byte) error { + return proto.Unmarshal(data, m) +} diff --git a/forgeConfig.proto b/forgeConfig.proto new file mode 100644 index 0000000..ab4e033 --- /dev/null +++ b/forgeConfig.proto @@ -0,0 +1,39 @@ +syntax = "proto3"; + +package forgepb; + +import "google/protobuf/timestamp.proto"; // Import the well-known type for Timestamp + +// define 3 branches. that is all that is supported +// the term 'master' is used in the code because 'main' is a reserved word in golang already +// allow 'read only' and 'private' flags +// 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 { + string goPath = 1; // 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 + + string masterBranch = 8; // git 'main' or 'master' branch name + string develBranch = 9; // whatever the git 'devel' branch name is + string userBranch = 10; // whatever your username branch is + + string debName = 11; // the actual name used with 'apt install' (or distro apt equivalent. +// todo: appeal to everyone to alias 'apt' on rhat, gentoo, arch, etc to alias 'apt install' +// so we can make easier instructions for new linux users. KISS + + google.protobuf.Timestamp verstamp = 12; // the git commit timestamp of the version +} + +// TODO: autogen 'Repos' +message ForgeConfigs { + string uuid = 1; // could be useful for /usr/share/file/magic someday? + string version = 2; // could be used for protobuf schema change violations? + repeated ForgeConfig ForgeConfigs = 3; +} diff --git a/forgeConfig.sort.go b/forgeConfig.sort.go new file mode 100644 index 0000000..4b0ff69 --- /dev/null +++ b/forgeConfig.sort.go @@ -0,0 +1,151 @@ +package forgepb + +// TODO: autogen this? (probably not feasible. need go-arglike tricks in proto) + +import ( + "fmt" + "os" + "sort" + sync "sync" + "time" +) + +// bad global lock until I figure out some other plan +var forgeConfigsLock sync.RWMutex + +type ForgeConfigIterator struct { + sync.RWMutex + + packs []*ForgeConfig + index int +} + +// NewForgeConfigIterator initializes a new iterator. +func NewForgeConfigIterator(packs []*ForgeConfig) *ForgeConfigIterator { + return &ForgeConfigIterator{packs: packs} +} + +// Scan moves to the next element and returns false if there are no more packs. +func (it *ForgeConfigIterator) Scan() bool { + if it.index >= len(it.packs) { + return false + } + it.index++ + return true +} + +// ForgeConfig returns the current forgeConfig. +func (it *ForgeConfigIterator) Next() *ForgeConfig { + if it.packs[it.index-1] == nil { + for i, d := range it.packs { + fmt.Println("i =", i, d) + } + fmt.Println("len =", len(it.packs)) + fmt.Println("forgeConfig == nil", it.index, it.index-1) + os.Exit(-1) + } + return it.packs[it.index-1] +} + +// Use Scan() in a loop, similar to a while loop +// +// for iterator.Scan() { +// d := iterator.ForgeConfig() +// fmt.Println("ForgeConfig UUID:", d.Uuid) +// } + +func (r *ForgeConfigs) All() *ForgeConfigIterator { + forgeConfigPointers := r.selectAllForgeConfigs() + + iterator := NewForgeConfigIterator(forgeConfigPointers) + return iterator +} + +func (r *ForgeConfigs) SortByPath() *ForgeConfigIterator { + packs := r.selectAllForgeConfigs() + + sort.Sort(ByForgeConfigPath(packs)) + + iterator := NewForgeConfigIterator(packs) + return iterator +} + +// enforces no duplicate forgeConfig paths +func (r *ForgeConfigs) Append(newP *ForgeConfig) bool { + forgeConfigsLock.Lock() + defer forgeConfigsLock.Unlock() + + for _, p := range r.ForgeConfigs { + if p.GoPath == newP.GoPath { + return false + } + } + + r.ForgeConfigs = append(r.ForgeConfigs, newP) + return true +} + +// returns time.Duration since last Update() +func (r *ForgeConfig) Age(newP *ForgeConfig) time.Duration { + t := time.Since(r.Verstamp.AsTime()) + return t +} + +// find a forgeConfig by path +func (r *ForgeConfigs) FindByPath(gopath string) *ForgeConfig { + forgeConfigsLock.RLock() + defer forgeConfigsLock.RUnlock() + + for _, p := range r.ForgeConfigs { + if p.GoPath == gopath { + return p + } + } + + return nil +} + +func (r *ForgeConfigs) Len() int { + forgeConfigsLock.RLock() + defer forgeConfigsLock.RUnlock() + + return len(r.ForgeConfigs) +} + +type ByForgeConfigPath []*ForgeConfig + +func (a ByForgeConfigPath) Len() int { return len(a) } +func (a ByForgeConfigPath) Less(i, j int) bool { return a[i].GoPath < a[j].GoPath } +func (a ByForgeConfigPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +func (all *ForgeConfigs) DeleteByPath(gopath string) *ForgeConfig { + forgeConfigsLock.Lock() + defer forgeConfigsLock.Unlock() + + var newr ForgeConfig + + for i, _ := range all.ForgeConfigs { + if all.ForgeConfigs[i].GoPath == gopath { + newr = *all.ForgeConfigs[i] + all.ForgeConfigs[i] = all.ForgeConfigs[len(all.ForgeConfigs)-1] + all.ForgeConfigs = all.ForgeConfigs[:len(all.ForgeConfigs)-1] + return &newr + } + } + return nil +} + +// safely returns a slice of pointers to the ForgeConfig protobufs +func (r *ForgeConfigs) selectAllForgeConfigs() []*ForgeConfig { + forgeConfigsLock.RLock() + defer forgeConfigsLock.RUnlock() + + // Create a new slice to hold pointers to each ForgeConfig + var allPacks []*ForgeConfig + allPacks = make([]*ForgeConfig, len(r.ForgeConfigs)) + for i, p := range r.ForgeConfigs { + allPacks[i] = p // Copy pointers for safe iteration + } + + return allPacks +} diff --git a/forgeConfig/main.go b/forgeConfig/main.go index 1efa31e..fe53fe8 100644 --- a/forgeConfig/main.go +++ b/forgeConfig/main.go @@ -11,7 +11,7 @@ import ( var VERSION string func main() { - var repos forgepb.Repos + var repos forgepb.ForgeConfigs if err := repos.ConfigLoad(); err != nil { log.Warn("forgepb.ConfigLoad() failed", err) os.Exit(-1) @@ -55,7 +55,7 @@ func main() { // try to add, then save config and exit if argv.Add { log.Info("going to add a new repo", argv.GoPath) - new1 := forgepb.Repo{ + new1 := forgepb.ForgeConfig{ GoPath: argv.GoPath, Writable: argv.Writable, ReadOnly: argv.ReadOnly, diff --git a/human.go b/human.go index 66d1211..294a789 100644 --- a/human.go +++ b/human.go @@ -18,7 +18,7 @@ func standardHeader() string { return fmt.Sprintf("%-4s %-40s %s", "", "Path", "flags") } -func (all *Repos) standardHeader(r *Repo) string { +func (all *ForgeConfigs) standardHeader(r *ForgeConfig) string { var flags string var readonly string if all.IsPrivate(r.GoPath) { @@ -36,7 +36,7 @@ func (all *Repos) standardHeader(r *Repo) string { } // print a human readable table to STDOUT -func (all *Repos) PrintTable() { +func (all *ForgeConfigs) PrintTable() { if all == nil { log.Info("WTF") os.Exit(0) @@ -44,7 +44,7 @@ func (all *Repos) PrintTable() { log.Info(standardHeader()) loop := all.SortByPath() for loop.Scan() { - r := loop.Repo() + r := loop.Next() log.Info(all.standardHeader(r)) } } diff --git a/sampleConfig.go b/sampleConfig.go index fc83f6f..52038ab 100644 --- a/sampleConfig.go +++ b/sampleConfig.go @@ -6,8 +6,8 @@ import ( "go.wit.com/log" ) -func (all *Repos) SampleConfig() { - new1 := new(Repo) +func (all *ForgeConfigs) SampleConfig() { + new1 := new(ForgeConfig) new1.GoPath = "go.wit.com" new1.Writable = true new1.Directory = true @@ -17,7 +17,7 @@ func (all *Repos) SampleConfig() { log.Info("added", new1.GoPath, "failed") } - new1 = new(Repo) + new1 = new(ForgeConfig) new1.GoPath = "go.wit.com/apps/zookeeper" new1.DebName = "zookeeper-go" if all.Append(new1) { @@ -26,7 +26,7 @@ func (all *Repos) SampleConfig() { log.Info("added", new1.GoPath, "failed") } - new1 = new(Repo) + new1 = new(ForgeConfig) new1.GoPath = "go.wit.com/apps/wit-package" new1.Private = true if all.Append(new1) { @@ -35,7 +35,7 @@ func (all *Repos) SampleConfig() { log.Info("added", new1.GoPath, "failed") } - new1 = new(Repo) + new1 = new(ForgeConfig) new1.GoPath = "go.wit.com/apps/networkQuality" new1.DebName = "networkquality" new1.ReadOnly = true @@ -45,7 +45,7 @@ func (all *Repos) SampleConfig() { log.Info("added", new1.GoPath, "failed") } - new2 := new(Repo) + new2 := new(ForgeConfig) new2.GoPath = "go.wit.com/apps/go-clone" if all.Append(new2) { log.Info("added", new2.GoPath, "ok") @@ -59,5 +59,5 @@ func (all *Repos) SampleConfig() { log.Info("added", new2.GoPath, "failed (but ok)") } - fmt.Println("first time user. adding an example config file with", len(all.Repos), "repos") + fmt.Println("first time user. adding an example config file with", len(all.ForgeConfigs), "repos") } diff --git a/settings.go b/settings.go index f40bdb4..61f0d98 100644 --- a/settings.go +++ b/settings.go @@ -14,7 +14,7 @@ import ( "strings" ) -func (all *Repos) UpdateGoPath(name string, gopath string) bool { +func (all *ForgeConfigs) UpdateGoPath(name string, gopath string) bool { oldr := all.DeleteByPath(name) if oldr == nil { // nothing to update @@ -28,12 +28,12 @@ func (all *Repos) UpdateGoPath(name string, gopath string) bool { // returns true if gopath is readonly() // will attempt to match IsWritable("foo") against anything ending in "foo" -func (all *Repos) IsReadOnly(gopath string) bool { - var match *Repo +func (all *ForgeConfigs) IsReadOnly(gopath string) bool { + var match *ForgeConfig loop := all.SortByPath() // get the list of repos for loop.Scan() { - r := loop.Repo() + r := loop.Next() if r.GoPath == gopath { // exact gopath match if r.Writable { @@ -88,13 +88,13 @@ func (all *Repos) IsReadOnly(gopath string) bool { // this let's you check a git tag version against a package .deb version // allows gopath's to not need to match the .deb name // this is important in lots of cases! It is normal and happens often enough. -func (all *Repos) DebName(gopath string) string { +func (all *ForgeConfigs) DebName(gopath string) string { // get "zookeeper" from "go.wit.com/apps/zookeeper" normalBase := filepath.Base(gopath) loop := all.SortByPath() for loop.Scan() { - r := loop.Repo() + r := loop.Next() if r.GoPath == gopath { // returns "zookeeper-go" for "go.wit.com/apps/zookeeper" if r.DebName != "" { @@ -115,15 +115,15 @@ func (all *Repos) DebName(gopath string) string { // // IsPrivate("go.foo.com/jcarr/foo") returns true if private // IsPrivate("foo") also returns true if "go.bar.com/jcarr/foo" is private -func (all *Repos) IsPrivate(thing string) bool { - var match *Repo +func (all *ForgeConfigs) IsPrivate(thing string) bool { + var match *ForgeConfig // sort by path means the simple 'match' logic // here works in the sense the last directory match // is the one that is used loop := all.SortByPath() // get the list of repos for loop.Scan() { - r := loop.Repo() + r := loop.Next() if r.GoPath == thing { // if private is set here, then ok, otherwise // still check if a Directory match exists @@ -159,12 +159,12 @@ func (all *Repos) IsPrivate(thing string) bool { // file that lets you set things as favorites // so you can just go-clone a bunch of common things // on a new box or after you reset/delete your ~/go/src dir -func (all *Repos) IsFavorite(thing string) bool { - var match *Repo +func (all *ForgeConfigs) IsFavorite(thing string) bool { + var match *ForgeConfig loop := all.SortByPath() // get the list of repos for loop.Scan() { - r := loop.Repo() + r := loop.Next() if r.GoPath == thing { if r.Favorite { return true