Add pull request capability
Change-Id: Ib54054cc9b32930764cc2110203742c3948f9ea3
diff --git a/server/git/github.go b/server/git/github.go
new file mode 100644
index 0000000..8f15c68
--- /dev/null
+++ b/server/git/github.go
@@ -0,0 +1,397 @@
+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
+ }
+}