diff --git a/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubRelease.md b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubRelease.md new file mode 100644 index 00000000000..b522999c412 --- /dev/null +++ b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubRelease.md @@ -0,0 +1,17 @@ +# `gitHubRelease` *owner-repo* *version* + +`gitHubRelease` calls the GitHub API to retrieve the latest releases about +the given *owner-repo*, It iterates through all the versions of the release, +fetching the first entry equal to *version* + +It then returns structured data as defined by the [GitHub Go API +bindings](https://pkg.go.dev/github.com/google/go-github/v63/github#RepositoryRelease). + +Calls to `gitHubRelease` are cached so calling `gitHubRelease` with +the same *owner-repo* *version* will only result in one call to the GitHub API. + +!!! example + + ``` + {{ (gitHubRelease "docker/compose" "v2.29.1").TagName }} + ``` diff --git a/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubReleaseAssetURL.md b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubReleaseAssetURL.md new file mode 100644 index 00000000000..ed91b917a72 --- /dev/null +++ b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubReleaseAssetURL.md @@ -0,0 +1,21 @@ +# `gitHubReleaseAssetURL` *owner-repo* *version* *pattern* + +`gitHubReleaseAssetURL` calls the GitHub API to retrieve the latest +releases about the given *owner-repo*, returning structured data as defined by +the [GitHub Go API +bindings](https://pkg.go.dev/github.com/google/go-github/v63/github#RepositoryRelease). +It iterates through all the versions of the release, returning the first entry equal to *version*. +It then iterates through all the release's assets, returning the first one that +matches *pattern*. *pattern* is a shell pattern as [described in +`path.Match`](https://pkg.go.dev/path#Match). + +Calls to `gitHubReleaseAssetURL` are cached so calling +`gitHubReleaseAssetURL` with the same *owner-repo* will only result in one +call to the GitHub API. + +!!! example + + ``` + {{ gitHubReleaseAssetURL "FiloSottile/age" "age v1.2.0" (printf "age-*-%s-%s.tar.gz" .chezmoi.os .chezmoi.arch) }} + {{ gitHubReleaseAssetURL "twpayne/chezmoi" "v2.50.0" (printf "chezmoi-%s-%s" .chezmoi.os .chezmoi.arch) }} + ``` diff --git a/assets/chezmoi.io/mkdocs.yml b/assets/chezmoi.io/mkdocs.yml index 31a9df256d9..e0ec98ebca4 100644 --- a/assets/chezmoi.io/mkdocs.yml +++ b/assets/chezmoi.io/mkdocs.yml @@ -225,7 +225,9 @@ nav: - reference/templates/github-functions/index.md - gitHubKeys: reference/templates/github-functions/gitHubKeys.md - gitHubLatestRelease: reference/templates/github-functions/gitHubLatestRelease.md + - gitHubRelease: reference/templates/github-functions/gitHubRelease.md - gitHubLatestReleaseAssetURL: reference/templates/github-functions/gitHubLatestReleaseAssetURL.md + - gitHubReleaseAssetURL: reference/templates/github-functions/gitHubReleaseAssetURL.md - gitHubLatestTag: reference/templates/github-functions/gitHubLatestTag.md - gitHubReleases: reference/templates/github-functions/gitHubReleases.md - gitHubTags: reference/templates/github-functions/gitHubTags.md diff --git a/internal/cmd/config.go b/internal/cmd/config.go index b1a512a9493..5d9fcf63f02 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -436,7 +436,9 @@ func newConfig(options ...configOption) (*Config, error) { "fromYaml": c.fromYamlTemplateFunc, "gitHubKeys": c.gitHubKeysTemplateFunc, "gitHubLatestRelease": c.gitHubLatestReleaseTemplateFunc, + "gitHubRelease": c.gitHubReleaseTemplateFunc, "gitHubLatestReleaseAssetURL": c.gitHubLatestReleaseAssetURLTemplateFunc, + "gitHubReleaseAssetURL": c.gitHubReleaseAssetURLTemplateFunc, "gitHubLatestTag": c.gitHubLatestTagTemplateFunc, "gitHubReleases": c.gitHubReleasesTemplateFunc, "gitHubTags": c.gitHubTagsTemplateFunc, diff --git a/internal/cmd/githubtemplatefuncs.go b/internal/cmd/githubtemplatefuncs.go index 6f918987282..0b022d349fe 100644 --- a/internal/cmd/githubtemplatefuncs.go +++ b/internal/cmd/githubtemplatefuncs.go @@ -37,19 +37,21 @@ type gitHubTagsState struct { } var ( - gitHubKeysStateBucket = []byte("gitHubLatestKeysState") - gitHubLatestReleaseStateBucket = []byte("gitHubLatestReleaseState") - gitHubReleasesStateBucket = []byte("gitHubReleasesState") - gitHubTagsStateBucket = []byte("gitHubTagsState") + gitHubKeysStateBucket = []byte("gitHubLatestKeysState") + gitHubVersionReleaseStateBucket = []byte("gitHubVersionReleaseState") + gitHubLatestReleaseStateBucket = []byte("gitHubLatestReleaseState") + gitHubReleasesStateBucket = []byte("gitHubReleasesState") + gitHubTagsStateBucket = []byte("gitHubTagsState") ) type gitHubData struct { - client *github.Client - clientErr error - keysCache map[string][]*github.Key - latestReleaseCache map[string]map[string]*github.RepositoryRelease - releasesCache map[string]map[string][]*github.RepositoryRelease - tagsCache map[string]map[string][]*github.RepositoryTag + client *github.Client + clientErr error + keysCache map[string][]*github.Key + versionReleaseCache map[string]map[string]map[string]*github.RepositoryRelease + latestReleaseCache map[string]map[string]*github.RepositoryRelease + releasesCache map[string]map[string][]*github.RepositoryRelease + tagsCache map[string]map[string][]*github.RepositoryTag } func (c *Config) gitHubKeysTemplateFunc(user string) []*github.Key { @@ -108,11 +110,7 @@ func (c *Config) gitHubKeysTemplateFunc(user string) []*github.Key { return allKeys } -func (c *Config) gitHubLatestReleaseAssetURLTemplateFunc(ownerRepo, pattern string) string { - release, err := c.gitHubLatestRelease(ownerRepo) - if err != nil { - panic(err) - } +func (c *Config) githubMatchingReleaseAssetURL(release *github.RepositoryRelease, pattern string) string { for _, asset := range release.Assets { if asset.Name == nil { continue @@ -127,6 +125,79 @@ func (c *Config) gitHubLatestReleaseAssetURLTemplateFunc(ownerRepo, pattern stri return "" } +func (c *Config) gitHubLatestReleaseAssetURLTemplateFunc(ownerRepo, pattern string) string { + release, err := c.gitHubLatestRelease(ownerRepo) + if err != nil { + panic(err) + } + return c.githubMatchingReleaseAssetURL(release, pattern) +} + +func (c *Config) gitHubReleaseAssetURLTemplateFunc(ownerRepo, version, pattern string) string { + release, err := c.gitHubRelease(ownerRepo, version) + if err != nil { + panic(err) + } + return c.githubMatchingReleaseAssetURL(release, pattern) +} + +func (c *Config) gitHubRelease(ownerRepo, version string) (*github.RepositoryRelease, error) { + owner, repo, err := gitHubSplitOwnerRepo(ownerRepo) + if err != nil { + return nil, err + } + + if c.gitHub.versionReleaseCache == nil { + c.gitHub.versionReleaseCache = make(map[string]map[string]map[string]*github.RepositoryRelease) + } + if c.gitHub.versionReleaseCache[owner] == nil { + c.gitHub.versionReleaseCache[owner] = make(map[string]map[string]*github.RepositoryRelease) + } + if c.gitHub.versionReleaseCache[owner][repo] == nil { + c.gitHub.versionReleaseCache[owner][repo] = make(map[string]*github.RepositoryRelease) + } + + if release := c.gitHub.versionReleaseCache[owner][repo][version]; release != nil { + return release, nil + } + + now := time.Now() + gitHubVersionReleaseKey := []byte(owner + "/" + repo + "/" + version) + if c.GitHub.RefreshPeriod != 0 { + var gitHubVersionReleaseStateValue gitHubLatestReleaseState + switch ok, err := chezmoi.PersistentStateGet(c.persistentState, gitHubVersionReleaseStateBucket, gitHubVersionReleaseKey, &gitHubVersionReleaseStateValue); { + case err != nil: + return nil, err + case ok && now.Before(gitHubVersionReleaseStateValue.RequestedAt.Add(c.GitHub.RefreshPeriod)): + return gitHubVersionReleaseStateValue.Release, nil + } + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + gitHubClient, err := c.getGitHubClient(ctx) + if err != nil { + return nil, err + } + + release, _, err := gitHubClient.Repositories.GetReleaseByTag(ctx, owner, repo, version) + if err != nil { + return nil, err + } + + if err := chezmoi.PersistentStateSet(c.persistentState, gitHubVersionReleaseStateBucket, gitHubVersionReleaseKey, &gitHubLatestReleaseState{ + RequestedAt: now, + Release: release, + }); err != nil { + return nil, err + } + + c.gitHub.versionReleaseCache[owner][repo][version] = release + + return release, nil +} + func (c *Config) gitHubLatestRelease(ownerRepo string) (*github.RepositoryRelease, error) { owner, repo, err := gitHubSplitOwnerRepo(ownerRepo) if err != nil { @@ -188,6 +259,14 @@ func (c *Config) gitHubLatestReleaseTemplateFunc(ownerRepo string) *github.Repos return release } +func (c *Config) gitHubReleaseTemplateFunc(ownerRepo, version string) *github.RepositoryRelease { + release, err := c.gitHubRelease(ownerRepo, version) + if err != nil { + panic(err) + } + return release +} + func (c *Config) gitHubLatestTagTemplateFunc(ownerRepo string) *github.RepositoryTag { tags, err := c.getGitHubTags(ownerRepo) if err != nil { diff --git a/internal/cmd/statecmd.go b/internal/cmd/statecmd.go index 6f2034d0cec..102a7981e65 100644 --- a/internal/cmd/statecmd.go +++ b/internal/cmd/statecmd.go @@ -171,14 +171,15 @@ func (c *Config) runStateDeleteBucketCmd(cmd *cobra.Command, args []string) erro func (c *Config) runStateDumpCmd(cmd *cobra.Command, args []string) error { data, err := chezmoi.PersistentStateData(c.persistentState, map[string][]byte{ - "configState": chezmoi.ConfigStateBucket, - "entryState": chezmoi.EntryStateBucket, - "gitHubKeysState": gitHubKeysStateBucket, - "gitHubLatestReleaseState": gitHubLatestReleaseStateBucket, - "gitHubReleasesState": gitHubReleasesStateBucket, - "gitHubTagsState": gitHubTagsStateBucket, - "gitRepoExternalState": chezmoi.GitRepoExternalStateBucket, - "scriptState": chezmoi.ScriptStateBucket, + "configState": chezmoi.ConfigStateBucket, + "entryState": chezmoi.EntryStateBucket, + "gitHubKeysState": gitHubKeysStateBucket, + "gitHubLatestReleaseState": gitHubLatestReleaseStateBucket, + "gitHubVersionReleaseState": gitHubVersionReleaseStateBucket, + "gitHubReleasesState": gitHubReleasesStateBucket, + "gitHubTagsState": gitHubTagsStateBucket, + "gitRepoExternalState": chezmoi.GitRepoExternalStateBucket, + "scriptState": chezmoi.ScriptStateBucket, }) if err != nil { return err diff --git a/internal/cmd/testdata/scripts/configstate.txtar b/internal/cmd/testdata/scripts/configstate.txtar index 1cc08c5e65b..77b43adf708 100644 --- a/internal/cmd/testdata/scripts/configstate.txtar +++ b/internal/cmd/testdata/scripts/configstate.txtar @@ -71,6 +71,7 @@ gitHubKeysState: {} gitHubLatestReleaseState: {} gitHubReleasesState: {} gitHubTagsState: {} +gitHubVersionReleaseState: {} gitRepoExternalState: {} scriptState: {} -- home/user/.local/share/chezmoi/.chezmoi.toml.tmpl -- diff --git a/internal/cmd/testdata/scripts/githubtemplatefuncs.txtar b/internal/cmd/testdata/scripts/githubtemplatefuncs.txtar index 933a15f7343..79a3c93da00 100644 --- a/internal/cmd/testdata/scripts/githubtemplatefuncs.txtar +++ b/internal/cmd/testdata/scripts/githubtemplatefuncs.txtar @@ -8,6 +8,14 @@ stdout ^ssh-rsa exec chezmoi execute-template '{{ (gitHubLatestRelease "twpayne/chezmoi").TagName }}' stdout ^v2\. +# test gitHubLatestRelease template function +exec chezmoi execute-template '{{ (gitHubLatestRelease "2.51.0" "twpayne/chezmoi").TagName }}' +stdout ^v2.51.0 + +# test gitHubLatestRelease template function +exec chezmoi execute-template '{{ (gitHubLatestRelease "2.49.0" "twpayne/chezmoi").TagName }}' +stdout ^v2.49.0 + # test gitHubLatestTag template function exec chezmoi execute-template '{{ (gitHubLatestTag "twpayne/chezmoi").Name }}' stdout ^v2\. diff --git a/internal/cmd/testdata/scripts/state_unix.txtar b/internal/cmd/testdata/scripts/state_unix.txtar index b7130f4500a..93d1ab7484b 100644 --- a/internal/cmd/testdata/scripts/state_unix.txtar +++ b/internal/cmd/testdata/scripts/state_unix.txtar @@ -44,6 +44,7 @@ gitHubKeysState: {} gitHubLatestReleaseState: {} gitHubReleasesState: {} gitHubTagsState: {} +gitHubVersionReleaseState: {} gitRepoExternalState: {} scriptState: {} -- home/user/.local/share/chezmoi/run_once_script.sh -- diff --git a/internal/cmd/testdata/scripts/state_windows.txtar b/internal/cmd/testdata/scripts/state_windows.txtar index 0f936cee10c..b3e64cc8a04 100644 --- a/internal/cmd/testdata/scripts/state_windows.txtar +++ b/internal/cmd/testdata/scripts/state_windows.txtar @@ -25,6 +25,7 @@ gitHubKeysState: {} gitHubLatestReleaseState: {} gitHubReleasesState: {} gitHubTagsState: {} +gitHubVersionReleaseState: {} gitRepoExternalState: {} scriptState: {} -- home/user/.local/share/chezmoi/run_once_script.cmd --