Add pull request capability

Change-Id: Ib54054cc9b32930764cc2110203742c3948f9ea3
diff --git a/server/git/gerrit.go b/server/git/gerrit.go
new file mode 100644
index 0000000..6ce60e9
--- /dev/null
+++ b/server/git/gerrit.go
@@ -0,0 +1,454 @@
+package git
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// GerritConfig holds configuration for Gerrit operations
+type GerritConfig struct {
+	Username   string
+	Password   string // Can be HTTP password or API token
+	BaseURL    string
+	HTTPClient *http.Client
+}
+
+// GerritPullRequestProvider implements PullRequestProvider for Gerrit
+type GerritPullRequestProvider struct {
+	config  GerritConfig
+	project string
+}
+
+// NewGerritPullRequestProvider creates a new Gerrit pull request provider
+func NewGerritPullRequestProvider(project string, config GerritConfig) PullRequestProvider {
+	if config.HTTPClient == nil {
+		config.HTTPClient = &http.Client{Timeout: 30 * time.Second}
+	}
+
+	return &GerritPullRequestProvider{
+		config:  config,
+		project: project,
+	}
+}
+
+// Gerrit API response types
+type gerritChange struct {
+	ID              string                     `json:"id"`
+	Number          int                        `json:"_number"`
+	Subject         string                     `json:"subject"`
+	Description     string                     `json:"description"`
+	Status          string                     `json:"status"`
+	Owner           gerritAccount              `json:"owner"`
+	Created         time.Time                  `json:"created"`
+	Updated         time.Time                  `json:"updated"`
+	Branch          string                     `json:"branch"`
+	Topic           string                     `json:"topic"`
+	Labels          map[string]gerritLabelInfo `json:"labels"`
+	Reviewers       map[string][]gerritAccount `json:"reviewers"`
+	CurrentRevision string                     `json:"current_revision"`
+	Revisions       map[string]gerritRevision  `json:"revisions"`
+	Messages        []gerritMessage            `json:"messages"`
+}
+
+type gerritAccount struct {
+	AccountID int    `json:"_account_id"`
+	Name      string `json:"name"`
+	Email     string `json:"email"`
+	Username  string `json:"username"`
+}
+
+type gerritLabelInfo struct {
+	All []gerritApproval `json:"all"`
+}
+
+type gerritApproval struct {
+	AccountID int    `json:"_account_id"`
+	Name      string `json:"name"`
+	Email     string `json:"email"`
+	Value     int    `json:"value"`
+}
+
+type gerritRevision struct {
+	Number int                        `json:"_number"`
+	Ref    string                     `json:"ref"`
+	Fetch  map[string]gerritFetchInfo `json:"fetch"`
+	Commit gerritCommit               `json:"commit"`
+	Files  map[string]gerritFileInfo  `json:"files"`
+}
+
+type gerritFetchInfo struct {
+	URL string `json:"url"`
+	Ref string `json:"ref"`
+}
+
+type gerritCommit struct {
+	Subject string       `json:"subject"`
+	Message string       `json:"message"`
+	Author  gerritPerson `json:"author"`
+}
+
+type gerritPerson struct {
+	Name  string `json:"name"`
+	Email string `json:"email"`
+	Date  string `json:"date"`
+}
+
+type gerritFileInfo struct {
+	Status        string `json:"status"`
+	LinesInserted int    `json:"lines_inserted"`
+	LinesDeleted  int    `json:"lines_deleted"`
+}
+
+type gerritMessage struct {
+	ID             string        `json:"id"`
+	Author         gerritAccount `json:"author"`
+	Message        string        `json:"message"`
+	Date           time.Time     `json:"date"`
+	RevisionNumber int           `json:"_revision_number"`
+}
+
+type gerritCreateChangeRequest struct {
+	Project     string `json:"project"`
+	Subject     string `json:"subject"`
+	Description string `json:"description"`
+	Branch      string `json:"branch"`
+	Topic       string `json:"topic,omitempty"`
+	Base        string `json:"base,omitempty"`
+}
+
+type gerritUpdateChangeRequest struct {
+	Subject     string `json:"subject,omitempty"`
+	Description string `json:"description,omitempty"`
+	Topic       string `json:"topic,omitempty"`
+	Status      string `json:"status,omitempty"`
+}
+
+type gerritReviewRequest struct {
+	Message   string                     `json:"message,omitempty"`
+	Labels    map[string]int             `json:"labels,omitempty"`
+	Reviewers []string                   `json:"reviewers,omitempty"`
+	Comments  map[string][]gerritComment `json:"comments,omitempty"`
+}
+
+type gerritComment struct {
+	Line    int    `json:"line"`
+	Message string `json:"message"`
+}
+
+// CreatePullRequest creates a new change (pull request) on Gerrit
+func (g *GerritPullRequestProvider) CreatePullRequest(ctx context.Context, options PullRequestOptions) (*PullRequest, error) {
+	reqBody := gerritCreateChangeRequest{
+		Project:     g.project,
+		Subject:     options.Title,
+		Description: options.Description,
+		Branch:      options.BaseBranch,
+		Topic:       options.HeadBranch, // Use head branch as topic
+	}
+
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal request body: %w", err)
+	}
+
+	url := fmt.Sprintf("%s/a/changes/", g.config.BaseURL)
+	req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %w", err)
+	}
+
+	req.SetBasicAuth(g.config.Username, g.config.Password)
+	req.Header.Set("Content-Type", "application/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("Gerrit API error: %d", resp.StatusCode)
+	}
+
+	// Gerrit returns the change ID in the response body
+	var changeID string
+	if err := json.NewDecoder(resp.Body).Decode(&changeID); err != nil {
+		return nil, fmt.Errorf("failed to decode response: %w", err)
+	}
+
+	// Remove the ")]}'" prefix that Gerrit adds to responses
+	changeID = strings.TrimPrefix(changeID, ")]}'")
+
+	// Fetch the created change
+	return g.GetPullRequest(ctx, changeID)
+}
+
+// GetPullRequest retrieves a change by ID
+func (g *GerritPullRequestProvider) GetPullRequest(ctx context.Context, id string) (*PullRequest, error) {
+	url := fmt.Sprintf("%s/a/changes/%s?o=DETAILED_ACCOUNTS&o=DETAILED_LABELS&o=MESSAGES&o=CURRENT_REVISION&o=CURRENT_COMMIT", g.config.BaseURL, id)
+	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %w", err)
+	}
+
+	req.SetBasicAuth(g.config.Username, g.config.Password)
+
+	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("Gerrit API error: %d", resp.StatusCode)
+	}
+
+	// Read and remove Gerrit's ")]}'" prefix
+	body := make([]byte, 4)
+	if _, err := resp.Body.Read(body); err != nil {
+		return nil, fmt.Errorf("failed to read response prefix: %w", err)
+	}
+	if string(body) != ")]}'" {
+		return nil, fmt.Errorf("unexpected response prefix: %s", string(body))
+	}
+
+	var gerritChange gerritChange
+	if err := json.NewDecoder(resp.Body).Decode(&gerritChange); err != nil {
+		return nil, fmt.Errorf("failed to decode response: %w", err)
+	}
+
+	return g.convertGerritChange(gerritChange), nil
+}
+
+// ListPullRequests lists changes
+func (g *GerritPullRequestProvider) ListPullRequests(ctx context.Context, options ListPullRequestOptions) ([]PullRequest, error) {
+	url := fmt.Sprintf("%s/a/changes/", g.config.BaseURL)
+	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()
+	q.Add("q", fmt.Sprintf("project:%s", g.project))
+
+	if options.State != "" {
+		switch options.State {
+		case "open":
+			q.Add("q", "status:open")
+		case "closed":
+			q.Add("q", "status:closed")
+		}
+	}
+	if options.Author != "" {
+		q.Add("q", fmt.Sprintf("owner:%s", options.Author))
+	}
+	if options.Assignee != "" {
+		q.Add("q", fmt.Sprintf("reviewer:%s", options.Assignee))
+	}
+	if options.BaseBranch != "" {
+		q.Add("q", fmt.Sprintf("branch:%s", options.BaseBranch))
+	}
+	if options.Limit > 0 {
+		q.Add("n", strconv.Itoa(options.Limit))
+	}
+	req.URL.RawQuery = q.Encode()
+
+	req.SetBasicAuth(g.config.Username, g.config.Password)
+
+	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("Gerrit API error: %d", resp.StatusCode)
+	}
+
+	// Read and remove Gerrit's ")]}'" prefix
+	body := make([]byte, 4)
+	if _, err := resp.Body.Read(body); err != nil {
+		return nil, fmt.Errorf("failed to read response prefix: %w", err)
+	}
+	if string(body) != ")]}'" {
+		return nil, fmt.Errorf("unexpected response prefix: %s", string(body))
+	}
+
+	var gerritChanges []gerritChange
+	if err := json.NewDecoder(resp.Body).Decode(&gerritChanges); err != nil {
+		return nil, fmt.Errorf("failed to decode response: %w", err)
+	}
+
+	prs := make([]PullRequest, len(gerritChanges))
+	for i, change := range gerritChanges {
+		prs[i] = *g.convertGerritChange(change)
+	}
+
+	return prs, nil
+}
+
+// UpdatePullRequest updates a change
+func (g *GerritPullRequestProvider) UpdatePullRequest(ctx context.Context, id string, options PullRequestOptions) (*PullRequest, error) {
+	reqBody := gerritUpdateChangeRequest{
+		Subject:     options.Title,
+		Description: options.Description,
+	}
+
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal request body: %w", err)
+	}
+
+	url := fmt.Sprintf("%s/a/changes/%s", g.config.BaseURL, id)
+	req, err := http.NewRequestWithContext(ctx, "PUT", url, bytes.NewBuffer(jsonBody))
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %w", err)
+	}
+
+	req.SetBasicAuth(g.config.Username, g.config.Password)
+	req.Header.Set("Content-Type", "application/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("Gerrit API error: %d", resp.StatusCode)
+	}
+
+	// Fetch the updated change
+	return g.GetPullRequest(ctx, id)
+}
+
+// ClosePullRequest closes a change
+func (g *GerritPullRequestProvider) ClosePullRequest(ctx context.Context, id string) error {
+	reqBody := gerritUpdateChangeRequest{
+		Status: "ABANDONED",
+	}
+
+	jsonBody, err := json.Marshal(reqBody)
+	if err != nil {
+		return fmt.Errorf("failed to marshal request body: %w", err)
+	}
+
+	url := fmt.Sprintf("%s/a/changes/%s/abandon", g.config.BaseURL, id)
+	req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
+	if err != nil {
+		return fmt.Errorf("failed to create request: %w", err)
+	}
+
+	req.SetBasicAuth(g.config.Username, g.config.Password)
+	req.Header.Set("Content-Type", "application/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("Gerrit API error: %d", resp.StatusCode)
+	}
+
+	return nil
+}
+
+// MergePullRequest merges a change
+func (g *GerritPullRequestProvider) MergePullRequest(ctx context.Context, id string, options MergePullRequestOptions) error {
+	url := fmt.Sprintf("%s/a/changes/%s/submit", g.config.BaseURL, id)
+	req, err := http.NewRequestWithContext(ctx, "POST", url, nil)
+	if err != nil {
+		return fmt.Errorf("failed to create request: %w", err)
+	}
+
+	req.SetBasicAuth(g.config.Username, g.config.Password)
+
+	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("Gerrit API error: %d", resp.StatusCode)
+	}
+
+	return nil
+}
+
+// convertGerritChange converts a Gerrit change to our PullRequest type
+func (g *GerritPullRequestProvider) convertGerritChange(change gerritChange) *PullRequest {
+	// Extract labels
+	var labels []string
+	for labelName := range change.Labels {
+		labels = append(labels, labelName)
+	}
+
+	// Extract reviewers
+	var reviewers []Author
+	if reviewersMap, exists := change.Reviewers["REVIEWER"]; exists {
+		for _, reviewer := range reviewersMap {
+			reviewers = append(reviewers, Author{
+				Name:  reviewer.Name,
+				Email: reviewer.Email,
+			})
+		}
+	}
+
+	// Extract comments from messages
+	var comments []PullRequestComment
+	for _, message := range change.Messages {
+		if message.Message != "" {
+			comments = append(comments, PullRequestComment{
+				ID: message.ID,
+				Author: Author{
+					Name:  message.Author.Name,
+					Email: message.Author.Email,
+				},
+				Content:   message.Message,
+				CreatedAt: message.Date,
+				UpdatedAt: message.Date,
+			})
+		}
+	}
+
+	// Determine state
+	state := "open"
+	switch change.Status {
+	case "MERGED":
+		state = "merged"
+	case "ABANDONED":
+		state = "closed"
+	}
+
+	return &PullRequest{
+		ID:          change.ID,
+		Number:      change.Number,
+		Title:       change.Subject,
+		Description: change.Description,
+		State:       state,
+		Author: Author{
+			Name:  change.Owner.Name,
+			Email: change.Owner.Email,
+		},
+		CreatedAt:  change.Created,
+		UpdatedAt:  change.Updated,
+		BaseBranch: change.Branch,
+		HeadBranch: change.Topic, // Use topic as head branch
+		BaseRepo:   g.project,
+		HeadRepo:   g.project, // Gerrit changes are within the same project
+		Labels:     labels,
+		Assignees:  []Author{}, // Gerrit doesn't have assignees in the same way
+		Reviewers:  reviewers,
+		Commits:    []Commit{}, // Would need additional API call to populate
+		Comments:   comments,
+	}
+}