From c0bf43ed586f961b22403dc3be2f2c18492f7f1a Mon Sep 17 00:00:00 2001 From: b4b4r07 Date: Sat, 18 Mar 2023 11:42:25 +0900 Subject: [PATCH 1/8] Support gh extension --- docs/configuration/package/github.md | 22 +++++++++++++++++ docs/index.md | 2 ++ pkg/config/github.go | 35 +++++++++++++++++++++++++--- pkg/github/github.go | 16 +++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/docs/configuration/package/github.md b/docs/configuration/package/github.md index 1894053..d2e4896 100644 --- a/docs/configuration/package/github.md +++ b/docs/configuration/package/github.md @@ -87,6 +87,28 @@ number | `0` (all commits) Limit fetching to the specified number of commits from the tip of each remote branch history. If fetching to a shallow repository, specify 1 or more number, deepen or shorten the history to the specified number of commits. +### as + +Type | Default +---|--- +string | `""` + +Available arguments: + +- gh-extension + +=== "gh-extension" + + Install a package as [gh extension](https://github.blog/2023-01-13-new-github-cli-extension-tools/). Officially gh extensions can be installed with `gh extension install owern/repo` command ([guide](https://cli.github.com/manual/gh_extension_install)) but it's difficult to manage what we downloaded as code. In afx, by handling them as the same as other packages, it allows us to codenize them. + + ```yaml + - name: dlvhdr/gh-dash + description: A beautiful CLI dashboard for GitHub + owner: dlvhdr + repo: gh-dash + as: gh-extension + ``` + ### release.name Type | Default diff --git a/docs/index.md b/docs/index.md index 6e67319..fef01e3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,3 +29,5 @@ Flags: Use "afx [command] --help" for more information about a command. ``` + +![](https://user-images.githubusercontent.com/4442708/224565945-2c09b729-82b7-4829-9cbc-e247b401b689.gif) diff --git a/pkg/config/github.go b/pkg/config/github.go index 09a2256..d73009f 100644 --- a/pkg/config/github.go +++ b/pkg/config/github.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "log" + "net/http" "os" "path/filepath" @@ -22,16 +23,19 @@ import ( "github.com/fatih/color" ) +const GHExtension = "gh-extension" + // GitHub represents GitHub repository type GitHub struct { Name string `yaml:"name" validate:"required"` - Owner string `yaml:"owner" validate:"required"` - Repo string `yaml:"repo" validate:"required"` + Owner string `yaml:"owner" validate:"required"` + Repo string `yaml:"repo" validate:"required"` Description string `yaml:"description"` Branch string `yaml:"branch"` Option *GitHubOption `yaml:"with"` + As string `yaml:"as" validate:"excluded_with=Release"` Release *GitHubRelease `yaml:"release"` @@ -167,6 +171,20 @@ func (c GitHub) Install(ctx context.Context, status chan<- Status) error { } } + switch c.As { + case GHExtension: + // https://github.com/cli/cli/tree/trunk/pkg/cmd/extension + ok, _ := github.HasRelease(http.DefaultClient, c.Owner, c.Repo) + if ok { + err := c.InstallFromRelease(ctx) + if err != nil { + err = errors.Wrapf(err, "%s: failed to get from release", c.Name) + status <- Status{Name: c.GetName(), Done: true, Err: true} + return err + } + } + } + var errs errors.Errors if c.HasPluginBlock() { errs.Append(c.Plugin.Install(c)) @@ -202,13 +220,20 @@ func (c GitHub) Installed() bool { return check(list) } +func (c GitHub) GetReleaseTag() string { + if c.Release != nil { + return c.Release.Tag + } + return "latest" +} + // InstallFromRelease runs install from GitHub release, from not repository func (c GitHub) InstallFromRelease(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() release, err := github.NewRelease( - ctx, c.Owner, c.Repo, c.Release.Tag, + ctx, c.Owner, c.Repo, c.GetReleaseTag(), github.WithWorkdir(c.GetHome()), github.WithFilter(func(filename string) github.FilterFunc { if filename == "" { @@ -343,6 +368,10 @@ func (c GitHub) GetName() string { // GetHome returns a path func (c GitHub) GetHome() string { + switch c.As { + case GHExtension: + return filepath.Join(os.Getenv("HOME"), ".local", "share", "gh", "extensions", c.Repo) + } return filepath.Join(os.Getenv("HOME"), ".afx", "github.com", c.Owner, c.Repo) } diff --git a/pkg/github/github.go b/pkg/github/github.go index ef1108d..9183e1e 100644 --- a/pkg/github/github.go +++ b/pkg/github/github.go @@ -376,3 +376,19 @@ func (r *Release) Install(to string) error { TargetPath: to, }) } + +func HasRelease(httpClient *http.Client, owner, repo string) (bool, error) { + // https://github.com/cli/cli/blob/9596fd5368cdbd30d08555266890a2312e22eba9/pkg/cmd/extension/http.go#L110 + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return false, err + } + + resp, err := httpClient.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + return resp.StatusCode < 299, nil +} From 67861a8a10764c674f8f603a802d7ff805dd4ba0 Mon Sep 17 00:00:00 2001 From: b4b4r07 Date: Sat, 18 Mar 2023 11:47:20 +0900 Subject: [PATCH 2/8] Update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c66a80b..900b71d 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Full document is here: [AFX](https://babarot.me/afx/) - Allows to manage various packages types: - GitHub / GitHub Release / Gist / HTTP (web) / Local + - [gh extensions](https://github.com/topics/gh-extension) - Manages as CLI commands, shell plugins or both - Easy to install/update/uninstall - Easy to configure with YAML From 3c1b2a5a4d03a042fceab7e33e2eb9518189936e Mon Sep 17 00:00:00 2001 From: b4b4r07 Date: Sat, 18 Mar 2023 18:33:54 +0900 Subject: [PATCH 3/8] Use struct for as.gh-extension --- pkg/config/config.go | 5 ++--- pkg/config/github.go | 42 ++++++++++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 4bb83c4..3ee1b04 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/b4b4r07/afx/pkg/dependency" - "github.com/b4b4r07/afx/pkg/errors" "github.com/b4b4r07/afx/pkg/state" "github.com/go-playground/validator/v10" "github.com/goccy/go-yaml" @@ -98,7 +97,7 @@ func (c Config) Parse() ([]Package, error) { func visitYAML(files *[]string) filepath.WalkFunc { return func(path string, info os.FileInfo, err error) error { if err != nil { - return errors.Wrapf(err, "%s: failed to visit", path) + return fmt.Errorf("%w: %s: failed to visit", err, path) } switch filepath.Ext(path) { case ".yaml", ".yml": @@ -178,7 +177,7 @@ func Sort(given []Package) ([]Package, error) { resolved, err := dependency.Resolve(graph) if err != nil { - return pkgs, errors.Wrap(err, "failed to resolve dependency graph") + return pkgs, fmt.Errorf("%w: failed to resolve dependency graph", err) } for _, node := range resolved { diff --git a/pkg/config/github.go b/pkg/config/github.go index d73009f..9958459 100644 --- a/pkg/config/github.go +++ b/pkg/config/github.go @@ -23,8 +23,6 @@ import ( "github.com/fatih/color" ) -const GHExtension = "gh-extension" - // GitHub represents GitHub repository type GitHub struct { Name string `yaml:"name" validate:"required"` @@ -35,16 +33,25 @@ type GitHub struct { Branch string `yaml:"branch"` Option *GitHubOption `yaml:"with"` - As string `yaml:"as" validate:"excluded_with=Release"` Release *GitHubRelease `yaml:"release"` - Plugin *Plugin `yaml:"plugin"` - Command *Command `yaml:"command" validate:"required_with=Release"` // TODO: (not required Release) + Plugin *Plugin `yaml:"plugin"` + Command *Command `yaml:"command" validate:"required_with=Release"` // TODO: (not required Release) + As *GitHubAs `yaml:"as"` DependsOn []string `yaml:"depends-on"` } +type GitHubAs struct { + GHExtension *GHExtension `yaml:"gh-extension"` +} + +type GHExtension struct { + Name string `yaml:"name" validate:"required"` + RenameTo string `yaml:"rename-to" validate:"startswith=gh-,excludesall=/"` +} + type GitHubOption struct { Depth int `yaml:"depth"` } @@ -171,8 +178,8 @@ func (c GitHub) Install(ctx context.Context, status chan<- Status) error { } } - switch c.As { - case GHExtension: + var errs errors.Errors + if c.IsGHExtension() { // https://github.com/cli/cli/tree/trunk/pkg/cmd/extension ok, _ := github.HasRelease(http.DefaultClient, c.Owner, c.Repo) if ok { @@ -183,9 +190,21 @@ func (c GitHub) Install(ctx context.Context, status chan<- Status) error { return err } } + if gh := c.As.GHExtension; gh.RenameTo != "" { + dir := filepath.Join(filepath.Dir(c.GetHome()), gh.RenameTo) + errs.Append( + os.Symlink( + c.GetHome(), + dir, + )) + errs.Append( + os.Symlink( + filepath.Join(dir, gh.Name), + filepath.Join(dir, gh.RenameTo), + )) + } } - var errs errors.Errors if c.HasPluginBlock() { errs.Append(c.Plugin.Install(c)) } @@ -368,8 +387,7 @@ func (c GitHub) GetName() string { // GetHome returns a path func (c GitHub) GetHome() string { - switch c.As { - case GHExtension: + if c.IsGHExtension() { return filepath.Join(os.Getenv("HOME"), ".local", "share", "gh", "extensions", c.Repo) } return filepath.Join(os.Getenv("HOME"), ".afx", "github.com", c.Owner, c.Repo) @@ -466,3 +484,7 @@ func (c GitHub) checkUpdates(ctx context.Context) (report, error) { return report{}, errors.New("invalid version comparison") } } + +func (c GitHub) IsGHExtension() bool { + return c.As != nil && c.As.GHExtension != nil +} From 3eb19158168830f5ecd84e6ab3421fba8e578ace Mon Sep 17 00:00:00 2001 From: b4b4r07 Date: Sat, 18 Mar 2023 19:02:51 +0900 Subject: [PATCH 4/8] Implement gh-extension installation --- pkg/config/config.go | 6 ++++ pkg/config/github.go | 80 +++++++++++++++++++++++++++----------------- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 3ee1b04..ceef989 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -54,6 +54,7 @@ func Read(path string) (Config, error) { defer f.Close() validate := validator.New() + validate.RegisterValidation("gh-extension", ValidateGHExtension) d := yaml.NewDecoder( bufio.NewReader(f), yaml.DisallowUnknownField(), @@ -244,6 +245,11 @@ func getResource(pkg Package) state.Resource { if pkg.HasReleaseBlock() { id = fmt.Sprintf("github.com/release/%s/%s", pkg.Owner, pkg.Repo) } + if pkg.IsGHExtension() { + ty = "GitHub (gh extension)" + gh := pkg.As.GHExtension + paths = append(paths, gh.GetHome()) + } case Gist: ty = "Gist" id = fmt.Sprintf("gist.github.com/%s/%s", pkg.Owner, pkg.ID) diff --git a/pkg/config/github.go b/pkg/config/github.go index 9958459..7c7d2fd 100644 --- a/pkg/config/github.go +++ b/pkg/config/github.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "strings" git "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/config" @@ -21,6 +22,7 @@ import ( "github.com/b4b4r07/afx/pkg/state" "github.com/b4b4r07/afx/pkg/templates" "github.com/fatih/color" + "github.com/go-playground/validator/v10" ) // GitHub represents GitHub repository @@ -48,8 +50,41 @@ type GitHubAs struct { } type GHExtension struct { - Name string `yaml:"name" validate:"required"` - RenameTo string `yaml:"rename-to" validate:"startswith=gh-,excludesall=/"` + Name string `yaml:"name" validate:"required,startswith=gh-"` + RenameTo string `yaml:"rename-to" validate:"gh-extension,excludesall=/"` +} + +func (gh GHExtension) GetHome() string { + base := filepath.Join(os.Getenv("HOME"), ".local", "share", "gh", "extensions") + var ext string + if gh.RenameTo == "" { + ext = filepath.Join(base, gh.Name) + } else { + ext = filepath.Join(base, gh.RenameTo) + } + return ext +} + +func (gh GHExtension) Install(home string) error { + ghHome := gh.GetHome() + // ensure to create the parent dir of each gh extension's path + os.MkdirAll(filepath.Dir(ghHome), os.ModePerm) + + // make alias + if gh.RenameTo != "" { + if err := os.Symlink( + filepath.Join(home, gh.Name), + filepath.Join(home, gh.RenameTo), + ); err != nil { + return fmt.Errorf("%w: failed to symlink as alise", err) + } + } + + // install + if err := os.Symlink(home, ghHome); err != nil { + return fmt.Errorf("%w: failed to symlink as install", err) + } + return nil } type GitHubOption struct { @@ -179,10 +214,10 @@ func (c GitHub) Install(ctx context.Context, status chan<- Status) error { } var errs errors.Errors + if c.IsGHExtension() { - // https://github.com/cli/cli/tree/trunk/pkg/cmd/extension - ok, _ := github.HasRelease(http.DefaultClient, c.Owner, c.Repo) - if ok { + available, _ := github.HasRelease(http.DefaultClient, c.Owner, c.Repo) + if available { err := c.InstallFromRelease(ctx) if err != nil { err = errors.Wrapf(err, "%s: failed to get from release", c.Name) @@ -190,19 +225,10 @@ func (c GitHub) Install(ctx context.Context, status chan<- Status) error { return err } } - if gh := c.As.GHExtension; gh.RenameTo != "" { - dir := filepath.Join(filepath.Dir(c.GetHome()), gh.RenameTo) - errs.Append( - os.Symlink( - c.GetHome(), - dir, - )) - errs.Append( - os.Symlink( - filepath.Join(dir, gh.Name), - filepath.Join(dir, gh.RenameTo), - )) - } + gh := c.As.GHExtension + errs.Append( + gh.Install(c.GetHome()), + ) } if c.HasPluginBlock() { @@ -256,7 +282,7 @@ func (c GitHub) InstallFromRelease(ctx context.Context) error { github.WithWorkdir(c.GetHome()), github.WithFilter(func(filename string) github.FilterFunc { if filename == "" { - // do not use filterfunc + // cancel filtering return nil } return func(assets github.Assets) *github.Asset { @@ -320,22 +346,18 @@ func (c GitHub) templateFilename() string { return filename } -// HasPluginBlock is func (c GitHub) HasPluginBlock() bool { return c.Plugin != nil } -// HasCommandBlock is func (c GitHub) HasCommandBlock() bool { return c.Command != nil } -// HasReleaseBlock is func (c GitHub) HasReleaseBlock() bool { return c.Release != nil } -// GetPluginBlock is func (c GitHub) GetPluginBlock() Plugin { if c.HasPluginBlock() { return *c.Plugin @@ -343,7 +365,6 @@ func (c GitHub) GetPluginBlock() Plugin { return Plugin{} } -// GetCommandBlock is func (c GitHub) GetCommandBlock() Command { if c.HasCommandBlock() { return *c.Command @@ -351,7 +372,6 @@ func (c GitHub) GetCommandBlock() Command { return Command{} } -// Uninstall is func (c GitHub) Uninstall(ctx context.Context) error { var errs errors.Errors @@ -376,7 +396,6 @@ func (c GitHub) Uninstall(ctx context.Context) error { } delete(c.GetHome(), &errs) - return errs.ErrorOrNil() } @@ -387,9 +406,6 @@ func (c GitHub) GetName() string { // GetHome returns a path func (c GitHub) GetHome() string { - if c.IsGHExtension() { - return filepath.Join(os.Getenv("HOME"), ".local", "share", "gh", "extensions", c.Repo) - } return filepath.Join(os.Getenv("HOME"), ".afx", "github.com", c.Owner, c.Repo) } @@ -486,5 +502,9 @@ func (c GitHub) checkUpdates(ctx context.Context) (report, error) { } func (c GitHub) IsGHExtension() bool { - return c.As != nil && c.As.GHExtension != nil + return c.As != nil && c.As.GHExtension != nil && c.As.GHExtension.Name != "" +} + +func ValidateGHExtension(fl validator.FieldLevel) bool { + return fl.Field().String() == "" || strings.HasPrefix(fl.Field().String(), "gh-") } From 56337c59cfb5ff91d76d1012a5de93ef475e791c Mon Sep 17 00:00:00 2001 From: b4b4r07 Date: Sat, 18 Mar 2023 21:11:36 +0900 Subject: [PATCH 5/8] Update docs --- docs/configuration/package/github.md | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/configuration/package/github.md b/docs/configuration/package/github.md index d2e4896..794f34a 100644 --- a/docs/configuration/package/github.md +++ b/docs/configuration/package/github.md @@ -89,24 +89,30 @@ Limit fetching to the specified number of commits from the tip of each remote br ### as -Type | Default ----|--- -string | `""` +Key | Type | Default +---|---|--- +gh-extension | Object | `null` -Available arguments: - -- gh-extension +Change the installation behavior of the packages based on specified package type. In current afx, all packages are based on where it's hosted e.g. `github`. Almost all cases are not problem on that but some package types (such as "brew" or "gh extension") will be able to do more easily if there is a dedicated parameters to install the packages. In this `as` section, it expands more this installation method. Some of package types (especially "brew") will be coming soon in near future. === "gh-extension" Install a package as [gh extension](https://github.blog/2023-01-13-new-github-cli-extension-tools/). Officially gh extensions can be installed with `gh extension install owern/repo` command ([guide](https://cli.github.com/manual/gh_extension_install)) but it's difficult to manage what we downloaded as code. In afx, by handling them as the same as other packages, it allows us to codenize them. + Key | Type | Default + ---|---|--- + name | string | (required) + rename-to | string | `""` + ```yaml - - name: dlvhdr/gh-dash - description: A beautiful CLI dashboard for GitHub - owner: dlvhdr - repo: gh-dash - as: gh-extension + - name: yusukebe/gh-markdown-preview + description: GitHub CLI extension to preview Markdown looks like GitHub. + owner: yusukebe + repo: gh-markdown-preview + as: + gh-extension: + name: gh-markdown-preview + rename-to: gh-md # markdown-preview is long so rename it to shorten. ``` ### release.name From 9178a63122cee834684ccb3a89500d1f5e5ab508 Mon Sep 17 00:00:00 2001 From: b4b4r07 Date: Sat, 18 Mar 2023 21:38:31 +0900 Subject: [PATCH 6/8] Allow tag --- pkg/config/github.go | 89 ++++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/pkg/config/github.go b/pkg/config/github.go index 7c7d2fd..360d1c7 100644 --- a/pkg/config/github.go +++ b/pkg/config/github.go @@ -51,42 +51,10 @@ type GitHubAs struct { type GHExtension struct { Name string `yaml:"name" validate:"required,startswith=gh-"` + Tag string `yaml:"tag"` RenameTo string `yaml:"rename-to" validate:"gh-extension,excludesall=/"` } -func (gh GHExtension) GetHome() string { - base := filepath.Join(os.Getenv("HOME"), ".local", "share", "gh", "extensions") - var ext string - if gh.RenameTo == "" { - ext = filepath.Join(base, gh.Name) - } else { - ext = filepath.Join(base, gh.RenameTo) - } - return ext -} - -func (gh GHExtension) Install(home string) error { - ghHome := gh.GetHome() - // ensure to create the parent dir of each gh extension's path - os.MkdirAll(filepath.Dir(ghHome), os.ModePerm) - - // make alias - if gh.RenameTo != "" { - if err := os.Symlink( - filepath.Join(home, gh.Name), - filepath.Join(home, gh.RenameTo), - ); err != nil { - return fmt.Errorf("%w: failed to symlink as alise", err) - } - } - - // install - if err := os.Symlink(home, ghHome); err != nil { - return fmt.Errorf("%w: failed to symlink as install", err) - } - return nil -} - type GitHubOption struct { Depth int `yaml:"depth"` } @@ -205,7 +173,7 @@ func (c GitHub) Install(ctx context.Context, status chan<- Status) error { return err } case c.Release != nil: - err := c.InstallFromRelease(ctx) + err := c.InstallFromRelease(ctx, c.Owner, c.Repo, c.GetReleaseTag()) if err != nil { err = errors.Wrapf(err, "%s: failed to get from release", c.Name) status <- Status{Name: c.GetName(), Done: true, Err: true} @@ -216,16 +184,16 @@ func (c GitHub) Install(ctx context.Context, status chan<- Status) error { var errs errors.Errors if c.IsGHExtension() { + gh := c.As.GHExtension available, _ := github.HasRelease(http.DefaultClient, c.Owner, c.Repo) if available { - err := c.InstallFromRelease(ctx) + err := c.InstallFromRelease(ctx, c.Owner, c.Repo, gh.GetTag()) if err != nil { err = errors.Wrapf(err, "%s: failed to get from release", c.Name) status <- Status{Name: c.GetName(), Done: true, Err: true} return err } } - gh := c.As.GHExtension errs.Append( gh.Install(c.GetHome()), ) @@ -273,12 +241,13 @@ func (c GitHub) GetReleaseTag() string { } // InstallFromRelease runs install from GitHub release, from not repository -func (c GitHub) InstallFromRelease(ctx context.Context) error { +func (c GitHub) InstallFromRelease(ctx context.Context, owner, repo, tag string) error { ctx, cancel := context.WithCancel(ctx) defer cancel() + log.Printf("[DEBUG] install from release: %s/%s (%s)", owner, repo, tag) release, err := github.NewRelease( - ctx, c.Owner, c.Repo, c.GetReleaseTag(), + ctx, owner, repo, tag, github.WithWorkdir(c.GetHome()), github.WithFilter(func(filename string) github.FilterFunc { if filename == "" { @@ -501,10 +470,50 @@ func (c GitHub) checkUpdates(ctx context.Context) (report, error) { } } +func ValidateGHExtension(fl validator.FieldLevel) bool { + return fl.Field().String() == "" || strings.HasPrefix(fl.Field().String(), "gh-") +} + func (c GitHub) IsGHExtension() bool { return c.As != nil && c.As.GHExtension != nil && c.As.GHExtension.Name != "" } -func ValidateGHExtension(fl validator.FieldLevel) bool { - return fl.Field().String() == "" || strings.HasPrefix(fl.Field().String(), "gh-") +func (gh GHExtension) GetHome() string { + base := filepath.Join(os.Getenv("HOME"), ".local", "share", "gh", "extensions") + var ext string + if gh.RenameTo == "" { + ext = filepath.Join(base, gh.Name) + } else { + ext = filepath.Join(base, gh.RenameTo) + } + return ext +} + +func (gh GHExtension) GetTag() string { + if gh.Tag != "" { + return gh.Tag + } + return "latest" +} + +func (gh GHExtension) Install(home string) error { + ghHome := gh.GetHome() + // ensure to create the parent dir of each gh extension's path + os.MkdirAll(filepath.Dir(ghHome), os.ModePerm) + + // make alias + if gh.RenameTo != "" { + if err := os.Symlink( + filepath.Join(home, gh.Name), + filepath.Join(home, gh.RenameTo), + ); err != nil { + return fmt.Errorf("%w: failed to symlink as alise", err) + } + } + + // install + if err := os.Symlink(home, ghHome); err != nil { + return fmt.Errorf("%w: failed to symlink as install", err) + } + return nil } From 8b7d9aad20c1b56cd3680accac801642bdda9a26 Mon Sep 17 00:00:00 2001 From: b4b4r07 Date: Sat, 18 Mar 2023 22:47:45 +0900 Subject: [PATCH 7/8] Add overwrite --- pkg/config/github.go | 1 + pkg/github/github.go | 29 +++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/pkg/config/github.go b/pkg/config/github.go index 360d1c7..b79c851 100644 --- a/pkg/config/github.go +++ b/pkg/config/github.go @@ -248,6 +248,7 @@ func (c GitHub) InstallFromRelease(ctx context.Context, owner, repo, tag string) release, err := github.NewRelease( ctx, owner, repo, tag, + github.WithOverwrite(), github.WithWorkdir(c.GetHome()), github.WithFilter(func(filename string) github.FilterFunc { if filename == "" { diff --git a/pkg/github/github.go b/pkg/github/github.go index 9183e1e..52b6edb 100644 --- a/pkg/github/github.go +++ b/pkg/github/github.go @@ -27,10 +27,11 @@ type Release struct { Tag string Assets Assets - client *Client - workdir string - verbose bool - filter func(Assets) *Asset + client *Client + workdir string + verbose bool + overwrite bool + filter func(Assets) *Asset } // Asset represents GitHub release's asset. @@ -89,6 +90,12 @@ type Option func(r *Release) type FilterFunc func(assets Assets) *Asset +func WithOverwrite() Option { + return func(r *Release) { + r.overwrite = true + } +} + func WithWorkdir(workdir string) Option { return func(r *Release) { r.workdir = workdir @@ -292,11 +299,17 @@ func (r *Release) Unarchive(asset Asset) error { // because this logic renames a binary of 'jq-1.6' to 'jq' // target := filepath.Join(r.workdir, r.Name) - if _, err := os.Stat(target); err != nil { - log.Printf("[DEBUG] renamed from %s to %s", archive, target) - os.Rename(archive, target) - os.Chmod(target, 0755) + if _, err := os.Stat(target); err == nil { + if r.overwrite { + log.Printf("[WARN] %s: already exist. but overwrite", target) + } else { + log.Printf("[WARN] %s: already exist. so skip to unarchive", target) + return nil + } } + log.Printf("[DEBUG] renamed from %s to %s", archive, target) + os.Rename(archive, target) + os.Chmod(target, 0755) return nil } From 614a7b6134b453dce3eafb5ef44b59590c915f80 Mon Sep 17 00:00:00 2001 From: b4b4r07 Date: Sun, 19 Mar 2023 01:17:39 +0900 Subject: [PATCH 8/8] Refactor --- docs/configuration/package/github.md | 2 + go.mod | 1 + go.sum | 1 + pkg/config/github.go | 114 ++++++++++++++++++++++----- pkg/github/github.go | 12 ++- 5 files changed, 106 insertions(+), 24 deletions(-) diff --git a/docs/configuration/package/github.md b/docs/configuration/package/github.md index 794f34a..2936de4 100644 --- a/docs/configuration/package/github.md +++ b/docs/configuration/package/github.md @@ -102,6 +102,7 @@ Change the installation behavior of the packages based on specified package type Key | Type | Default ---|---|--- name | string | (required) + tag | string | `latest` rename-to | string | `""` ```yaml @@ -112,6 +113,7 @@ Change the installation behavior of the packages based on specified package type as: gh-extension: name: gh-markdown-preview + tag: v1.4.0 rename-to: gh-md # markdown-preview is long so rename it to shorten. ``` diff --git a/go.mod b/go.mod index fa56bdc..1b44c55 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( golang.org/x/sync v0.1.0 golang.org/x/term v0.6.0 gopkg.in/src-d/go-git.v4 v4.13.1 + gopkg.in/yaml.v2 v2.2.2 ) require ( diff --git a/go.sum b/go.sum index eedb474..6c0b027 100644 --- a/go.sum +++ b/go.sum @@ -265,6 +265,7 @@ gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/config/github.go b/pkg/config/github.go index b79c851..5012651 100644 --- a/pkg/config/github.go +++ b/pkg/config/github.go @@ -13,6 +13,7 @@ import ( git "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/config" "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/yaml.v2" "github.com/Masterminds/semver" "github.com/b4b4r07/afx/pkg/data" @@ -173,7 +174,7 @@ func (c GitHub) Install(ctx context.Context, status chan<- Status) error { return err } case c.Release != nil: - err := c.InstallFromRelease(ctx, c.Owner, c.Repo, c.GetReleaseTag()) + err := c.InstallFromRelease(ctx) if err != nil { err = errors.Wrapf(err, "%s: failed to get from release", c.Name) status <- Status{Name: c.GetName(), Done: true, Err: true} @@ -185,18 +186,12 @@ func (c GitHub) Install(ctx context.Context, status chan<- Status) error { if c.IsGHExtension() { gh := c.As.GHExtension - available, _ := github.HasRelease(http.DefaultClient, c.Owner, c.Repo) - if available { - err := c.InstallFromRelease(ctx, c.Owner, c.Repo, gh.GetTag()) - if err != nil { - err = errors.Wrapf(err, "%s: failed to get from release", c.Name) - status <- Status{Name: c.GetName(), Done: true, Err: true} - return err - } + err := gh.Install(ctx, c.Owner, c.Repo, gh.GetTag()) + if err != nil { + err = errors.Wrapf(err, "%s: failed to get from release", c.Name) + status <- Status{Name: c.GetName(), Done: true, Err: true} + return err } - errs.Append( - gh.Install(c.GetHome()), - ) } if c.HasPluginBlock() { @@ -241,14 +236,15 @@ func (c GitHub) GetReleaseTag() string { } // InstallFromRelease runs install from GitHub release, from not repository -func (c GitHub) InstallFromRelease(ctx context.Context, owner, repo, tag string) error { +func (c GitHub) InstallFromRelease(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) defer cancel() + + owner, repo, tag := c.Owner, c.Repo, c.GetReleaseTag() log.Printf("[DEBUG] install from release: %s/%s (%s)", owner, repo, tag) release, err := github.NewRelease( ctx, owner, repo, tag, - github.WithOverwrite(), github.WithWorkdir(c.GetHome()), github.WithFilter(func(filename string) github.FilterFunc { if filename == "" { @@ -376,6 +372,9 @@ func (c GitHub) GetName() string { // GetHome returns a path func (c GitHub) GetHome() string { + if c.IsGHExtension() { + return c.As.GHExtension.GetHome() + } return filepath.Join(os.Getenv("HOME"), ".afx", "github.com", c.Owner, c.Repo) } @@ -479,6 +478,15 @@ func (c GitHub) IsGHExtension() bool { return c.As != nil && c.As.GHExtension != nil && c.As.GHExtension.Name != "" } +type ghManifest struct { + Owner string `yaml:"owner"` + Name string `yaml:"name"` + Host string `yaml:"host"` + Tag string `yaml:"tag"` + IsPinned bool `yaml:"ispinned"` + Path string `yaml:"path"` +} + func (gh GHExtension) GetHome() string { base := filepath.Join(os.Getenv("HOME"), ".local", "share", "gh", "extensions") var ext string @@ -497,24 +505,88 @@ func (gh GHExtension) GetTag() string { return "latest" } -func (gh GHExtension) Install(home string) error { +func (gh GHExtension) Install(ctx context.Context, owner, repo, tag string) error { + available, _ := github.HasRelease(http.DefaultClient, owner, repo, tag) + if available { + err := gh.InstallFromRelease(ctx, owner, repo, tag) + if err != nil { + return fmt.Errorf("%w: %s: failed to get gh extension", err, gh.Name) + } + } + ghHome := gh.GetHome() // ensure to create the parent dir of each gh extension's path - os.MkdirAll(filepath.Dir(ghHome), os.ModePerm) + _ = os.MkdirAll(filepath.Dir(ghHome), os.ModePerm) // make alias if gh.RenameTo != "" { if err := os.Symlink( - filepath.Join(home, gh.Name), - filepath.Join(home, gh.RenameTo), + filepath.Join(ghHome, gh.Name), + filepath.Join(ghHome, gh.RenameTo), ); err != nil { return fmt.Errorf("%w: failed to symlink as alise", err) } } - // install - if err := os.Symlink(home, ghHome); err != nil { - return fmt.Errorf("%w: failed to symlink as install", err) + if gh.GetTag() == "latest" { + // in case of not making manifest yaml + return nil + } + + return gh.makeManifest(owner) +} + +func (gh GHExtension) InstallFromRelease(ctx context.Context, owner, repo, tag string) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + log.Printf("[DEBUG] install from release: %s/%s (%s)", owner, repo, tag) + release, err := github.NewRelease( + ctx, owner, repo, tag, + github.WithOverwrite(), + github.WithWorkdir(gh.GetHome()), + ) + if err != nil { + return err + } + + asset, err := release.Download(ctx) + if err != nil { + return errors.Wrapf(err, "%s: failed to download", release.Name) + } + + if err := release.Unarchive(asset); err != nil { + return errors.Wrapf(err, "%s: failed to unarchive", release.Name) + } + + return nil +} + +func (gh GHExtension) makeManifest(owner string) error { + // https://github.com/cli/cli/blob/c9a2d85793c4cef026d5bb941b3ac4121c81ae10/pkg/cmd/extension/manager.go#L424-L451 + manifest := ghManifest{ + Name: gh.Name, + Owner: owner, + Host: "github.com", + Path: gh.GetHome(), + Tag: gh.GetTag(), + IsPinned: false, + } + bs, err := yaml.Marshal(manifest) + if err != nil { + return fmt.Errorf("failed to serialize manifest: %w", err) + } + + manifestPath := filepath.Join(gh.GetHome(), "manifest.yml") + f, err := os.OpenFile(manifestPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("failed to open manifest for writing: %w", err) + } + defer f.Close() + + _, err = f.Write(bs) + if err != nil { + return fmt.Errorf("failed write manifest file: %w", err) } return nil } diff --git a/pkg/github/github.go b/pkg/github/github.go index 52b6edb..f295408 100644 --- a/pkg/github/github.go +++ b/pkg/github/github.go @@ -390,10 +390,16 @@ func (r *Release) Install(to string) error { }) } -func HasRelease(httpClient *http.Client, owner, repo string) (bool, error) { +func HasRelease(httpClient *http.Client, owner, repo, tag string) (bool, error) { // https://github.com/cli/cli/blob/9596fd5368cdbd30d08555266890a2312e22eba9/pkg/cmd/extension/http.go#L110 - url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo) - req, err := http.NewRequest("GET", url, nil) + releaseURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", owner, repo) + switch tag { + case "latest", "": + releaseURL += "/latest" + default: + releaseURL += fmt.Sprintf("/tags/%s", tag) + } + req, err := http.NewRequest("GET", releaseURL, nil) if err != nil { return false, err }