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