diff --git a/client.go b/client.go index dd86a39..49f276e 100644 --- a/client.go +++ b/client.go @@ -1,11 +1,11 @@ package scanner import ( - "bytes" "context" - "encoding/json" "fmt" "net/http" + + "github.com/google/go-github/v72/github" ) // FileEntry represents a file or directory in a repo. @@ -42,182 +42,105 @@ type GitHubClient interface { } type realGitHubClient struct { - token string - httpClient *http.Client + client *github.Client } // NewGitHubClient creates a GitHubClient that calls the GitHub REST API. func NewGitHubClient(token string) GitHubClient { return &realGitHubClient{ - token: token, - httpClient: &http.Client{}, + client: github.NewClient(nil).WithAuthToken(token), } } -func (c *realGitHubClient) doRequest(ctx context.Context, url string, target interface{}) error { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return fmt.Errorf("create request for %s: %w", url, err) - } - req.Header.Set("Authorization", "Bearer "+c.token) - req.Header.Set("Accept", "application/vnd.github+json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return fmt.Errorf("request %s: %w", url, err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("request %s: status %d", url, resp.StatusCode) - } - - if err := json.NewDecoder(resp.Body).Decode(target); err != nil { - return fmt.Errorf("decode response from %s: %w", url, err) - } - return nil -} - -type ghRepo struct { - Name string `json:"name"` - Description string `json:"description"` - DefaultBranch string `json:"default_branch"` - Archived bool `json:"archived"` -} - func (c *realGitHubClient) ListRepos(ctx context.Context, org string) ([]Repo, error) { var allRepos []Repo - page := 1 + opts := &github.RepositoryListByOrgOptions{ + ListOptions: github.ListOptions{PerPage: 100}, + } for { - url := fmt.Sprintf("https://api.github.com/orgs/%s/repos?per_page=100&page=%d", org, page) - var ghRepos []ghRepo - if err := c.doRequest(ctx, url, &ghRepos); err != nil { + ghRepos, resp, err := c.client.Repositories.ListByOrg(ctx, org, opts) + if err != nil { return nil, fmt.Errorf("list repos for org %s: %w", org, err) } - if len(ghRepos) == 0 { - break - } - for _, r := range ghRepos { allRepos = append(allRepos, Repo{ - Name: r.Name, - Description: r.Description, - DefaultBranch: r.DefaultBranch, - Archived: r.Archived, + Name: r.GetName(), + Description: r.GetDescription(), + DefaultBranch: r.GetDefaultBranch(), + Archived: r.GetArchived(), }) } - if len(ghRepos) < 100 { + if resp.NextPage == 0 { break } - page++ + opts.Page = resp.NextPage } return allRepos, nil } func (c *realGitHubClient) GetTree(ctx context.Context, owner, repo, branch string) ([]FileEntry, error) { - url := fmt.Sprintf("https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1", owner, repo, branch) - - var result struct { - Tree []struct { - Path string `json:"path"` - Type string `json:"type"` - Size int `json:"size"` - } `json:"tree"` - } - if err := c.doRequest(ctx, url, &result); err != nil { + tree, _, err := c.client.Git.GetTree(ctx, owner, repo, branch, true) + if err != nil { return nil, fmt.Errorf("get tree for %s/%s: %w", owner, repo, err) } - files := make([]FileEntry, len(result.Tree)) - for i, e := range result.Tree { - files[i] = FileEntry{Path: e.Path, Type: e.Type, Size: e.Size} + files := make([]FileEntry, len(tree.Entries)) + for i, e := range tree.Entries { + files[i] = FileEntry{ + Path: e.GetPath(), + Type: e.GetType(), + Size: e.GetSize(), + } } return files, nil } func (c *realGitHubClient) GetBranchProtection(ctx context.Context, owner, repo, branch string) (*BranchProtection, error) { - url := fmt.Sprintf("https://api.github.com/repos/%s/%s/branches/%s/protection", owner, repo, branch) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, fmt.Errorf("create request for %s: %w", url, err) - } - req.Header.Set("Authorization", "Bearer "+c.token) - req.Header.Set("Accept", "application/vnd.github+json") - - resp, err := c.httpClient.Do(req) + prot, resp, err := c.client.Repositories.GetBranchProtection(ctx, owner, repo, branch) if err != nil { - return nil, fmt.Errorf("request %s: %w", url, err) - } - defer resp.Body.Close() - - if resp.StatusCode == http.StatusNotFound { - return nil, nil - } - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("get branch protection for %s/%s: status %d", owner, repo, resp.StatusCode) - } - - var result struct { - RequiredPullRequestReviews *struct { - RequiredApprovingReviewCount int `json:"required_approving_review_count"` - } `json:"required_pull_request_reviews"` - RequiredStatusChecks *struct { - Contexts []string `json:"contexts"` - } `json:"required_status_checks"` - } - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, fmt.Errorf("decode branch protection for %s/%s: %w", owner, repo, err) + if resp != nil && (resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusForbidden) { + return nil, nil + } + return nil, fmt.Errorf("get branch protection for %s/%s: %w", owner, repo, err) } bp := &BranchProtection{} - if result.RequiredPullRequestReviews != nil { - bp.RequiredReviewers = result.RequiredPullRequestReviews.RequiredApprovingReviewCount + if prot.RequiredPullRequestReviews != nil { + bp.RequiredReviewers = prot.RequiredPullRequestReviews.RequiredApprovingReviewCount } - if result.RequiredStatusChecks != nil { - bp.RequiredStatusChecks = result.RequiredStatusChecks.Contexts + if prot.RequiredStatusChecks != nil && prot.RequiredStatusChecks.Contexts != nil { + bp.RequiredStatusChecks = *prot.RequiredStatusChecks.Contexts } return bp, nil } func (c *realGitHubClient) GetRulesets(ctx context.Context, owner, repo, branch string) (*BranchProtection, error) { - url := fmt.Sprintf("https://api.github.com/repos/%s/%s/rules/branches/%s", owner, repo, branch) - - var rules []struct { - Type string `json:"type"` - Parameters *struct { - RequiredApprovingReviewCount int `json:"required_approving_review_count"` - RequiredStatusChecks []struct { - Context string `json:"context"` - } `json:"required_status_checks"` - } `json:"parameters"` - } - if err := c.doRequest(ctx, url, &rules); err != nil { + rules, resp, err := c.client.Repositories.GetRulesForBranch(ctx, owner, repo, branch, nil) + if err != nil { + if resp != nil && (resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusForbidden) { + return nil, nil + } return nil, fmt.Errorf("get branch rules for %s/%s: %w", owner, repo, err) } var bp BranchProtection found := false - for _, rule := range rules { - if rule.Parameters == nil { - continue + for _, pr := range rules.PullRequest { + found = true + if pr.Parameters.RequiredApprovingReviewCount > bp.RequiredReviewers { + bp.RequiredReviewers = pr.Parameters.RequiredApprovingReviewCount } - switch rule.Type { - case "pull_request": - found = true - if rule.Parameters.RequiredApprovingReviewCount > bp.RequiredReviewers { - bp.RequiredReviewers = rule.Parameters.RequiredApprovingReviewCount - } - case "required_status_checks": - found = true - for _, sc := range rule.Parameters.RequiredStatusChecks { - bp.RequiredStatusChecks = append(bp.RequiredStatusChecks, sc.Context) - } + } + + for _, sc := range rules.RequiredStatusChecks { + found = true + for _, check := range sc.Parameters.RequiredStatusChecks { + bp.RequiredStatusChecks = append(bp.RequiredStatusChecks, check.Context) } } @@ -228,35 +151,14 @@ func (c *realGitHubClient) GetRulesets(ctx context.Context, owner, repo, branch } func (c *realGitHubClient) CreateIssue(ctx context.Context, owner, repo, title, body string) error { - url := fmt.Sprintf("https://api.github.com/repos/%s/%s/issues", owner, repo) - - payload := struct { - Title string `json:"title"` - Body string `json:"body"` - }{Title: title, Body: body} - - jsonBody, err := json.Marshal(payload) - if err != nil { - return fmt.Errorf("marshal issue payload: %w", err) + req := &github.IssueRequest{ + Title: github.Ptr(title), + Body: github.Ptr(body), } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(jsonBody)) + _, _, err := c.client.Issues.Create(ctx, owner, repo, req) if err != nil { - return fmt.Errorf("create request for %s: %w", url, err) + return fmt.Errorf("create issue in %s/%s: %w", owner, repo, err) } - req.Header.Set("Authorization", "Bearer "+c.token) - req.Header.Set("Accept", "application/vnd.github+json") - req.Header.Set("Content-Type", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return fmt.Errorf("request %s: %w", url, err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusCreated { - return fmt.Errorf("create issue in %s/%s: status %d", owner, repo, resp.StatusCode) - } - return nil } diff --git a/go.mod b/go.mod index 8c120de..67ad008 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,7 @@ module github.com/CodatusHQ/scanner -go 1.22.2 +go 1.26.1 + +require github.com/google/go-github/v72 v72.0.0 + +require github.com/google/go-querystring v1.1.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..afde61e --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v72 v72.0.0 h1:FcIO37BLoVPBO9igQQ6tStsv2asG4IPcYFi655PPvBM= +github.com/google/go-github/v72 v72.0.0/go.mod h1:WWtw8GMRiL62mvIquf1kO3onRHeWWKmK01qdCY8c5fg= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=