package bulk import ( "context" "errors" "fmt" "strings" "time" "tangled.org/odd.computer/isomorph/internal/fetch" "tangled.org/odd.computer/isomorph/internal/repourl" ) const ( defaultGitHubAPI = "https://api.github.com" githubAccept = "application/vnd.github+json" // githubTTL is how long a repository lookup is cached. Renames are rare and GitHub keeps // redirecting the old name for as long as it is unclaimed, so a stale answer is harmless. githubTTL = 30 * 24 * time.Hour ) // githubAPI overrides the GitHub REST base URL; tests point it at an httptest server. Empty // means defaultGitHubAPI. var githubAPI string // LookupGitHub resolves a github.com key through the repositories API, which follows renames // and ownership transfers: the key derived from the response's full_name is the current one. // gone is true (with key returned unchanged) when the API answers 404, the repository no longer // exists. Rate limits, offline mode and network failures are returned as errors. Keys on other // hosts are returned as is. func LookupGitHub(ctx context.Context, client *fetch.Client, key string) (canonical string, gone bool, err error) { if !strings.HasPrefix(key, "github.com/") { return key, false, nil } c, err := repourl.Parse("https://" + key) if err != nil || c.Host != "github.com" || c.Kind != repourl.KindRepo { return key, false, nil } if client == nil { return key, false, errors.New("bulk: rename lookup needs a fetch.Client") } api := githubAPI if api == "" { api = defaultGitHubAPI } var resp struct { FullName string `json:"full_name"` } u := api + "/repos/" + c.Path err = client.GetJSON(ctx, u, &resp, fetch.WithHeader("Accept", githubAccept), fetch.WithTTL(githubTTL)) if fetch.IsNotFound(err) { return key, true, nil } if err != nil { return key, false, fmt.Errorf("bulk: %s: rename lookup: %w", key, err) } if resp.FullName == "" { return key, false, fmt.Errorf("bulk: %s: rename lookup: response has no full_name", key) } return "github.com/" + strings.ToLower(resp.FullName), false, nil }