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
+	}
+}