feat: github adapter
This commit is contained in:
312
internal/adapter/github/forge.go
Normal file
312
internal/adapter/github/forge.go
Normal file
@ -0,0 +1,312 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/core/port"
|
||||
"forge.cadoles.com/wpetit/clearcase/internal/core/service"
|
||||
"github.com/google/go-github/v69/github"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Forge struct {
|
||||
client *github.Client
|
||||
}
|
||||
|
||||
// CreateIssue implements port.Forge.
|
||||
func (f *Forge) CreateIssue(ctx context.Context, projectID string, title string, body string) (string, error) {
|
||||
repo, err := f.getRepository(ctx, projectID)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
issue, res, err := f.client.Issues.Create(ctx, *repo.Owner.Login, *repo.Name, &github.IssueRequest{
|
||||
Title: &title,
|
||||
Body: &body,
|
||||
})
|
||||
if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized {
|
||||
slog.ErrorContext(ctx, "could not create issue", slog.Any("error", errors.WithStack(err)))
|
||||
return "", errors.WithStack(service.ErrForgeNotAvailable)
|
||||
}
|
||||
|
||||
return *issue.HTMLURL, nil
|
||||
}
|
||||
|
||||
// GetAllProjects implements port.Forge.
|
||||
func (f *Forge) GetAllProjects(ctx context.Context) ([]*model.Project, error) {
|
||||
projects := make([]*model.Project, 0)
|
||||
|
||||
collectUserProjects := func() error {
|
||||
page := 1
|
||||
for {
|
||||
repos, res, err := f.client.Repositories.ListByAuthenticatedUser(ctx, &github.RepositoryListByAuthenticatedUserOptions{
|
||||
Type: "all",
|
||||
ListOptions: github.ListOptions{
|
||||
Page: page,
|
||||
PerPage: 100,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized {
|
||||
slog.ErrorContext(ctx, "could not retrieve user repositories", slog.Any("error", errors.WithStack(err)))
|
||||
return errors.WithStack(service.ErrForgeNotAvailable)
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, r := range repos {
|
||||
if r.ID == nil || r.FullName == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var projectDescription string
|
||||
if r.Description != nil {
|
||||
projectDescription = *r.Description
|
||||
}
|
||||
|
||||
projects = append(projects, &model.Project{
|
||||
ID: strconv.FormatInt(*r.ID, 10),
|
||||
Name: *r.FullName,
|
||||
Description: projectDescription,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if res.NextPage == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
page = res.NextPage
|
||||
}
|
||||
}
|
||||
|
||||
collectOrgProjects := func(owner string) error {
|
||||
page := 1
|
||||
for {
|
||||
repos, res, err := f.client.Repositories.ListByOrg(ctx, owner, &github.RepositoryListByOrgOptions{
|
||||
Type: "all",
|
||||
ListOptions: github.ListOptions{
|
||||
Page: page,
|
||||
PerPage: 100,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized {
|
||||
slog.ErrorContext(ctx, "could not retrieve org repositories", slog.String("org", owner), slog.Any("error", errors.WithStack(err)))
|
||||
return errors.WithStack(service.ErrForgeNotAvailable)
|
||||
}
|
||||
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, r := range repos {
|
||||
if r.ID == nil || r.FullName == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var projectDescription string
|
||||
if r.Description != nil {
|
||||
projectDescription = *r.Description
|
||||
}
|
||||
|
||||
projects = append(projects, &model.Project{
|
||||
ID: strconv.FormatInt(*r.ID, 10),
|
||||
Name: *r.FullName,
|
||||
Description: projectDescription,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if res.NextPage == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
page = res.NextPage
|
||||
}
|
||||
}
|
||||
|
||||
if err := collectUserProjects(); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
page := 1
|
||||
for {
|
||||
orgs, res, err := f.client.Organizations.ListOrgMemberships(ctx, &github.ListOrgMembershipsOptions{
|
||||
State: "active",
|
||||
ListOptions: github.ListOptions{
|
||||
Page: page,
|
||||
PerPage: 100,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized {
|
||||
slog.ErrorContext(ctx, "could not retrieve user organizations", slog.Any("error", errors.WithStack(err)))
|
||||
return nil, errors.WithStack(service.ErrForgeNotAvailable)
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
for _, o := range orgs {
|
||||
if err := collectOrgProjects(*o.Organization.Login); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
if res.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
page = res.NextPage
|
||||
}
|
||||
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
// GetFile implements port.Forge.
|
||||
func (f *Forge) GetFile(ctx context.Context, rawProjectID string, path string) ([]byte, error) {
|
||||
repo, err := f.getRepository(ctx, rawProjectID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
fileContent, _, res, err := f.client.Repositories.GetContents(ctx, *repo.Owner.Login, *repo.Name, path, nil)
|
||||
if err != nil {
|
||||
if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized {
|
||||
slog.ErrorContext(ctx, "could not retrieve file content", slog.Any("error", errors.WithStack(err)))
|
||||
return nil, errors.WithStack(service.ErrForgeNotAvailable)
|
||||
}
|
||||
|
||||
if res.StatusCode == http.StatusNotFound {
|
||||
return nil, errors.WithStack(port.ErrFileNotFound)
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(*fileContent.Content)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
// GetIssueTemplate implements port.Forge.
|
||||
func (f *Forge) GetIssueTemplate(ctx context.Context, projectID string) (string, error) {
|
||||
data, err := f.GetFile(ctx, projectID, ".github/ISSUE_TEMPLATE/bug_report.md")
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// GetIssues implements port.Forge.
|
||||
func (f *Forge) GetIssues(ctx context.Context, projectID string, issueIDs ...string) ([]*model.Issue, error) {
|
||||
repo, err := f.getRepository(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
issues := make([]*model.Issue, 0)
|
||||
for _, rawIssueID := range issueIDs {
|
||||
issueID, err := strconv.ParseInt(rawIssueID, 10, 64)
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "could not parse issue id", slog.Any("error", errors.WithStack(err)))
|
||||
issues = append(issues, nil)
|
||||
continue
|
||||
}
|
||||
|
||||
issue, _, err := f.client.Issues.Get(ctx, *repo.Owner.Login, *repo.Name, int(issueID))
|
||||
if err != nil {
|
||||
slog.ErrorContext(ctx, "could not parse retrieve issue", slog.Int("issueID", int(issueID)), slog.String("repository", *repo.FullName), slog.Any("error", errors.WithStack(err)))
|
||||
issues = append(issues, nil)
|
||||
continue
|
||||
}
|
||||
|
||||
issues = append(issues, &model.Issue{
|
||||
ID: strconv.FormatInt(*issue.ID, 10),
|
||||
Title: *issue.Title,
|
||||
Body: *issue.Body,
|
||||
})
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
// GetProject implements port.Forge.
|
||||
func (f *Forge) GetProject(ctx context.Context, rawProjectID string) (*model.Project, error) {
|
||||
repo, err := f.getRepository(ctx, rawProjectID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
var projectDescription string
|
||||
if repo.Description != nil {
|
||||
projectDescription = *repo.Description
|
||||
}
|
||||
|
||||
return &model.Project{
|
||||
ID: strconv.FormatInt(*repo.ID, 10),
|
||||
Name: *repo.Name,
|
||||
Description: projectDescription,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetProjectLanguages implements port.Forge.
|
||||
func (f *Forge) GetProjectLanguages(ctx context.Context, rawProjectID string) ([]string, error) {
|
||||
repo, err := f.getRepository(ctx, rawProjectID)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
mappedLanguages, res, err := f.client.Repositories.ListLanguages(ctx, *repo.Owner.Login, *repo.Name)
|
||||
if err != nil {
|
||||
if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized {
|
||||
slog.ErrorContext(ctx, "could not retrieve repository languages", slog.Any("error", errors.WithStack(err)))
|
||||
return nil, errors.WithStack(service.ErrForgeNotAvailable)
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
languages := make([]string, 0, len(mappedLanguages))
|
||||
|
||||
for l := range mappedLanguages {
|
||||
languages = append(languages, l)
|
||||
}
|
||||
|
||||
return languages, nil
|
||||
}
|
||||
|
||||
func (f *Forge) getRepository(ctx context.Context, rawProjectID string) (*github.Repository, error) {
|
||||
projectID, err := strconv.ParseInt(rawProjectID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
repo, res, err := f.client.Repositories.GetByID(ctx, projectID)
|
||||
if err != nil {
|
||||
if res.StatusCode == http.StatusForbidden || res.StatusCode == http.StatusUnauthorized {
|
||||
slog.ErrorContext(ctx, "could not retrieve repository", slog.Any("error", errors.WithStack(err)))
|
||||
return nil, errors.WithStack(service.ErrForgeNotAvailable)
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func NewForge(client *github.Client) *Forge {
|
||||
return &Forge{client: client}
|
||||
}
|
||||
|
||||
var _ port.Forge = &Forge{}
|
Reference in New Issue
Block a user