blob: 6ce60e9474243c655a0d4fd383a1c3d228e3a817 [file] [log] [blame]
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,
}
}