From 246ad0eb2b0052e56198e7a2bd621642c7b3a2c4 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Wed, 1 May 2024 19:12:41 +0200 Subject: [PATCH] chore: snapshot --- internal/chezmoi/github.go | 46 +++++++-- internal/chezmoi/github_test.go | 27 +++++ internal/cmd/config.go | 5 + internal/cmd/doctorcmd.go | 2 +- internal/cmd/githubtemplatefuncs.go | 136 ++++++++++++++----------- internal/cmd/upgradecmd.go | 2 +- internal/cmds/execute-template/main.go | 6 +- 7 files changed, 154 insertions(+), 70 deletions(-) create mode 100644 internal/chezmoi/github_test.go diff --git a/internal/chezmoi/github.go b/internal/chezmoi/github.go index b724143a181..75f0a4ce62c 100644 --- a/internal/chezmoi/github.go +++ b/internal/chezmoi/github.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "os" + "strings" "github.com/google/go-github/v62/github" "golang.org/x/oauth2" @@ -11,13 +12,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 { + for _, key := range accessTokenEnvKeys(host) { if accessToken := os.Getenv(key); accessToken != "" { httpClient = oauth2.NewClient( context.WithValue(ctx, oauth2.HTTPClient, httpClient), @@ -29,3 +25,39 @@ func NewGitHubClient(ctx context.Context, httpClient *http.Client) *github.Clien } return github.NewClient(httpClient) } + +func accessTokenEnvKeys(host string) []string { + var keys []string + for _, chezmoiKey := range []string{"CHEZMOI", ""} { + hostKeys := []string{makeHostKey(host)} + if host == "github.com" { + hostKeys = append(hostKeys, "GITHUB") + } + for _, hostKey := range hostKeys { + for _, accessKey := range []string{"ACCESS", ""} { + key := strings.Join(nonEmpty([]string{chezmoiKey, hostKey, accessKey, "TOKEN"}), "_") + keys = append(keys, key) + } + } + } + return keys +} + +func makeHostKey(host string) string { + // FIXME split host on non-ASCII characters + // FIXME convert everything to uppercase + // FIXME join components with _ + return host +} + +func nonEmpty[S []T, T comparable](s S) S { + // FIXME use something from slices for this + result := make([]T, 0, len(s)) + var zero T + for _, e := range s { + if e != zero { + result = append(result, e) + } + } + return result +} diff --git a/internal/chezmoi/github_test.go b/internal/chezmoi/github_test.go new file mode 100644 index 00000000000..803bc572c64 --- /dev/null +++ b/internal/chezmoi/github_test.go @@ -0,0 +1,27 @@ +package chezmoi + +import ( + "testing" + + "github.com/alecthomas/assert/v2" +) + +func TestAccessTokenEnvKeys(t *testing.T) { + for _, tc := range []struct { + host string + expected []string + }{ + { + expected: []string{ + "CHEZMOI_GITHUB_ACCESS_TOKEN", + "CHEZMOI_GITHUB_TOKEN", + "GITHUB_ACCESS_TOKEN", + "GITHUB_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 88d92c73250..b68843fc1b4 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 cbce95a106b..e6bda51e514 100644 --- a/internal/cmd/doctorcmd.go +++ b/internal/cmd/doctorcmd.go @@ -659,7 +659,7 @@ func (c *latestVersionCheck) Run(system chezmoi.System, homeDirAbsPath chezmoi.A ctx := context.Background() - gitHubClient := chezmoi.NewGitHubClient(ctx, c.httpClient) + gitHubClient := chezmoi.NewGitHubClient(ctx, c.httpClient, "github.com") 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 83912b6ca34..09a655e7bf2 100644 --- a/internal/cmd/githubtemplatefuncs.go +++ b/internal/cmd/githubtemplatefuncs.go @@ -1,5 +1,7 @@ package cmd +// FIXME store per-host state in persistent state + import ( "context" "fmt" @@ -43,13 +45,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 +84,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 +120,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 +139,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 := []byte(hor.Owner + "/" + hor.Repo) if c.GitHub.RefreshPeriod != 0 { var gitHubLatestReleaseStateValue gitHubLatestReleaseState switch ok, err := chezmoi.PersistentStateGet(c.persistentState, gitHubLatestReleaseStateBucket, gitHubLatestReleaseKey, &gitHubLatestReleaseStateValue); { @@ -152,12 +164,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 +182,23 @@ func (c *Config) gitHubLatestRelease(ownerRepo string) (*github.RepositoryReleas } if c.gitHub.latestReleaseCache == nil { - c.gitHub.latestReleaseCache = make(map[string]map[string]*github.RepositoryRelease) - } - if c.gitHub.latestReleaseCache[owner] == nil { - c.gitHub.latestReleaseCache[owner] = make(map[string]*github.RepositoryRelease) + c.gitHub.latestReleaseCache = make(map[gitHubHostOwnerRepo]*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 +210,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 := []byte(hor.Owner + "/" + hor.Repo) if c.GitHub.RefreshPeriod != 0 { var gitHubReleasesStateValue gitHubReleasesState switch ok, err := chezmoi.PersistentStateGet(c.persistentState, gitHubReleasesStateBucket, gitHubReleasesKey, &gitHubReleasesStateValue); { @@ -226,12 +235,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 +253,15 @@ func (c *Config) gitHubReleasesTemplateFunc(ownerRepo string) []*github.Reposito } if c.gitHub.releasesCache == nil { - c.gitHub.releasesCache = make(map[string]map[string][]*github.RepositoryRelease) + c.gitHub.releasesCache = make(map[gitHubHostOwnerRepo][]*github.RepositoryRelease) } - if c.gitHub.releasesCache[owner] == nil { - c.gitHub.releasesCache[owner] = make(map[string][]*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 +269,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 := []byte(hor.Owner + "/" + hor.Repo) if c.GitHub.RefreshPeriod != 0 { var gitHubTagsStateValue gitHubTagsState switch ok, err := chezmoi.PersistentStateGet(c.persistentState, gitHubTagsStateBucket, gitHubTagsKey, &gitHubTagsStateValue); { @@ -288,12 +294,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 +312,49 @@ func (c *Config) getGitHubTags(ownerRepo string) ([]*github.RepositoryTag, error } if c.gitHub.tagsCache == nil { - c.gitHub.tagsCache = make(map[string]map[string][]*github.RepositoryTag) - } - if c.gitHub.tagsCache[owner] == nil { - c.gitHub.tagsCache[owner] = make(map[string][]*github.RepositoryTag) + c.gitHub.tagsCache = make(map[gitHubHostOwnerRepo][]*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 } - c.gitHub.client = chezmoi.NewGitHubClient(ctx, httpClient) - return c.gitHub.client, nil + client := chezmoi.NewGitHubClient(ctx, httpClient, host) + c.gitHub.clientsByHost[host] = gitHubClientResult{ + client: client, + } + + return client, nil } -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 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) } - return owner, repo, nil } diff --git a/internal/cmd/upgradecmd.go b/internal/cmd/upgradecmd.go index b37449b260c..5ce08c810ed 100644 --- a/internal/cmd/upgradecmd.go +++ b/internal/cmd/upgradecmd.go @@ -79,7 +79,7 @@ func (c *Config) runUpgradeCmd(cmd *cobra.Command, args []string) error { if err != nil { return err } - client := chezmoi.NewGitHubClient(ctx, httpClient) + client := chezmoi.NewGitHubClient(ctx, httpClient, "github.com") // Get the latest release. rr, _, err := client.Repositories.GetLatestRelease(ctx, "twpayne", "chezmoi") diff --git a/internal/cmds/execute-template/main.go b/internal/cmds/execute-template/main.go index e6eb0064f8f..041536eb2fd 100644 --- a/internal/cmds/execute-template/main.go +++ b/internal/cmds/execute-template/main.go @@ -33,10 +33,10 @@ type gitHubClient struct { client *github.Client } -func newGitHubClient(ctx context.Context) *gitHubClient { +func newGitHubClient(ctx context.Context, host string) *gitHubClient { return &gitHubClient{ ctx: ctx, - client: chezmoi.NewGitHubClient(ctx, http.DefaultClient), + client: chezmoi.NewGitHubClient(ctx, http.DefaultClient, host), } } @@ -99,7 +99,7 @@ func run() error { templateName := path.Base(flag.Arg(0)) buffer := &bytes.Buffer{} funcMap := sprig.TxtFuncMap() - gitHubClient := newGitHubClient(context.Background()) + gitHubClient := newGitHubClient(context.Background(), "github.com") funcMap["exists"] = func(name string) bool { switch _, err := os.Stat(name); { case err == nil: