blob: 8f15c68c8e7cd57b4eaa38725a48bb3241c9d70d [file] [log] [blame]
package git
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
// GitHubConfig holds configuration for GitHub operations
type GitHubConfig struct {
Token string
BaseURL string // Default: https://api.github.com
HTTPClient *http.Client
}
// GitHubPullRequestProvider implements PullRequestProvider for GitHub
type GitHubPullRequestProvider struct {
config GitHubConfig
owner string
repo string
}
// NewGitHubPullRequestProvider creates a new GitHub pull request provider
func NewGitHubPullRequestProvider(owner, repo string, config GitHubConfig) PullRequestProvider {
if config.BaseURL == "" {
config.BaseURL = "https://api.github.com"
}
if config.HTTPClient == nil {
config.HTTPClient = &http.Client{Timeout: 30 * time.Second}
}
return &GitHubPullRequestProvider{
config: config,
owner: owner,
repo: repo,
}
}
// GitHub API response types
type githubPullRequest struct {
ID int `json:"id"`
Number int `json:"number"`
Title string `json:"title"`
Body string `json:"body"`
State string `json:"state"`
User githubUser `json:"user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Base githubBranch `json:"base"`
Head githubBranch `json:"head"`
Labels []githubLabel `json:"labels"`
Assignees []githubUser `json:"assignees"`
RequestedReviewers []githubUser `json:"requested_reviewers"`
CommitsURL string `json:"commits_url"`
CommentsURL string `json:"comments_url"`
}
type githubUser struct {
Login string `json:"login"`
ID int `json:"id"`
Type string `json:"type"`
}
type githubBranch struct {
Ref string `json:"ref"`
SHA string `json:"sha"`
Repo githubRepo `json:"repo"`
}
type githubRepo struct {
FullName string `json:"full_name"`
}
type githubLabel struct {
Name string `json:"name"`
Color string `json:"color"`
}
type githubCreatePRRequest struct {
Title string `json:"title"`
Body string `json:"body"`
Head string `json:"head"`
Base string `json:"base"`
Labels []string `json:"labels,omitempty"`
Assignees []string `json:"assignees,omitempty"`
Reviewers []string `json:"reviewers,omitempty"`
Draft bool `json:"draft,omitempty"`
}
type githubUpdatePRRequest struct {
Title string `json:"title,omitempty"`
Body string `json:"body,omitempty"`
State string `json:"state,omitempty"`
Base string `json:"base,omitempty"`
Labels []string `json:"labels,omitempty"`
Assignees []string `json:"assignees,omitempty"`
}
type githubMergePRRequest struct {
CommitTitle string `json:"commit_title,omitempty"`
CommitMsg string `json:"commit_message,omitempty"`
MergeMethod string `json:"merge_method,omitempty"`
}
// CreatePullRequest creates a new pull request on GitHub
func (g *GitHubPullRequestProvider) CreatePullRequest(ctx context.Context, options PullRequestOptions) (*PullRequest, error) {
reqBody := githubCreatePRRequest{
Title: options.Title,
Body: options.Description,
Head: options.HeadBranch,
Base: options.BaseBranch,
Labels: options.Labels,
Assignees: options.Assignees,
Reviewers: options.Reviewers,
Draft: options.Draft,
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
url := fmt.Sprintf("%s/repos/%s/%s/pulls", g.config.BaseURL, g.owner, g.repo)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "token "+g.config.Token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/vnd.github.v3+json")
resp, err := g.config.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("GitHub API error: %d", resp.StatusCode)
}
var githubPR githubPullRequest
if err := json.NewDecoder(resp.Body).Decode(&githubPR); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return g.convertGitHubPR(githubPR), nil
}
// GetPullRequest retrieves a pull request by number
func (g *GitHubPullRequestProvider) GetPullRequest(ctx context.Context, id string) (*PullRequest, error) {
url := fmt.Sprintf("%s/repos/%s/%s/pulls/%s", g.config.BaseURL, g.owner, g.repo, id)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "token "+g.config.Token)
req.Header.Set("Accept", "application/vnd.github.v3+json")
resp, err := g.config.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GitHub API error: %d", resp.StatusCode)
}
var githubPR githubPullRequest
if err := json.NewDecoder(resp.Body).Decode(&githubPR); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return g.convertGitHubPR(githubPR), nil
}
// ListPullRequests lists pull requests
func (g *GitHubPullRequestProvider) ListPullRequests(ctx context.Context, options ListPullRequestOptions) ([]PullRequest, error) {
url := fmt.Sprintf("%s/repos/%s/%s/pulls", g.config.BaseURL, g.owner, g.repo)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
// Add query parameters
q := req.URL.Query()
if options.State != "" {
q.Add("state", options.State)
}
if options.Author != "" {
q.Add("author", options.Author)
}
if options.Assignee != "" {
q.Add("assignee", options.Assignee)
}
if options.BaseBranch != "" {
q.Add("base", options.BaseBranch)
}
if options.HeadBranch != "" {
q.Add("head", options.HeadBranch)
}
if options.Limit > 0 {
q.Add("per_page", fmt.Sprintf("%d", options.Limit))
}
req.URL.RawQuery = q.Encode()
req.Header.Set("Authorization", "token "+g.config.Token)
req.Header.Set("Accept", "application/vnd.github.v3+json")
resp, err := g.config.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GitHub API error: %d", resp.StatusCode)
}
var githubPRs []githubPullRequest
if err := json.NewDecoder(resp.Body).Decode(&githubPRs); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
prs := make([]PullRequest, len(githubPRs))
for i, githubPR := range githubPRs {
prs[i] = *g.convertGitHubPR(githubPR)
}
return prs, nil
}
// UpdatePullRequest updates a pull request
func (g *GitHubPullRequestProvider) UpdatePullRequest(ctx context.Context, id string, options PullRequestOptions) (*PullRequest, error) {
reqBody := githubUpdatePRRequest{
Title: options.Title,
Body: options.Description,
Base: options.BaseBranch,
Labels: options.Labels,
Assignees: options.Assignees,
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
url := fmt.Sprintf("%s/repos/%s/%s/pulls/%s", g.config.BaseURL, g.owner, g.repo, id)
req, err := http.NewRequestWithContext(ctx, "PATCH", url, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "token "+g.config.Token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/vnd.github.v3+json")
resp, err := g.config.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GitHub API error: %d", resp.StatusCode)
}
var githubPR githubPullRequest
if err := json.NewDecoder(resp.Body).Decode(&githubPR); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return g.convertGitHubPR(githubPR), nil
}
// ClosePullRequest closes a pull request
func (g *GitHubPullRequestProvider) ClosePullRequest(ctx context.Context, id string) error {
reqBody := githubUpdatePRRequest{
State: "closed",
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
}
url := fmt.Sprintf("%s/repos/%s/%s/pulls/%s", g.config.BaseURL, g.owner, g.repo, id)
req, err := http.NewRequestWithContext(ctx, "PATCH", url, bytes.NewBuffer(jsonBody))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "token "+g.config.Token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/vnd.github.v3+json")
resp, err := g.config.HTTPClient.Do(req)
if err != nil {
return fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("GitHub API error: %d", resp.StatusCode)
}
return nil
}
// MergePullRequest merges a pull request
func (g *GitHubPullRequestProvider) MergePullRequest(ctx context.Context, id string, options MergePullRequestOptions) error {
reqBody := githubMergePRRequest{
CommitTitle: options.CommitTitle,
CommitMsg: options.CommitMsg,
MergeMethod: options.MergeMethod,
}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
}
url := fmt.Sprintf("%s/repos/%s/%s/pulls/%s/merge", g.config.BaseURL, g.owner, g.repo, id)
req, err := http.NewRequestWithContext(ctx, "PUT", url, bytes.NewBuffer(jsonBody))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "token "+g.config.Token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/vnd.github.v3+json")
resp, err := g.config.HTTPClient.Do(req)
if err != nil {
return fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("GitHub API error: %d", resp.StatusCode)
}
return nil
}
// convertGitHubPR converts a GitHub pull request to our PullRequest type
func (g *GitHubPullRequestProvider) convertGitHubPR(githubPR githubPullRequest) *PullRequest {
labels := make([]string, len(githubPR.Labels))
for i, label := range githubPR.Labels {
labels[i] = label.Name
}
assignees := make([]Author, len(githubPR.Assignees))
for i, assignee := range githubPR.Assignees {
assignees[i] = Author{
Name: assignee.Login,
Email: "", // GitHub API doesn't provide email in this context
}
}
reviewers := make([]Author, len(githubPR.RequestedReviewers))
for i, reviewer := range githubPR.RequestedReviewers {
reviewers[i] = Author{
Name: reviewer.Login,
Email: "", // GitHub API doesn't provide email in this context
}
}
return &PullRequest{
ID: fmt.Sprintf("%d", githubPR.ID),
Number: githubPR.Number,
Title: githubPR.Title,
Description: githubPR.Body,
State: githubPR.State,
Author: Author{
Name: githubPR.User.Login,
Email: "", // GitHub API doesn't provide email in this context
},
CreatedAt: githubPR.CreatedAt,
UpdatedAt: githubPR.UpdatedAt,
BaseBranch: githubPR.Base.Ref,
HeadBranch: githubPR.Head.Ref,
BaseRepo: githubPR.Base.Repo.FullName,
HeadRepo: githubPR.Head.Repo.FullName,
Labels: labels,
Assignees: assignees,
Reviewers: reviewers,
Commits: []Commit{}, // Would need additional API call to populate
Comments: []PullRequestComment{}, // Would need additional API call to populate
}
}