772 lines
21 KiB
Go
772 lines
21 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/go-github/v70/github"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath"
|
|
)
|
|
|
|
type githubClient struct {
|
|
gh *github.Client
|
|
}
|
|
|
|
var _ Client = (*githubClient)(nil)
|
|
|
|
func NewClient(client *github.Client) *githubClient {
|
|
return &githubClient{client}
|
|
}
|
|
|
|
func (r *githubClient) IsAuthenticated(ctx context.Context) error {
|
|
if _, _, err := r.gh.Users.Get(ctx, ""); err != nil {
|
|
var ghErr *github.ErrorResponse
|
|
if errors.As(err, &ghErr) {
|
|
switch ghErr.Response.StatusCode {
|
|
case http.StatusUnauthorized:
|
|
return apierrors.NewUnauthorized("token is invalid or expired")
|
|
case http.StatusForbidden:
|
|
return &apierrors.StatusError{
|
|
ErrStatus: metav1.Status{
|
|
Status: metav1.StatusFailure,
|
|
Code: http.StatusUnauthorized,
|
|
Reason: metav1.StatusReasonUnauthorized,
|
|
Message: "token is revoked or has insufficient permissions",
|
|
},
|
|
}
|
|
case http.StatusServiceUnavailable:
|
|
return ErrServiceUnavailable
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *githubClient) RepoExists(ctx context.Context, owner, repository string) (bool, error) {
|
|
_, resp, err := r.gh.Repositories.Get(ctx, owner, repository)
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return false, nil
|
|
}
|
|
|
|
return false, err
|
|
}
|
|
|
|
const (
|
|
maxDirectoryItems = 1000 // Maximum number of items allowed in a directory
|
|
maxTreeItems = 10000 // Maximum number of items allowed in a tree
|
|
maxCommits = 1000 // Maximum number of commits to fetch
|
|
maxCompareFiles = 1000 // Maximum number of files to compare between commits
|
|
maxWebhooks = 100 // Maximum number of webhooks allowed per repository
|
|
maxPRFiles = 1000 // Maximum number of files allowed in a pull request
|
|
maxPullRequestsFileComments = 1000 // Maximum number of comments allowed in a pull request
|
|
maxFileSize = 10 * 1024 * 1024 // 10MB in bytes
|
|
)
|
|
|
|
func (r *githubClient) GetContents(ctx context.Context, owner, repository, path, ref string) (fileContents RepositoryContent, dirContents []RepositoryContent, err error) {
|
|
// First try to get repository contents
|
|
opts := &github.RepositoryContentGetOptions{
|
|
Ref: ref,
|
|
}
|
|
|
|
fc, dc, _, err := r.gh.Repositories.GetContents(ctx, owner, repository, path, opts)
|
|
if err != nil {
|
|
var ghErr *github.ErrorResponse
|
|
if !errors.As(err, &ghErr) {
|
|
return nil, nil, err
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
|
return nil, nil, ErrServiceUnavailable
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusNotFound {
|
|
return nil, nil, ErrResourceNotFound
|
|
}
|
|
return nil, nil, err
|
|
}
|
|
|
|
if fc != nil {
|
|
// Check file size before returning content
|
|
if fc.GetSize() > maxFileSize {
|
|
return nil, nil, ErrFileTooLarge
|
|
}
|
|
return realRepositoryContent{fc}, nil, nil
|
|
}
|
|
|
|
// For directories, check size limits
|
|
if len(dc) > maxDirectoryItems {
|
|
return nil, nil, fmt.Errorf("directory contains too many items (more than %d)", maxDirectoryItems)
|
|
}
|
|
|
|
// Convert directory contents
|
|
allContents := make([]RepositoryContent, 0, len(dc))
|
|
for _, original := range dc {
|
|
allContents = append(allContents, realRepositoryContent{original})
|
|
}
|
|
|
|
return nil, allContents, nil
|
|
}
|
|
|
|
func (r *githubClient) GetTree(ctx context.Context, owner, repository, basePath, ref string, recursive bool) ([]RepositoryContent, bool, error) {
|
|
var tree *github.Tree
|
|
var err error
|
|
|
|
subPaths := safepath.Split(basePath)
|
|
currentRef := ref
|
|
|
|
for {
|
|
// If subPaths is empty, we can read recursively, as we're reading the tree from the "base" of the repository. Otherwise, always read only the direct children.
|
|
recursive := recursive && len(subPaths) == 0
|
|
|
|
tree, _, err = r.gh.Git.GetTree(ctx, owner, repository, currentRef, recursive)
|
|
if err != nil {
|
|
var ghErr *github.ErrorResponse
|
|
if !errors.As(err, &ghErr) {
|
|
return nil, false, err
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
|
return nil, false, ErrServiceUnavailable
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusNotFound {
|
|
if currentRef != ref {
|
|
// We're operating with a subpath which doesn't exist yet.
|
|
// Pretend as if there is simply no files.
|
|
return nil, false, nil
|
|
}
|
|
// currentRef == ref
|
|
// This indicates the repository or commitish reference doesn't exist. This should always return an error.
|
|
return nil, false, ErrResourceNotFound
|
|
}
|
|
return nil, false, err
|
|
}
|
|
|
|
// Check if we've exceeded the maximum allowed items
|
|
if len(tree.Entries) > maxTreeItems {
|
|
return nil, false, fmt.Errorf("tree contains too many items (more than %d)", maxTreeItems)
|
|
}
|
|
|
|
// Prep for next iteration.
|
|
if len(subPaths) == 0 {
|
|
// We're done: we've discovered the tree we want.
|
|
break
|
|
}
|
|
|
|
// the ref must be equal the SHA of the entry corresponding to subPaths[0]
|
|
currentRef = ""
|
|
for _, e := range tree.Entries {
|
|
if e.GetPath() == subPaths[0] {
|
|
currentRef = e.GetSHA()
|
|
break
|
|
}
|
|
}
|
|
subPaths = subPaths[1:]
|
|
if currentRef == "" {
|
|
// We couldn't find the folder in the tree...
|
|
return nil, false, nil
|
|
}
|
|
}
|
|
|
|
// If the tree is truncated and we're in recursive mode, return an error
|
|
if tree.GetTruncated() && recursive {
|
|
return nil, true, fmt.Errorf("tree is too large to fetch recursively (more than %d items)", maxTreeItems)
|
|
}
|
|
|
|
entries := make([]RepositoryContent, 0, len(tree.Entries))
|
|
for _, te := range tree.Entries {
|
|
rrc := &realRepositoryContent{
|
|
real: &github.RepositoryContent{
|
|
Path: te.Path,
|
|
Size: te.Size,
|
|
SHA: te.SHA,
|
|
},
|
|
}
|
|
if te.GetType() == "tree" {
|
|
rrc.real.Type = github.Ptr("dir")
|
|
} else {
|
|
rrc.real.Type = te.Type
|
|
}
|
|
entries = append(entries, rrc)
|
|
}
|
|
return entries, tree.GetTruncated(), nil
|
|
}
|
|
|
|
func (r *githubClient) CreateFile(ctx context.Context, owner, repository, path, branch, message string, content []byte) error {
|
|
if message == "" {
|
|
message = fmt.Sprintf("Create %s", path)
|
|
}
|
|
|
|
_, _, err := r.gh.Repositories.CreateFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{
|
|
Branch: &branch,
|
|
Message: &message,
|
|
Content: content,
|
|
})
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
var ghErr *github.ErrorResponse
|
|
if !errors.As(err, &ghErr) {
|
|
return err
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusUnprocessableEntity {
|
|
return ErrResourceAlreadyExists
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (r *githubClient) UpdateFile(ctx context.Context, owner, repository, path, branch, message, hash string, content []byte) error {
|
|
if message == "" {
|
|
message = fmt.Sprintf("Update %s", path)
|
|
}
|
|
|
|
_, _, err := r.gh.Repositories.UpdateFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{
|
|
Branch: &branch,
|
|
Message: &message,
|
|
Content: content,
|
|
SHA: &hash,
|
|
})
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
var ghErr *github.ErrorResponse
|
|
if !errors.As(err, &ghErr) {
|
|
return err
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusNotFound {
|
|
return ErrResourceNotFound
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusConflict {
|
|
return ErrMismatchedHash
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
|
return ErrServiceUnavailable
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (r *githubClient) DeleteFile(ctx context.Context, owner, repository, path, branch, message, hash string) error {
|
|
if message == "" {
|
|
message = fmt.Sprintf("Delete %s", path)
|
|
}
|
|
|
|
_, _, err := r.gh.Repositories.DeleteFile(ctx, owner, repository, path, &github.RepositoryContentFileOptions{
|
|
Branch: &branch,
|
|
Message: &message,
|
|
SHA: &hash,
|
|
})
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
var ghErr *github.ErrorResponse
|
|
if !errors.As(err, &ghErr) {
|
|
return err
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusNotFound {
|
|
return ErrResourceNotFound
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusConflict {
|
|
return ErrMismatchedHash
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
|
return ErrServiceUnavailable
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Commits returns a list of commits for a given repository and branch.
|
|
func (r *githubClient) Commits(ctx context.Context, owner, repository, path, branch string) ([]Commit, error) {
|
|
listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.RepositoryCommit, *github.Response, error) {
|
|
return r.gh.Repositories.ListCommits(ctx, owner, repository, &github.CommitsListOptions{
|
|
Path: path,
|
|
SHA: branch,
|
|
ListOptions: *opts,
|
|
})
|
|
}
|
|
|
|
commits, err := paginatedList(
|
|
ctx,
|
|
listFn,
|
|
defaultListOptions(maxCommits),
|
|
)
|
|
if errors.Is(err, ErrTooManyItems) {
|
|
return nil, fmt.Errorf("too many commits to fetch (more than %d)", maxCommits)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ret := make([]Commit, 0, len(commits))
|
|
for _, c := range commits {
|
|
var createdAt time.Time
|
|
var author *CommitAuthor
|
|
if c.GetCommit().GetAuthor() != nil {
|
|
author = &CommitAuthor{
|
|
Name: c.GetCommit().GetAuthor().GetName(),
|
|
Username: c.GetAuthor().GetLogin(),
|
|
AvatarURL: c.GetAuthor().GetAvatarURL(),
|
|
}
|
|
|
|
createdAt = c.GetCommit().GetAuthor().GetDate().Time
|
|
}
|
|
|
|
var committer *CommitAuthor
|
|
if c.GetCommitter() != nil {
|
|
committer = &CommitAuthor{
|
|
Name: c.GetCommit().GetCommitter().GetName(),
|
|
Username: c.GetCommitter().GetLogin(),
|
|
AvatarURL: c.GetCommitter().GetAvatarURL(),
|
|
}
|
|
}
|
|
|
|
ret = append(ret, Commit{
|
|
Ref: c.GetSHA(),
|
|
Message: c.GetCommit().GetMessage(),
|
|
Author: author,
|
|
Committer: committer,
|
|
CreatedAt: createdAt,
|
|
})
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (r *githubClient) CompareCommits(ctx context.Context, owner, repository, base, head string) ([]CommitFile, error) {
|
|
listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.CommitFile, *github.Response, error) {
|
|
compare, resp, err := r.gh.Repositories.CompareCommits(ctx, owner, repository, base, head, opts)
|
|
if err != nil {
|
|
return nil, resp, err
|
|
}
|
|
return compare.Files, resp, nil
|
|
}
|
|
|
|
files, err := paginatedList(
|
|
ctx,
|
|
listFn,
|
|
defaultListOptions(maxCompareFiles),
|
|
)
|
|
if errors.Is(err, ErrTooManyItems) {
|
|
return nil, fmt.Errorf("too many files changed between commits (more than %d)", maxCompareFiles)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert to the interface type
|
|
ret := make([]CommitFile, 0, len(files))
|
|
for _, f := range files {
|
|
ret = append(ret, f)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (r *githubClient) GetBranch(ctx context.Context, owner, repository, branchName string) (Branch, error) {
|
|
branch, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0)
|
|
if err != nil {
|
|
var ghErr *github.ErrorResponse
|
|
if !errors.As(err, &ghErr) {
|
|
return Branch{}, err
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
|
return Branch{}, ErrServiceUnavailable
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusNotFound {
|
|
return Branch{}, ErrResourceNotFound
|
|
}
|
|
return Branch{}, err
|
|
}
|
|
|
|
return Branch{
|
|
Name: branch.GetName(),
|
|
Sha: branch.GetCommit().GetSHA(),
|
|
}, nil
|
|
}
|
|
|
|
func (r *githubClient) CreateBranch(ctx context.Context, owner, repository, sourceBranch, branchName string) error {
|
|
// Fail if the branch already exists
|
|
if _, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0); err == nil {
|
|
return ErrResourceAlreadyExists
|
|
}
|
|
|
|
// Branch out based on the repository branch
|
|
baseRef, _, err := r.gh.Repositories.GetBranch(ctx, owner, repository, sourceBranch, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("get base branch: %w", err)
|
|
}
|
|
|
|
if _, _, err := r.gh.Git.CreateRef(ctx, owner, repository, &github.Reference{
|
|
Ref: github.Ptr(fmt.Sprintf("refs/heads/%s", branchName)),
|
|
Object: &github.GitObject{
|
|
SHA: baseRef.Commit.SHA,
|
|
},
|
|
}); err != nil {
|
|
return fmt.Errorf("create branch ref: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *githubClient) BranchExists(ctx context.Context, owner, repository, branchName string) (bool, error) {
|
|
_, resp, err := r.gh.Repositories.GetBranch(ctx, owner, repository, branchName, 0)
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return false, nil
|
|
}
|
|
|
|
return false, err
|
|
}
|
|
|
|
func (r *githubClient) ListWebhooks(ctx context.Context, owner, repository string) ([]WebhookConfig, error) {
|
|
listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.Hook, *github.Response, error) {
|
|
return r.gh.Repositories.ListHooks(ctx, owner, repository, opts)
|
|
}
|
|
|
|
hooks, err := paginatedList(
|
|
ctx,
|
|
listFn,
|
|
defaultListOptions(maxWebhooks),
|
|
)
|
|
if errors.Is(err, ErrTooManyItems) {
|
|
return nil, fmt.Errorf("too many webhooks configured (more than %d)", maxWebhooks)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Pre-allocate the result slice
|
|
ret := make([]WebhookConfig, 0, len(hooks))
|
|
for _, h := range hooks {
|
|
contentType := h.GetConfig().GetContentType()
|
|
if contentType == "" {
|
|
contentType = "form"
|
|
}
|
|
|
|
ret = append(ret, WebhookConfig{
|
|
ID: h.GetID(),
|
|
Events: h.Events,
|
|
Active: h.GetActive(),
|
|
URL: h.GetConfig().GetURL(),
|
|
ContentType: contentType,
|
|
// Intentionally not setting Secret.
|
|
})
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (r *githubClient) CreateWebhook(ctx context.Context, owner, repository string, cfg WebhookConfig) (WebhookConfig, error) {
|
|
if cfg.ContentType == "" {
|
|
cfg.ContentType = "form"
|
|
}
|
|
|
|
hook := &github.Hook{
|
|
URL: &cfg.URL,
|
|
Events: cfg.Events,
|
|
Active: &cfg.Active,
|
|
Config: &github.HookConfig{
|
|
ContentType: &cfg.ContentType,
|
|
Secret: &cfg.Secret,
|
|
URL: &cfg.URL,
|
|
},
|
|
}
|
|
|
|
createdHook, _, err := r.gh.Repositories.CreateHook(ctx, owner, repository, hook)
|
|
var ghErr *github.ErrorResponse
|
|
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
|
return WebhookConfig{}, ErrServiceUnavailable
|
|
}
|
|
if err != nil {
|
|
return WebhookConfig{}, err
|
|
}
|
|
|
|
return WebhookConfig{
|
|
ID: createdHook.GetID(),
|
|
// events is not returned by GitHub.
|
|
Events: cfg.Events,
|
|
Active: createdHook.GetActive(),
|
|
URL: createdHook.GetConfig().GetURL(),
|
|
ContentType: createdHook.GetConfig().GetContentType(),
|
|
// Secret is not returned by GitHub.
|
|
Secret: cfg.Secret,
|
|
}, nil
|
|
}
|
|
|
|
func (r *githubClient) GetWebhook(ctx context.Context, owner, repository string, webhookID int64) (WebhookConfig, error) {
|
|
hook, _, err := r.gh.Repositories.GetHook(ctx, owner, repository, webhookID)
|
|
if err != nil {
|
|
var ghErr *github.ErrorResponse
|
|
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
|
return WebhookConfig{}, ErrServiceUnavailable
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusNotFound {
|
|
return WebhookConfig{}, ErrResourceNotFound
|
|
}
|
|
return WebhookConfig{}, err
|
|
}
|
|
|
|
contentType := hook.GetConfig().GetContentType()
|
|
if contentType == "" {
|
|
contentType = "json"
|
|
}
|
|
|
|
return WebhookConfig{
|
|
ID: hook.GetID(),
|
|
Events: hook.Events,
|
|
Active: hook.GetActive(),
|
|
URL: hook.GetConfig().GetURL(),
|
|
ContentType: contentType,
|
|
// Intentionally not setting Secret.
|
|
}, nil
|
|
}
|
|
|
|
func (r *githubClient) DeleteWebhook(ctx context.Context, owner, repository string, webhookID int64) error {
|
|
_, err := r.gh.Repositories.DeleteHook(ctx, owner, repository, webhookID)
|
|
var ghErr *github.ErrorResponse
|
|
if !errors.As(err, &ghErr) {
|
|
return err
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
|
return ErrServiceUnavailable
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusNotFound {
|
|
return ErrResourceNotFound
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (r *githubClient) EditWebhook(ctx context.Context, owner, repository string, cfg WebhookConfig) error {
|
|
if cfg.ContentType == "" {
|
|
cfg.ContentType = "form"
|
|
}
|
|
|
|
hook := &github.Hook{
|
|
URL: &cfg.URL,
|
|
Events: cfg.Events,
|
|
Active: &cfg.Active,
|
|
Config: &github.HookConfig{
|
|
ContentType: &cfg.ContentType,
|
|
Secret: &cfg.Secret,
|
|
URL: &cfg.URL,
|
|
},
|
|
}
|
|
_, _, err := r.gh.Repositories.EditHook(ctx, owner, repository, cfg.ID, hook)
|
|
var ghErr *github.ErrorResponse
|
|
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
|
return ErrServiceUnavailable
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (r *githubClient) ListPullRequestFiles(ctx context.Context, owner, repository string, number int) ([]CommitFile, error) {
|
|
listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.CommitFile, *github.Response, error) {
|
|
return r.gh.PullRequests.ListFiles(ctx, owner, repository, number, opts)
|
|
}
|
|
|
|
files, err := paginatedList(
|
|
ctx,
|
|
listFn,
|
|
defaultListOptions(maxPRFiles),
|
|
)
|
|
if errors.Is(err, ErrTooManyItems) {
|
|
return nil, fmt.Errorf("pull request contains too many files (more than %d)", maxPRFiles)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert to the interface type
|
|
ret := make([]CommitFile, 0, len(files))
|
|
for _, f := range files {
|
|
ret = append(ret, f)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (r *githubClient) CreatePullRequestComment(ctx context.Context, owner, repository string, number int, body string) error {
|
|
comment := &github.IssueComment{
|
|
Body: &body,
|
|
}
|
|
|
|
if _, _, err := r.gh.Issues.CreateComment(ctx, owner, repository, number, comment); err != nil {
|
|
var ghErr *github.ErrorResponse
|
|
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
|
return ErrServiceUnavailable
|
|
}
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *githubClient) CreatePullRequestFileComment(ctx context.Context, owner, repository string, number int, comment FileComment) error {
|
|
commentRequest := &github.PullRequestComment{
|
|
Body: &comment.Content,
|
|
CommitID: &comment.Ref,
|
|
Path: &comment.Path,
|
|
Position: &comment.Position,
|
|
}
|
|
|
|
if _, _, err := r.gh.PullRequests.CreateComment(ctx, owner, repository, number, commentRequest); err != nil {
|
|
var ghErr *github.ErrorResponse
|
|
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
|
return ErrServiceUnavailable
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *githubClient) ClearAllPullRequestFileComments(ctx context.Context, owner, repository string, number int) error {
|
|
listFn := func(ctx context.Context, opts *github.ListOptions) ([]*github.PullRequestComment, *github.Response, error) {
|
|
return r.gh.PullRequests.ListComments(ctx, owner, repository, number, &github.PullRequestListCommentsOptions{
|
|
ListOptions: *opts,
|
|
})
|
|
}
|
|
|
|
comments, err := paginatedList(ctx, listFn, defaultListOptions(maxPullRequestsFileComments))
|
|
if errors.Is(err, ErrTooManyItems) {
|
|
return fmt.Errorf("too many comments to process (more than %d)", maxPullRequestsFileComments)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
userLogin, _, err := r.gh.Users.Get(ctx, "")
|
|
if err != nil {
|
|
return fmt.Errorf("get user: %w", err)
|
|
}
|
|
|
|
for _, c := range comments {
|
|
// skip if comments were not created by us
|
|
if c.User.GetLogin() != userLogin.GetLogin() {
|
|
continue
|
|
}
|
|
|
|
if _, err := r.gh.PullRequests.DeleteComment(ctx, owner, repository, c.GetID()); err != nil {
|
|
return fmt.Errorf("delete comment: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type realRepositoryContent struct {
|
|
real *github.RepositoryContent
|
|
}
|
|
|
|
var _ RepositoryContent = realRepositoryContent{}
|
|
|
|
func (c realRepositoryContent) IsDirectory() bool {
|
|
return c.real.GetType() == "dir"
|
|
}
|
|
|
|
func (c realRepositoryContent) GetFileContent() (string, error) {
|
|
return c.real.GetContent()
|
|
}
|
|
|
|
func (c realRepositoryContent) IsSymlink() bool {
|
|
return c.real.Target != nil
|
|
}
|
|
|
|
func (c realRepositoryContent) GetPath() string {
|
|
return c.real.GetPath()
|
|
}
|
|
|
|
func (c realRepositoryContent) GetSHA() string {
|
|
return c.real.GetSHA()
|
|
}
|
|
|
|
func (c realRepositoryContent) GetSize() int64 {
|
|
if c.real.Size != nil {
|
|
return int64(*c.real.Size)
|
|
}
|
|
if c.real.Content != nil {
|
|
if c, err := c.real.GetContent(); err == nil {
|
|
return int64(len(c))
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// listOptions represents pagination parameters for list operations
|
|
type listOptions struct {
|
|
github.ListOptions
|
|
MaxItems int
|
|
}
|
|
|
|
// defaultListOptions returns a ListOptions with sensible defaults
|
|
func defaultListOptions(maxItems int) listOptions {
|
|
return listOptions{
|
|
ListOptions: github.ListOptions{
|
|
Page: 1,
|
|
PerPage: 100,
|
|
},
|
|
MaxItems: maxItems,
|
|
}
|
|
}
|
|
|
|
// paginatedList is a generic function to handle GitHub API pagination
|
|
func paginatedList[T any](
|
|
ctx context.Context,
|
|
listFn func(context.Context, *github.ListOptions) ([]T, *github.Response, error),
|
|
opts listOptions,
|
|
) ([]T, error) {
|
|
var allItems []T
|
|
|
|
for {
|
|
items, resp, err := listFn(ctx, &opts.ListOptions)
|
|
if err != nil {
|
|
var ghErr *github.ErrorResponse
|
|
if !errors.As(err, &ghErr) {
|
|
return nil, err
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusServiceUnavailable {
|
|
return nil, ErrServiceUnavailable
|
|
}
|
|
if ghErr.Response.StatusCode == http.StatusNotFound {
|
|
return nil, ErrResourceNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Pre-allocate the slice if this is the first page
|
|
if allItems == nil {
|
|
allItems = make([]T, 0, len(items)*2) // Estimate double the first page size
|
|
}
|
|
|
|
allItems = append(allItems, items...)
|
|
|
|
// Check if we've exceeded the maximum allowed items
|
|
if len(allItems) > opts.MaxItems {
|
|
return nil, ErrTooManyItems
|
|
}
|
|
|
|
// If there are no more pages, break
|
|
if resp.NextPage == 0 {
|
|
break
|
|
}
|
|
|
|
// Set up next page
|
|
opts.Page = resp.NextPage
|
|
}
|
|
|
|
return allItems, nil
|
|
}
|