From 0aeae8d523ecb762e296b938cff3e012551e27b7 Mon Sep 17 00:00:00 2001 From: Fuwn Date: Sun, 13 Oct 2024 01:11:14 -0700 Subject: refactor(yae): move commands and library to internal package --- .gitignore | 2 +- internal/commands/add.go | 70 ++++++++++++++++++++++ internal/commands/drop.go | 24 ++++++++ internal/commands/init.go | 19 ++++++ internal/commands/update.go | 51 +++++++++++++++++ internal/yae/source.go | 124 +++++++++++++++++++++++++++++++++++++++ internal/yae/sources.go | 53 +++++++++++++++++ internal/yae/utilities.go | 55 ++++++++++++++++++ source.go | 124 --------------------------------------- sources.go | 53 ----------------- utilities.go | 55 ------------------ yae.go | 137 ++++---------------------------------------- 12 files changed, 409 insertions(+), 358 deletions(-) create mode 100644 internal/commands/add.go create mode 100644 internal/commands/drop.go create mode 100644 internal/commands/init.go create mode 100644 internal/commands/update.go create mode 100644 internal/yae/source.go create mode 100644 internal/yae/sources.go create mode 100644 internal/yae/utilities.go delete mode 100644 source.go delete mode 100644 sources.go delete mode 100644 utilities.go diff --git a/.gitignore b/.gitignore index ee13815..6572f3d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -yae +/yae .pre-commit-config.yaml result yae.json diff --git a/internal/commands/add.go b/internal/commands/add.go new file mode 100644 index 0000000..4ee15ec --- /dev/null +++ b/internal/commands/add.go @@ -0,0 +1,70 @@ +package commands + +import ( + "fmt" + "strings" + + "github.com/Fuwn/yae/internal/yae" + "github.com/urfave/cli/v2" +) + +func Add(sources *yae.Sources) func(c *cli.Context) error { + return func(c *cli.Context) error { + if c.Args().Len() != 2 { + return fmt.Errorf("invalid number of arguments") + } + + if sources.Exists(c.Args().Get(0)) { + return fmt.Errorf("source already exists") + } + + source := yae.Source{ + Unpack: c.Bool("unpack"), + Type: c.String("type"), + } + version := c.String("version") + + if version != "" { + source.URLTemplate = c.Args().Get(1) + source.Version = c.String("version") + + if strings.Contains(source.URLTemplate, "{version}") { + source.URL = strings.ReplaceAll(source.URLTemplate, "{version}", source.Version) + } + } else { + source.URL = c.Args().Get(1) + } + + if source.Type == "git" && c.String("tag-predicate") != "" { + source.TagPredicate = c.String("tag-predicate") + } + + if c.String("trim-tag-prefix") != "" { + source.TrimTagPrefix = c.String("trim-tag-prefix") + } + + if c.Bool("pin") { + source.Pinned = true + } + + if c.Bool("force") { + if source.Pinned { + return fmt.Errorf("cannot set a source to be statically forced and pinned at the same time") + } + + source.Force = true + } + + if sha256, err := yae.FetchSHA256(source.URL, c.Bool("unpack")); err != nil { + return err + } else { + source.SHA256 = sha256 + } + + if err := sources.Add(c.Args().Get(0), source); err != nil { + return err + } + + return sources.Save(c.String("sources")) + } +} diff --git a/internal/commands/drop.go b/internal/commands/drop.go new file mode 100644 index 0000000..58855df --- /dev/null +++ b/internal/commands/drop.go @@ -0,0 +1,24 @@ +package commands + +import ( + "fmt" + + "github.com/Fuwn/yae/internal/yae" + "github.com/urfave/cli/v2" +) + +func Drop(sources *yae.Sources) func(c *cli.Context) error { + return func(c *cli.Context) error { + if c.Args().Len() == 0 { + return fmt.Errorf("invalid number of arguments") + } + + if !sources.Exists(c.Args().Get(0)) { + return fmt.Errorf("source does not exist") + } + + sources.Drop(c.Args().Get(0)) + + return sources.Save(c.String("sources")) + } +} diff --git a/internal/commands/init.go b/internal/commands/init.go new file mode 100644 index 0000000..f38ec96 --- /dev/null +++ b/internal/commands/init.go @@ -0,0 +1,19 @@ +package commands + +import ( + "fmt" + "os" + + "github.com/Fuwn/yae/internal/yae" + "github.com/urfave/cli/v2" +) + +func Init(sources *yae.Sources) func(c *cli.Context) error { + return func(c *cli.Context) error { + if _, err := os.Stat(c.String("sources")); err == nil { + return fmt.Errorf("sources file already exists") + } + + return sources.Save(c.String("sources")) + } +} diff --git a/internal/commands/update.go b/internal/commands/update.go new file mode 100644 index 0000000..66ccfb3 --- /dev/null +++ b/internal/commands/update.go @@ -0,0 +1,51 @@ +package commands + +import ( + "fmt" + + "github.com/Fuwn/yae/internal/yae" + "github.com/urfave/cli/v2" +) + +func Update(sources *yae.Sources) func(c *cli.Context) error { + return func(c *cli.Context) error { + updates := []string{} + force := c.Bool("force-hashed") + forcePinned := c.Bool("force-pinned") + + if c.Args().Len() == 0 { + for name, source := range *sources { + if updated, err := source.Update(sources, name, force, forcePinned); err != nil { + return err + } else if updated { + updates = append(updates, name) + } + } + } else { + name := c.Args().Get(0) + source := (*sources)[name] + + if updated, err := source.Update(sources, name, force, forcePinned); err != nil { + return err + } else if updated { + updates = append(updates, name) + } + } + + if len(updates) > 0 { + if err := sources.Save(c.String("sources")); err != nil { + return err + } + } + + if c.Bool("output-updated-list") { + for _, update := range updates { + fmt.Println(update) + } + } else if c.Bool("output-formatted-updated-list") { + fmt.Println(yae.Lister(updates)) + } + + return nil + } +} diff --git a/internal/yae/source.go b/internal/yae/source.go new file mode 100644 index 0000000..185aba7 --- /dev/null +++ b/internal/yae/source.go @@ -0,0 +1,124 @@ +package yae + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/log" +) + +type Source struct { + URL string `json:"url"` + SHA256 string `json:"sha256"` + Unpack bool `json:"unpack"` + Type string `json:"type"` + Version string `json:"version,omitempty"` + URLTemplate string `json:"url_template,omitempty"` + TagPredicate string `json:"tag_predicate,omitempty"` + TrimTagPrefix string `json:"trim_tag_prefix,omitempty"` + Pinned bool `json:"pinned,omitempty"` + Force bool `json:"force,omitempty"` +} + +func (source *Source) Update(sources *Sources, name string, force bool, forcePinned bool) (bool, error) { + log.Infof("checking %s", name) + + updated := false + + if !sources.Exists(name) { + log.Warnf("skipped %s: source does not exist", name) + + return updated, nil + } + + if source.Pinned && !forcePinned { + log.Infof("skipped %s: source is pinned", name) + + return updated, nil + } + + if source.Type == "git" { + log.Debugf("checking %s: remote git tag", name) + + tag, err := source.fetchLatestGitTag() + + if err != nil { + return updated, err + } + + if tag != source.Version || force || source.Force { + if tag != source.Version { + log.Infof("bumped %s: %s -> %s", name, source.Version, tag) + } + + if tag != source.Version { + updated = true + } + + source.Version = tag + + if strings.Contains(source.URLTemplate, "{version}") { + source.URL = strings.ReplaceAll(source.URLTemplate, "{version}", source.Version) + + log.Debugf("patched %s: substituted url template", name) + } + } else { + log.Infof("skipped %s: version remains unchanged", name) + + return updated, nil + } + } + + log.Debugf("checking %s: sha256", name) + + sha256, err := FetchSHA256(source.URL, source.Unpack) + + if err != nil { + return updated, err + } + + if sha256 != source.SHA256 { + log.Infof("rehashed %s: %s -> %s", name, source.SHA256, sha256) + + source.SHA256 = sha256 + updated = true + } + + (*sources)[name] = *source + + return updated, nil +} + +func (source *Source) fetchLatestGitTag() (string, error) { + if source.Type == "git" { + repository := "https://github.com/" + strings.Split(source.URL, "/")[3] + "/" + strings.Split(source.URL, "/")[4] + remotes, err := command("bash", false, "-c", fmt.Sprintf("git ls-remote %s | awk -F'/' '{print $NF}' | sort -V", repository)) + + if err != nil { + return "", err + } + + refs := strings.Split(remotes, "\n") + var latest string + + if source.TagPredicate == "" { + latest = refs[len(refs)-2] + } else { + for i := len(refs) - 2; i >= 0; i-- { + if strings.Contains(refs[i], source.TagPredicate) { + latest = refs[i] + + break + } + } + } + + if source.TrimTagPrefix != "" { + latest = strings.TrimPrefix(latest, source.TrimTagPrefix) + } + + return latest, nil + } + + return "", fmt.Errorf("source is not a git repository") +} diff --git a/internal/yae/sources.go b/internal/yae/sources.go new file mode 100644 index 0000000..f8cb2b0 --- /dev/null +++ b/internal/yae/sources.go @@ -0,0 +1,53 @@ +package yae + +import ( + "encoding/json" + "fmt" + "os" +) + +type Sources map[string]Source + +func (s *Sources) Add(name string, d Source) error { + if s.Exists(name) { + return fmt.Errorf("source already exists") + } + + (*s)[name] = d + + return nil +} + +func (s *Sources) Exists(name string) bool { + _, ok := (*s)[name] + + return ok +} + +func (s *Sources) Drop(url string) { + delete((*s), url) +} + +func (s *Sources) Save(path string) error { + file, err := os.Create(path) + + if err != nil { + return err + } + + encoder := json.NewEncoder(file) + + encoder.SetIndent("", " ") + + return encoder.Encode(s) +} + +func (s *Sources) Load(path string) error { + file, err := os.Open(path) + + if err != nil { + return err + } + + return json.NewDecoder(file).Decode(s) +} diff --git a/internal/yae/utilities.go b/internal/yae/utilities.go new file mode 100644 index 0000000..ef9d334 --- /dev/null +++ b/internal/yae/utilities.go @@ -0,0 +1,55 @@ +package yae + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +func FetchSHA256(url string, unpack bool) (string, error) { + arguments := []string{"--type", "sha256", url} + + if unpack { + arguments = append([]string{"--unpack"}, arguments...) + } + + output, err := command("nix-prefetch-url", false, arguments...) + + if err != nil { + return "", err + } + + lines := strings.Split(output, "\n") + + return strings.Trim(lines[len(lines)-2], "\n"), nil +} + +func command(name string, show bool, args ...string) (string, error) { + executable, err := exec.LookPath(name) + out := []byte{} + + if show { + cmd := exec.Command(executable, args...) + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + out, err = cmd.Output() + } else { + cmd := exec.Command(executable, args...) + out, err = cmd.Output() + } + + return string(out), err +} + +func Lister(items []string) string { + if len(items) == 0 { + return "" + } else if len(items) == 1 { + return items[0] + } else if len(items) == 2 { + return fmt.Sprintf("%s & %s", items[0], items[1]) + } + + return fmt.Sprintf("%s, & %s", strings.Join(items[:len(items)-1], ", "), items[len(items)-1]) +} diff --git a/source.go b/source.go deleted file mode 100644 index 490b17c..0000000 --- a/source.go +++ /dev/null @@ -1,124 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - "github.com/charmbracelet/log" -) - -type Source struct { - URL string `json:"url"` - SHA256 string `json:"sha256"` - Unpack bool `json:"unpack"` - Type string `json:"type"` - Version string `json:"version,omitempty"` - URLTemplate string `json:"url_template,omitempty"` - TagPredicate string `json:"tag_predicate,omitempty"` - TrimTagPrefix string `json:"trim_tag_prefix,omitempty"` - Pinned bool `json:"pinned,omitempty"` - Force bool `json:"force,omitempty"` -} - -func (source *Source) Update(sources *Sources, name string, force bool, forcePinned bool) (bool, error) { - log.Infof("checking %s", name) - - updated := false - - if !sources.Exists(name) { - log.Warnf("skipped %s: source does not exist", name) - - return updated, nil - } - - if source.Pinned && !forcePinned { - log.Infof("skipped %s: source is pinned", name) - - return updated, nil - } - - if source.Type == "git" { - log.Debugf("checking %s: remote git tag", name) - - tag, err := source.fetchLatestGitTag() - - if err != nil { - return updated, err - } - - if tag != source.Version || force || source.Force { - if tag != source.Version { - log.Infof("bumped %s: %s -> %s", name, source.Version, tag) - } - - if tag != source.Version { - updated = true - } - - source.Version = tag - - if strings.Contains(source.URLTemplate, "{version}") { - source.URL = strings.ReplaceAll(source.URLTemplate, "{version}", source.Version) - - log.Debugf("patched %s: substituted url template", name) - } - } else { - log.Infof("skipped %s: version remains unchanged", name) - - return updated, nil - } - } - - log.Debugf("checking %s: sha256", name) - - sha256, err := fetchSHA256(source.URL, source.Unpack) - - if err != nil { - return updated, err - } - - if sha256 != source.SHA256 { - log.Infof("rehashed %s: %s -> %s", name, source.SHA256, sha256) - - source.SHA256 = sha256 - updated = true - } - - (*sources)[name] = *source - - return updated, nil -} - -func (source *Source) fetchLatestGitTag() (string, error) { - if source.Type == "git" { - repository := "https://github.com/" + strings.Split(source.URL, "/")[3] + "/" + strings.Split(source.URL, "/")[4] - remotes, err := command("bash", false, "-c", fmt.Sprintf("git ls-remote %s | awk -F'/' '{print $NF}' | sort -V", repository)) - - if err != nil { - return "", err - } - - refs := strings.Split(remotes, "\n") - var latest string - - if source.TagPredicate == "" { - latest = refs[len(refs)-2] - } else { - for i := len(refs) - 2; i >= 0; i-- { - if strings.Contains(refs[i], source.TagPredicate) { - latest = refs[i] - - break - } - } - } - - if source.TrimTagPrefix != "" { - latest = strings.TrimPrefix(latest, source.TrimTagPrefix) - } - - return latest, nil - } - - return "", fmt.Errorf("source is not a git repository") -} diff --git a/sources.go b/sources.go deleted file mode 100644 index d092edd..0000000 --- a/sources.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "os" -) - -type Sources map[string]Source - -func (s *Sources) Add(name string, d Source) error { - if s.Exists(name) { - return fmt.Errorf("source already exists") - } - - (*s)[name] = d - - return nil -} - -func (s *Sources) Exists(name string) bool { - _, ok := (*s)[name] - - return ok -} - -func (s *Sources) Drop(url string) { - delete((*s), url) -} - -func (s *Sources) Save(path string) error { - file, err := os.Create(path) - - if err != nil { - return err - } - - encoder := json.NewEncoder(file) - - encoder.SetIndent("", " ") - - return encoder.Encode(s) -} - -func (s *Sources) Load(path string) error { - file, err := os.Open(path) - - if err != nil { - return err - } - - return json.NewDecoder(file).Decode(s) -} diff --git a/utilities.go b/utilities.go deleted file mode 100644 index fc9b339..0000000 --- a/utilities.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "fmt" - "os" - "os/exec" - "strings" -) - -func fetchSHA256(url string, unpack bool) (string, error) { - arguments := []string{"--type", "sha256", url} - - if unpack { - arguments = append([]string{"--unpack"}, arguments...) - } - - output, err := command("nix-prefetch-url", false, arguments...) - - if err != nil { - return "", err - } - - lines := strings.Split(output, "\n") - - return strings.Trim(lines[len(lines)-2], "\n"), nil -} - -func command(name string, show bool, args ...string) (string, error) { - executable, err := exec.LookPath(name) - out := []byte{} - - if show { - cmd := exec.Command(executable, args...) - cmd.Stdin = os.Stdin - cmd.Stderr = os.Stderr - out, err = cmd.Output() - } else { - cmd := exec.Command(executable, args...) - out, err = cmd.Output() - } - - return string(out), err -} - -func lister(items []string) string { - if len(items) == 0 { - return "" - } else if len(items) == 1 { - return items[0] - } else if len(items) == 2 { - return fmt.Sprintf("%s & %s", items[0], items[1]) - } - - return fmt.Sprintf("%s, & %s", strings.Join(items[:len(items)-1], ", "), items[len(items)-1]) -} diff --git a/yae.go b/yae.go index 309819a..60be017 100644 --- a/yae.go +++ b/yae.go @@ -3,15 +3,16 @@ package main import ( "fmt" "os" - "strings" "time" + "github.com/Fuwn/yae/internal/commands" + "github.com/Fuwn/yae/internal/yae" "github.com/charmbracelet/log" "github.com/urfave/cli/v2" ) func main() { - sources := Sources{} + sources := yae.Sources{} if err := (&cli.App{ Name: "yae", @@ -65,15 +66,9 @@ func main() { Suggest: true, Commands: []*cli.Command{ { - Name: "init", - Usage: "Initialise a new Yae environment", - Action: func(c *cli.Context) error { - if _, err := os.Stat(c.String("sources")); err == nil { - return fmt.Errorf("sources file already exists") - } - - return sources.Save(c.String("sources")) - }, + Name: "init", + Usage: "Initialise a new Yae environment", + Action: commands.Init(&sources), }, { Name: "add", @@ -119,82 +114,13 @@ func main() { Usage: "Always force update the source, regardless of unchanged remote tag", }, }, - Action: func(c *cli.Context) error { - if c.Args().Len() != 2 { - return fmt.Errorf("invalid number of arguments") - } - - if sources.Exists(c.Args().Get(0)) { - return fmt.Errorf("source already exists") - } - - source := Source{ - Unpack: c.Bool("unpack"), - Type: c.String("type"), - } - version := c.String("version") - - if version != "" { - source.URLTemplate = c.Args().Get(1) - source.Version = c.String("version") - - if strings.Contains(source.URLTemplate, "{version}") { - source.URL = strings.ReplaceAll(source.URLTemplate, "{version}", source.Version) - } - } else { - source.URL = c.Args().Get(1) - } - - if source.Type == "git" && c.String("tag-predicate") != "" { - source.TagPredicate = c.String("tag-predicate") - } - - if c.String("trim-tag-prefix") != "" { - source.TrimTagPrefix = c.String("trim-tag-prefix") - } - - if c.Bool("pin") { - source.Pinned = true - } - - if c.Bool("force") { - if source.Pinned { - return fmt.Errorf("cannot set a source to be statically forced and pinned at the same time") - } - - source.Force = true - } - - if sha256, err := fetchSHA256(source.URL, c.Bool("unpack")); err != nil { - return err - } else { - source.SHA256 = sha256 - } - - if err := sources.Add(c.Args().Get(0), source); err != nil { - return err - } - - return sources.Save(c.String("sources")) - }, + Action: commands.Add(&sources), }, { - Name: "drop", - Args: true, - Usage: "Drop a source", - Action: func(c *cli.Context) error { - if c.Args().Len() == 0 { - return fmt.Errorf("invalid number of arguments") - } - - if !sources.Exists(c.Args().Get(0)) { - return fmt.Errorf("source does not exist") - } - - sources.Drop(c.Args().Get(0)) - - return sources.Save(c.String("sources")) - }, + Name: "drop", + Args: true, + Usage: "Drop a source", + Action: commands.Drop(&sources), }, { Name: "update", @@ -219,46 +145,7 @@ func main() { Usage: "Force updates for all sources, including pinned sources (can be used with --force-hashed)", }, }, - Action: func(c *cli.Context) error { - updates := []string{} - force := c.Bool("force-hashed") - forcePinned := c.Bool("force-pinned") - - if c.Args().Len() == 0 { - for name, source := range sources { - if updated, err := source.Update(&sources, name, force, forcePinned); err != nil { - return err - } else if updated { - updates = append(updates, name) - } - } - } else { - name := c.Args().Get(0) - source := sources[name] - - if updated, err := source.Update(&sources, name, force, forcePinned); err != nil { - return err - } else if updated { - updates = append(updates, name) - } - } - - if len(updates) > 0 { - if err := sources.Save(c.String("sources")); err != nil { - return err - } - } - - if c.Bool("output-updated-list") { - for _, update := range updates { - fmt.Println(update) - } - } else if c.Bool("output-formatted-updated-list") { - fmt.Println(lister(updates)) - } - - return nil - }, + Action: commands.Update(&sources), }, }, }).Run(os.Args); err != nil { -- cgit v1.2.3