From 3b49cc5bb7635b7e9b58662247dd017777f176ab Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Thu, 2 May 2024 22:40:03 +0200 Subject: [PATCH] feat: Support self-hosted GitHub Enterprise servers --- .../github-functions/gitHubLatestRelease.md | 8 +- .../gitHubLatestReleaseAssetURL.md | 10 +- .../github-functions/gitHubLatestTag.md | 8 +- .../github-functions/gitHubReleases.md | 8 +- .../templates/github-functions/gitHubTags.md | 8 +- .../templates/github-functions/index.md | 23 ++- internal/chezmoi/github.go | 48 +++++- internal/chezmoi/github_test.go | 34 +++++ internal/cmd/config.go | 5 + internal/cmd/doctorcmd.go | 5 +- internal/cmd/githubtemplatefuncs.go | 144 +++++++++++------- internal/cmd/upgradecmd.go | 7 +- internal/cmds/execute-template/main.go | 15 +- 13 files changed, 225 insertions(+), 98 deletions(-) create mode 100644 internal/chezmoi/github_test.go diff --git a/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestRelease.md b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestRelease.md index 64e2da2d2953..34a00550d28b 100644 --- a/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestRelease.md +++ b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestRelease.md @@ -1,12 +1,12 @@ -# `gitHubLatestRelease` *owner-repo* +# `gitHubLatestRelease` *host-owner-repo* `gitHubLatestRelease` calls the GitHub API to retrieve the latest release about -the given *owner-repo*, returning structured data as defined by the [GitHub Go -API +the given *host-owner-repo*, returning structured data as defined by the [GitHub +Go API bindings](https://pkg.go.dev/github.com/google/go-github/v57/github#RepositoryRelease). Calls to `gitHubLatestRelease` are cached so calling `gitHubLatestRelease` with -the same *owner-repo* will only result in one call to the GitHub API. +the same *host-owner-repo* will only result in one call to the GitHub API. !!! example diff --git a/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestReleaseAssetURL.md b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestReleaseAssetURL.md index a36d9a15fc44..b4da786fc386 100644 --- a/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestReleaseAssetURL.md +++ b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestReleaseAssetURL.md @@ -1,16 +1,16 @@ -# `gitHubLatestReleaseAssetURL` *owner-repo* *pattern* +# `gitHubLatestReleaseAssetURL` *host-owner-repo* *pattern* `gitHubLatestReleaseAssetURL` calls the GitHub API to retrieve the latest -release about the given *owner-repo*, returning structured data as defined by -the [GitHub Go API +release about the given *host-owner-repo*, returning structured data as defined +by the [GitHub Go API bindings](https://pkg.go.dev/github.com/google/go-github/v61/github#RepositoryRelease). 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 `gitHubLatestReleaseAssetURL` are cached so calling -`gitHubLatestReleaseAssetURL` with the same *owner-repo* will only result in one -call to the GitHub API. +`gitHubLatestReleaseAssetURL` with the same *host-owner-repo* will only result +in one call to the GitHub API. !!! example diff --git a/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestTag.md b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestTag.md index a9a711e86e38..15f99ea59032 100644 --- a/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestTag.md +++ b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubLatestTag.md @@ -1,12 +1,12 @@ -# `gitHubLatestTag` *owner-repo* +# `gitHubLatestTag` *host-owner-repo* `gitHubLatestTag` calls the GitHub API to retrieve the latest tag for the given -*owner-repo*, returning structured data as defined by the [GitHub Go API +*host-owner-repo*, returning structured data as defined by the [GitHub Go API bindings](https://pkg.go.dev/github.com/google/go-github/v57/github#RepositoryTag). Calls to `gitHubLatestTag` are cached the same as [`githubTags`](gitHubTags.md), -so calling `gitHubLatestTag` with the same *owner-repo* will only result in one -call to the GitHub API. +so calling `gitHubLatestTag` with the same *host-owner-repo* will only result in +one call to the GitHub API. !!! example diff --git a/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubReleases.md b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubReleases.md index 459f9a6668d3..646aed2a6b39 100644 --- a/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubReleases.md +++ b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubReleases.md @@ -1,12 +1,12 @@ -# `gitHubReleases` *owner-repo* +# `gitHubReleases` *host-owner-repo* `gitHubReleases` calls the GitHub API to retrieve the first page of releases for -the given *owner-repo*, returning structured data as defined by the [GitHub Go -API +the given *host-owner-repo*, returning structured data as defined by the [GitHub +Go API bindings](https://pkg.go.dev/github.com/google/go-github/v57/github#RepositoryRelease). Calls to `gitHubReleases` are cached so calling `gitHubReleases` with the same -*owner-repo* will only result in one call to the GitHub API. +*host-owner-repo* will only result in one call to the GitHub API. !!! example diff --git a/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubTags.md b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubTags.md index 259d54455433..7880af3851a6 100644 --- a/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubTags.md +++ b/assets/chezmoi.io/docs/reference/templates/github-functions/gitHubTags.md @@ -1,12 +1,12 @@ -# `gitHubTags` *owner-repo* +# `gitHubTags` *host-owner-repo* -`gitHubTags` calls the GitHub API to retrieve the first page of tags for -the given *owner-repo*, returning structured data as defined by the [GitHub Go +`gitHubTags` calls the GitHub API to retrieve the first page of tags for the +given *host-owner-repo*, returning structured data as defined by the [GitHub Go API bindings](https://pkg.go.dev/github.com/google/go-github/v57/github#RepositoryTag). Calls to `gitHubTags` are cached so calling `gitHubTags` with the -same *owner-repo* will only result in one call to the GitHub API. +same *host-owner-repo* will only result in one call to the GitHub API. !!! example diff --git a/assets/chezmoi.io/docs/reference/templates/github-functions/index.md b/assets/chezmoi.io/docs/reference/templates/github-functions/index.md index 75d625395144..00b4087aefc4 100644 --- a/assets/chezmoi.io/docs/reference/templates/github-functions/index.md +++ b/assets/chezmoi.io/docs/reference/templates/github-functions/index.md @@ -2,6 +2,13 @@ The `gitHub*` template functions return data from the GitHub API. +All functions take a *host-owner-repo* argument of the form: + + [host/]owner/repo + +The optional `host` specifies the host and defaults to `github.com` if omitted. +`owner` and `repo` specify the repository owner and name respectively. + By default, chezmoi makes anonymous GitHub API requests, which are subject to [GitHub's rate limits](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting) @@ -9,10 +16,18 @@ limits](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate- from identical GitHub API requests for the period defined in `gitHub.refreshPeriod` (default one minute). -If any of the environment variables `$CHEZMOI_GITHUB_ACCESS_TOKEN`, -`$GITHUB_ACCESS_TOKEN`, or `$GITHUB_TOKEN` are found, then the first one found -will be used to authenticate the GitHub API requests which have a higher rate -limit (currently 5,000 requests per hour per user). +For `github.com` repos, if any of the environment variables +`$CHEZMOI_GITHUB_ACCESS_TOKEN`, `$CHEZMOI_GITHUB_TOKEN`, `$GITHUB_ACCESS_TOKEN`, +or `$GITHUB_TOKEN` are found, then the first one found will be used to +authenticate the GitHub API requests which have a higher rate limit (currently +5,000 requests per hour per user). + +For non-`github.com` repos, i.e. self-host GitHub enterprise repos, chezmoi will +use an access token from the `$CHEZMOI__ACCESS_TOKEN` environment +variable, if set, where `` is the host converted to uppercase and with all +non-letter characters replaced with underscores. For example, given the host +`git.example.com`, chezmoi look for a `$CHEZMOI_GIT_EXAMPLE_COM_ACCESS_TOKEN` +environment variable. In practice, GitHub API rate limits are high enough chezmoi's caching of results mean that you should rarely need to set a token, unless you are sharing a source diff --git a/internal/chezmoi/github.go b/internal/chezmoi/github.go index 4fad9e53e522..9ef2de0cb509 100644 --- a/internal/chezmoi/github.go +++ b/internal/chezmoi/github.go @@ -11,13 +11,8 @@ import ( // NewGitHubClient returns a new github.Client configured with an access token // and a http client, if available. -func NewGitHubClient(ctx context.Context, httpClient *http.Client) *github.Client { - for _, key := range []string{ - "CHEZMOI_GITHUB_ACCESS_TOKEN", - "CHEZMOI_GITHUB_TOKEN", - "GITHUB_ACCESS_TOKEN", - "GITHUB_TOKEN", - } { +func NewGitHubClient(ctx context.Context, httpClient *http.Client, host string) (*github.Client, error) { + for _, key := range accessTokenEnvKeys(host) { if accessToken := os.Getenv(key); accessToken != "" { httpClient = oauth2.NewClient( context.WithValue(ctx, oauth2.HTTPClient, httpClient), @@ -27,5 +22,42 @@ func NewGitHubClient(ctx context.Context, httpClient *http.Client) *github.Clien break } } - return github.NewClient(httpClient) + gitHubClient := github.NewClient(httpClient) + if host == "github.com" { + return gitHubClient, nil + } + return gitHubClient.WithEnterpriseURLs( + "https://"+host+"/api/v3/", + "https://"+host+"/api/uploads/", + ) +} + +func accessTokenEnvKeys(host string) []string { + if host == "github.com" { + return []string{ + "CHEZMOI_GITHUB_ACCESS_TOKEN", + "CHEZMOI_GITHUB_TOKEN", + "GITHUB_ACCESS_TOKEN", + "GITHUB_TOKEN", + } + } + hostKey := makeHostKey(host) + return []string{ + "CHEZMOI_" + hostKey + "_ACCESS_TOKEN", + } +} + +func makeHostKey(host string) string { + hostKey := make([]byte, 0, len(host)) + for _, b := range []byte(host) { + switch { + case 'A' <= b && b <= 'Z': + hostKey = append(hostKey, b) + case 'a' <= b && b <= 'z': + hostKey = append(hostKey, b-'a'+'A') + default: + hostKey = append(hostKey, '_') + } + } + return string(hostKey) } diff --git a/internal/chezmoi/github_test.go b/internal/chezmoi/github_test.go new file mode 100644 index 000000000000..6ca4c6be34b1 --- /dev/null +++ b/internal/chezmoi/github_test.go @@ -0,0 +1,34 @@ +package chezmoi + +import ( + "testing" + + "github.com/alecthomas/assert/v2" +) + +func TestAccessTokenEnvKeys(t *testing.T) { + for _, tc := range []struct { + host string + expected []string + }{ + { + host: "github.com", + expected: []string{ + "CHEZMOI_GITHUB_ACCESS_TOKEN", + "CHEZMOI_GITHUB_TOKEN", + "GITHUB_ACCESS_TOKEN", + "GITHUB_TOKEN", + }, + }, + { + host: "git.example.com", + expected: []string{ + "CHEZMOI_GIT_EXAMPLE_COM_ACCESS_TOKEN", + }, + }, + } { + t.Run(tc.host, func(t *testing.T) { + assert.Equal(t, tc.expected, accessTokenEnvKeys(tc.host)) + }) + } +} diff --git a/internal/cmd/config.go b/internal/cmd/config.go index 6bc4525bff1d..0609657470fc 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -340,6 +340,11 @@ func newConfig(options ...configOption) (*Config, error) { homeDir: userHomeDir, templateFuncs: sprig.TxtFuncMap(), + // Password manager data. + gitHub: gitHubData{ + clientsByHost: make(map[string]gitHubClientResult), + }, + // Command configurations. apply: applyCmdConfig{ filter: chezmoi.NewEntryTypeFilter(chezmoi.EntryTypesAll, chezmoi.EntryTypesNone), diff --git a/internal/cmd/doctorcmd.go b/internal/cmd/doctorcmd.go index aae3f15f2be4..1a068a6e73cd 100644 --- a/internal/cmd/doctorcmd.go +++ b/internal/cmd/doctorcmd.go @@ -659,7 +659,10 @@ func (c *latestVersionCheck) Run(system chezmoi.System, homeDirAbsPath chezmoi.A ctx := context.Background() - gitHubClient := chezmoi.NewGitHubClient(ctx, c.httpClient) + gitHubClient, err := chezmoi.NewGitHubClient(ctx, c.httpClient, "github.com") + if err != nil { + return checkResultFailed, err.Error() + } rr, _, err := gitHubClient.Repositories.GetLatestRelease(ctx, "twpayne", "chezmoi") var rateLimitErr *github.RateLimitError var abuseRateLimitErr *github.AbuseRateLimitError diff --git a/internal/cmd/githubtemplatefuncs.go b/internal/cmd/githubtemplatefuncs.go index 9d9a52d40752..df1c8e0089e7 100644 --- a/internal/cmd/githubtemplatefuncs.go +++ b/internal/cmd/githubtemplatefuncs.go @@ -43,13 +43,23 @@ var ( gitHubTagsStateBucket = []byte("gitHubTagsState") ) +type gitHubHostOwnerRepo struct { + Host string + Owner string + Repo string +} + +type gitHubClientResult struct { + client *github.Client + err error +} + type gitHubData struct { - client *github.Client - clientErr error + clientsByHost map[string]gitHubClientResult 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 + latestReleaseCache map[gitHubHostOwnerRepo]*github.RepositoryRelease + releasesCache map[gitHubHostOwnerRepo][]*github.RepositoryRelease + tagsCache map[gitHubHostOwnerRepo][]*github.RepositoryTag } func (c *Config) gitHubKeysTemplateFunc(user string) []*github.Key { @@ -72,7 +82,7 @@ func (c *Config) gitHubKeysTemplateFunc(user string) []*github.Key { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - gitHubClient, err := c.getGitHubClient(ctx) + gitHubClient, err := c.getGitHubClient(ctx, "github.com") if err != nil { panic(err) } @@ -108,8 +118,8 @@ func (c *Config) gitHubKeysTemplateFunc(user string) []*github.Key { return allKeys } -func (c *Config) gitHubLatestReleaseAssetURLTemplateFunc(ownerRepo, pattern string) string { - release, err := c.gitHubLatestRelease(ownerRepo) +func (c *Config) gitHubLatestReleaseAssetURLTemplateFunc(hostOwnerRepo, pattern string) string { + release, err := c.gitHubLatestRelease(hostOwnerRepo) if err != nil { panic(err) } @@ -127,18 +137,18 @@ func (c *Config) gitHubLatestReleaseAssetURLTemplateFunc(ownerRepo, pattern stri return "" } -func (c *Config) gitHubLatestRelease(ownerRepo string) (*github.RepositoryRelease, error) { - owner, repo, err := gitHubSplitOwnerRepo(ownerRepo) +func (c *Config) gitHubLatestRelease(hostOwnerRepo string) (*github.RepositoryRelease, error) { + hor, err := gitHubSplitHostOwnerRepo(hostOwnerRepo) if err != nil { return nil, err } - if release := c.gitHub.latestReleaseCache[owner][repo]; release != nil { + if release := c.gitHub.latestReleaseCache[hor]; release != nil { return release, nil } now := time.Now() - gitHubLatestReleaseKey := []byte(owner + "/" + repo) + gitHubLatestReleaseKey := hor.Key() if c.GitHub.RefreshPeriod != 0 { var gitHubLatestReleaseStateValue gitHubLatestReleaseState switch ok, err := chezmoi.PersistentStateGet(c.persistentState, gitHubLatestReleaseStateBucket, gitHubLatestReleaseKey, &gitHubLatestReleaseStateValue); { @@ -152,12 +162,12 @@ func (c *Config) gitHubLatestRelease(ownerRepo string) (*github.RepositoryReleas ctx, cancel := context.WithCancel(context.Background()) defer cancel() - gitHubClient, err := c.getGitHubClient(ctx) + gitHubClient, err := c.getGitHubClient(ctx, hor.Host) if err != nil { return nil, err } - release, _, err := gitHubClient.Repositories.GetLatestRelease(ctx, owner, repo) + release, _, err := gitHubClient.Repositories.GetLatestRelease(ctx, hor.Owner, hor.Repo) if err != nil { return nil, err } @@ -170,26 +180,23 @@ func (c *Config) gitHubLatestRelease(ownerRepo string) (*github.RepositoryReleas } if c.gitHub.latestReleaseCache == nil { - c.gitHub.latestReleaseCache = make(map[string]map[string]*github.RepositoryRelease) + c.gitHub.latestReleaseCache = make(map[gitHubHostOwnerRepo]*github.RepositoryRelease) } - if c.gitHub.latestReleaseCache[owner] == nil { - c.gitHub.latestReleaseCache[owner] = make(map[string]*github.RepositoryRelease) - } - c.gitHub.latestReleaseCache[owner][repo] = release + c.gitHub.latestReleaseCache[hor] = release return release, nil } -func (c *Config) gitHubLatestReleaseTemplateFunc(ownerRepo string) *github.RepositoryRelease { - release, err := c.gitHubLatestRelease(ownerRepo) +func (c *Config) gitHubLatestReleaseTemplateFunc(hostOwnerRepo string) *github.RepositoryRelease { + release, err := c.gitHubLatestRelease(hostOwnerRepo) if err != nil { panic(err) } return release } -func (c *Config) gitHubLatestTagTemplateFunc(ownerRepo string) *github.RepositoryTag { - tags, err := c.getGitHubTags(ownerRepo) +func (c *Config) gitHubLatestTagTemplateFunc(hostOwnerRepo string) *github.RepositoryTag { + tags, err := c.getGitHubTags(hostOwnerRepo) if err != nil { panic(err) } @@ -201,18 +208,18 @@ func (c *Config) gitHubLatestTagTemplateFunc(ownerRepo string) *github.Repositor return nil } -func (c *Config) gitHubReleasesTemplateFunc(ownerRepo string) []*github.RepositoryRelease { - owner, repo, err := gitHubSplitOwnerRepo(ownerRepo) +func (c *Config) gitHubReleasesTemplateFunc(hostOwnerRepo string) []*github.RepositoryRelease { + hor, err := gitHubSplitHostOwnerRepo(hostOwnerRepo) if err != nil { panic(err) } - if releases := c.gitHub.releasesCache[owner][repo]; releases != nil { + if releases := c.gitHub.releasesCache[hor]; releases != nil { return releases } now := time.Now() - gitHubReleasesKey := []byte(owner + "/" + repo) + gitHubReleasesKey := hor.Key() if c.GitHub.RefreshPeriod != 0 { var gitHubReleasesStateValue gitHubReleasesState switch ok, err := chezmoi.PersistentStateGet(c.persistentState, gitHubReleasesStateBucket, gitHubReleasesKey, &gitHubReleasesStateValue); { @@ -226,12 +233,12 @@ func (c *Config) gitHubReleasesTemplateFunc(ownerRepo string) []*github.Reposito ctx, cancel := context.WithCancel(context.Background()) defer cancel() - gitHubClient, err := c.getGitHubClient(ctx) + gitHubClient, err := c.getGitHubClient(ctx, hor.Host) if err != nil { panic(err) } - releases, _, err := gitHubClient.Repositories.ListReleases(ctx, owner, repo, nil) + releases, _, err := gitHubClient.Repositories.ListReleases(ctx, hor.Owner, hor.Repo, nil) if err != nil { panic(err) } @@ -244,18 +251,15 @@ func (c *Config) gitHubReleasesTemplateFunc(ownerRepo string) []*github.Reposito } if c.gitHub.releasesCache == nil { - c.gitHub.releasesCache = make(map[string]map[string][]*github.RepositoryRelease) - } - if c.gitHub.releasesCache[owner] == nil { - c.gitHub.releasesCache[owner] = make(map[string][]*github.RepositoryRelease) + c.gitHub.releasesCache = make(map[gitHubHostOwnerRepo][]*github.RepositoryRelease) } - c.gitHub.releasesCache[owner][repo] = releases + c.gitHub.releasesCache[hor] = releases return releases } -func (c *Config) gitHubTagsTemplateFunc(ownerRepo string) []*github.RepositoryTag { - tags, err := c.getGitHubTags(ownerRepo) +func (c *Config) gitHubTagsTemplateFunc(hostOwnerRepo string) []*github.RepositoryTag { + tags, err := c.getGitHubTags(hostOwnerRepo) if err != nil { panic(err) } @@ -263,18 +267,18 @@ func (c *Config) gitHubTagsTemplateFunc(ownerRepo string) []*github.RepositoryTa return tags } -func (c *Config) getGitHubTags(ownerRepo string) ([]*github.RepositoryTag, error) { - owner, repo, err := gitHubSplitOwnerRepo(ownerRepo) +func (c *Config) getGitHubTags(hostOwnerRepo string) ([]*github.RepositoryTag, error) { + hor, err := gitHubSplitHostOwnerRepo(hostOwnerRepo) if err != nil { return nil, err } - if tags := c.gitHub.tagsCache[owner][repo]; tags != nil { + if tags := c.gitHub.tagsCache[hor]; tags != nil { return tags, nil } now := time.Now() - gitHubTagsKey := []byte(owner + "/" + repo) + gitHubTagsKey := hor.Key() if c.GitHub.RefreshPeriod != 0 { var gitHubTagsStateValue gitHubTagsState switch ok, err := chezmoi.PersistentStateGet(c.persistentState, gitHubTagsStateBucket, gitHubTagsKey, &gitHubTagsStateValue); { @@ -288,12 +292,12 @@ func (c *Config) getGitHubTags(ownerRepo string) ([]*github.RepositoryTag, error ctx, cancel := context.WithCancel(context.Background()) defer cancel() - gitHubClient, err := c.getGitHubClient(ctx) + gitHubClient, err := c.getGitHubClient(ctx, hor.Host) if err != nil { return nil, err } - tags, _, err := gitHubClient.Repositories.ListTags(ctx, owner, repo, nil) + tags, _, err := gitHubClient.Repositories.ListTags(ctx, hor.Owner, hor.Repo, nil) if err != nil { return nil, err } @@ -306,35 +310,59 @@ func (c *Config) getGitHubTags(ownerRepo string) ([]*github.RepositoryTag, error } if c.gitHub.tagsCache == nil { - c.gitHub.tagsCache = make(map[string]map[string][]*github.RepositoryTag) + c.gitHub.tagsCache = make(map[gitHubHostOwnerRepo][]*github.RepositoryTag) } - if c.gitHub.tagsCache[owner] == nil { - c.gitHub.tagsCache[owner] = make(map[string][]*github.RepositoryTag) - } - c.gitHub.tagsCache[owner][repo] = tags + c.gitHub.tagsCache[hor] = tags return tags, nil } -func (c *Config) getGitHubClient(ctx context.Context) (*github.Client, error) { - if c.gitHub.client != nil || c.gitHub.clientErr != nil { - return c.gitHub.client, c.gitHub.clientErr +func (c *Config) getGitHubClient(ctx context.Context, host string) (*github.Client, error) { + if gitHubClientResult, ok := c.gitHub.clientsByHost[host]; ok { + return gitHubClientResult.client, gitHubClientResult.err } httpClient, err := c.getHTTPClient() if err != nil { - c.gitHub.clientErr = err + c.gitHub.clientsByHost[host] = gitHubClientResult{ + err: err, + } + return nil, err + } + + gitHubClient, err := chezmoi.NewGitHubClient(ctx, httpClient, host) + if err != nil { return nil, err } + c.gitHub.clientsByHost[host] = gitHubClientResult{ + client: gitHubClient, + } + + return gitHubClient, nil +} - c.gitHub.client = chezmoi.NewGitHubClient(ctx, httpClient) - return c.gitHub.client, nil +func gitHubSplitHostOwnerRepo(hostOwnerRepo string) (gitHubHostOwnerRepo, error) { + switch components := strings.Split(hostOwnerRepo, "/"); len(components) { + case 2: + return gitHubHostOwnerRepo{ + Host: "github.com", + Owner: components[0], + Repo: components[1], + }, nil + case 3: + return gitHubHostOwnerRepo{ + Host: components[0], + Owner: components[1], + Repo: components[2], + }, nil + default: + return gitHubHostOwnerRepo{}, fmt.Errorf("%s: not a [host/]owner/repo", hostOwnerRepo) + } } -func gitHubSplitOwnerRepo(ownerRepo string) (string, string, error) { - owner, repo, ok := strings.Cut(ownerRepo, "/") - if !ok { - return "", "", fmt.Errorf("%s: not an owner/repo", ownerRepo) +func (hor gitHubHostOwnerRepo) Key() []byte { + if hor.Host == "github.com" { + return []byte(hor.Owner + "/" + hor.Repo) } - return owner, repo, nil + return []byte(hor.Host + "/" + hor.Owner + "/" + hor.Repo) } diff --git a/internal/cmd/upgradecmd.go b/internal/cmd/upgradecmd.go index 5ac460abb914..da2f04e01e79 100644 --- a/internal/cmd/upgradecmd.go +++ b/internal/cmd/upgradecmd.go @@ -79,10 +79,13 @@ func (c *Config) runUpgradeCmd(cmd *cobra.Command, args []string) error { if err != nil { return err } - client := chezmoi.NewGitHubClient(ctx, httpClient) + gitHubClient, err := chezmoi.NewGitHubClient(ctx, httpClient, "github.com") + if err != nil { + return err + } // Get the latest release. - rr, _, err := client.Repositories.GetLatestRelease(ctx, "twpayne", "chezmoi") + rr, _, err := gitHubClient.Repositories.GetLatestRelease(ctx, "twpayne", "chezmoi") if err != nil { return err } diff --git a/internal/cmds/execute-template/main.go b/internal/cmds/execute-template/main.go index 80359bb7d834..2f90a39bfb1b 100644 --- a/internal/cmds/execute-template/main.go +++ b/internal/cmds/execute-template/main.go @@ -33,11 +33,15 @@ type gitHubClient struct { client *github.Client } -func newGitHubClient(ctx context.Context) *gitHubClient { +func newGitHubClient(ctx context.Context, host string) (*gitHubClient, error) { + client, err := chezmoi.NewGitHubClient(ctx, http.DefaultClient, host) + if err != nil { + return nil, err + } return &gitHubClient{ ctx: ctx, - client: chezmoi.NewGitHubClient(ctx, http.DefaultClient), - } + client: client, + }, nil } func (c *gitHubClient) gitHubListReleases(ownerRepo string) []*github.RepositoryRelease { @@ -99,7 +103,10 @@ func run() error { templateName := path.Base(flag.Arg(0)) buffer := &bytes.Buffer{} funcMap := sprig.TxtFuncMap() - gitHubClient := newGitHubClient(context.Background()) + gitHubClient, err := newGitHubClient(context.Background(), "github.com") + if err != nil { + return err + } funcMap["exists"] = func(name string) bool { switch _, err := os.Stat(name); { case err == nil: