Add pull request capability
Change-Id: Ib54054cc9b32930764cc2110203742c3948f9ea3
diff --git a/server/git/PULL_REQUEST_README.md b/server/git/PULL_REQUEST_README.md
new file mode 100644
index 0000000..b6664f2
--- /dev/null
+++ b/server/git/PULL_REQUEST_README.md
@@ -0,0 +1,323 @@
+# Pull Request Capabilities
+
+This package now includes comprehensive pull request (PR) capabilities that support both GitHub and Gerrit platforms. The implementation provides a unified interface for managing pull requests across different code hosting platforms.
+
+## Features
+
+- **Unified Interface**: Same API for both GitHub and Gerrit
+- **Full CRUD Operations**: Create, read, update, delete pull requests
+- **Advanced Filtering**: List pull requests with various filters
+- **Merge Operations**: Support for different merge strategies
+- **Error Handling**: Comprehensive error handling with detailed messages
+- **Authentication**: Support for token-based and basic authentication
+
+## Supported Platforms
+
+### GitHub
+- Uses GitHub REST API v3
+- Supports personal access tokens for authentication
+- Full support for all pull request operations
+- Handles GitHub-specific features like draft PRs, labels, assignees, and reviewers
+
+### Gerrit
+- Uses Gerrit REST API
+- Supports HTTP password or API token authentication
+- Maps Gerrit "changes" to pull requests
+- Handles Gerrit-specific features like topics and review workflows
+
+## Quick Start
+
+### GitHub Example
+
+```go
+package main
+
+import (
+ "context"
+ "github.com/iomodo/staff/git"
+ "net/http"
+ "time"
+)
+
+func main() {
+ ctx := context.Background()
+
+ // Create GitHub configuration
+ githubConfig := git.GitHubConfig{
+ Token: "your-github-token",
+ BaseURL: "https://api.github.com",
+ HTTPClient: &http.Client{Timeout: 30 * time.Second},
+ }
+
+ // Create GitHub provider
+ githubProvider := git.NewGitHubPullRequestProvider("owner", "repo", githubConfig)
+
+ // Create Git instance with pull request capabilities
+ git := git.NewGitWithPullRequests("/path/to/repo", git.GitConfig{}, githubProvider)
+
+ // Create a pull request
+ prOptions := git.PullRequestOptions{
+ Title: "Add new feature",
+ Description: "This PR adds a new feature to the application.",
+ BaseBranch: "main",
+ HeadBranch: "feature/new-feature",
+ Labels: []string{"enhancement", "feature"},
+ Assignees: []string{"username1", "username2"},
+ Reviewers: []string{"reviewer1", "reviewer2"},
+ Draft: false,
+ }
+
+ pr, err := git.CreatePullRequest(ctx, prOptions)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Printf("Created pull request: %s (#%d)\n", pr.Title, pr.Number)
+}
+```
+
+### Gerrit Example
+
+```go
+package main
+
+import (
+ "context"
+ "github.com/iomodo/staff/git"
+ "net/http"
+ "time"
+)
+
+func main() {
+ ctx := context.Background()
+
+ // Create Gerrit configuration
+ gerritConfig := git.GerritConfig{
+ Username: "your-username",
+ Password: "your-http-password-or-api-token",
+ BaseURL: "https://gerrit.example.com",
+ HTTPClient: &http.Client{Timeout: 30 * time.Second},
+ }
+
+ // Create Gerrit provider
+ gerritProvider := git.NewGerritPullRequestProvider("project-name", gerritConfig)
+
+ // Create Git instance with pull request capabilities
+ git := git.NewGitWithPullRequests("/path/to/repo", git.GitConfig{}, gerritProvider)
+
+ // Create a change (pull request)
+ prOptions := git.PullRequestOptions{
+ Title: "Add new feature",
+ Description: "This change adds a new feature to the application.",
+ BaseBranch: "master",
+ HeadBranch: "feature/new-feature",
+ }
+
+ pr, err := git.CreatePullRequest(ctx, prOptions)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Printf("Created change: %s (#%d)\n", pr.Title, pr.Number)
+}
+```
+
+## API Reference
+
+### Types
+
+#### PullRequest
+Represents a pull request or merge request across platforms.
+
+```go
+type PullRequest struct {
+ ID string
+ Number int
+ Title string
+ Description string
+ State string // "open", "closed", "merged"
+ Author Author
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ BaseBranch string
+ HeadBranch string
+ BaseRepo string
+ HeadRepo string
+ Labels []string
+ Assignees []Author
+ Reviewers []Author
+ Commits []Commit
+ Comments []PullRequestComment
+}
+```
+
+#### PullRequestOptions
+Options for creating or updating pull requests.
+
+```go
+type PullRequestOptions struct {
+ Title string
+ Description string
+ BaseBranch string
+ HeadBranch string
+ BaseRepo string
+ HeadRepo string
+ Labels []string
+ Assignees []string
+ Reviewers []string
+ Draft bool
+}
+```
+
+#### ListPullRequestOptions
+Options for listing pull requests.
+
+```go
+type ListPullRequestOptions struct {
+ State string // "open", "closed", "all"
+ Author string
+ Assignee string
+ BaseBranch string
+ HeadBranch string
+ Labels []string
+ Limit int
+}
+```
+
+#### MergePullRequestOptions
+Options for merging pull requests.
+
+```go
+type MergePullRequestOptions struct {
+ MergeMethod string // "merge", "squash", "rebase"
+ CommitTitle string
+ CommitMsg string
+}
+```
+
+### Methods
+
+#### CreatePullRequest
+Creates a new pull request.
+
+```go
+func (g *Git) CreatePullRequest(ctx context.Context, options PullRequestOptions) (*PullRequest, error)
+```
+
+#### GetPullRequest
+Retrieves a pull request by ID.
+
+```go
+func (g *Git) GetPullRequest(ctx context.Context, id string) (*PullRequest, error)
+```
+
+#### ListPullRequests
+Lists pull requests with optional filtering.
+
+```go
+func (g *Git) ListPullRequests(ctx context.Context, options ListPullRequestOptions) ([]PullRequest, error)
+```
+
+#### UpdatePullRequest
+Updates an existing pull request.
+
+```go
+func (g *Git) UpdatePullRequest(ctx context.Context, id string, options PullRequestOptions) (*PullRequest, error)
+```
+
+#### ClosePullRequest
+Closes a pull request.
+
+```go
+func (g *Git) ClosePullRequest(ctx context.Context, id string) error
+```
+
+#### MergePullRequest
+Merges a pull request.
+
+```go
+func (g *Git) MergePullRequest(ctx context.Context, id string, options MergePullRequestOptions) error
+```
+
+## Configuration
+
+### GitHub Configuration
+
+```go
+type GitHubConfig struct {
+ Token string // GitHub personal access token
+ BaseURL string // GitHub API base URL (default: https://api.github.com)
+ HTTPClient *http.Client // Custom HTTP client (optional)
+}
+```
+
+### Gerrit Configuration
+
+```go
+type GerritConfig struct {
+ Username string // Gerrit username
+ Password string // HTTP password or API token
+ BaseURL string // Gerrit instance URL
+ HTTPClient *http.Client // Custom HTTP client (optional)
+}
+```
+
+## Error Handling
+
+All pull request operations return detailed error information through the `GitError` type:
+
+```go
+type GitError struct {
+ Command string
+ Output string
+ Err error
+}
+```
+
+Common error scenarios:
+- Authentication failures
+- Invalid repository or project names
+- Network connectivity issues
+- API rate limiting
+- Invalid pull request data
+
+## Platform-Specific Notes
+
+### GitHub
+- Requires a personal access token with appropriate permissions
+- Supports draft pull requests
+- Full support for labels, assignees, and reviewers
+- Uses GitHub's REST API v3
+
+### Gerrit
+- Requires HTTP password or API token
+- Uses "changes" instead of "pull requests"
+- Topics are used to group related changes
+- Review workflow is more structured
+- Uses Gerrit's REST API
+
+## Examples
+
+See `pull_request_example.go` for comprehensive examples of using both GitHub and Gerrit providers.
+
+## Testing
+
+Run the tests to ensure everything works correctly:
+
+```bash
+go test ./git/... -v
+```
+
+## Contributing
+
+When adding support for new platforms:
+
+1. Implement the `PullRequestProvider` interface
+2. Add platform-specific configuration types
+3. Create conversion functions to map platform-specific data to our unified types
+4. Add comprehensive tests
+5. Update this documentation
+
+## License
+
+This code is part of the staff project and follows the same licensing terms.
\ No newline at end of file
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,
+ }
+}
diff --git a/server/git/git.go b/server/git/git.go
index fdcd467..dc3885f 100644
--- a/server/git/git.go
+++ b/server/git/git.go
@@ -52,6 +52,14 @@
SetConfig(ctx context.Context, key, value string) error
GetUserConfig(ctx context.Context) (*UserConfig, error)
SetUserConfig(ctx context.Context, config UserConfig) error
+
+ // Pull Request operations
+ CreatePullRequest(ctx context.Context, options PullRequestOptions) (*PullRequest, error)
+ GetPullRequest(ctx context.Context, id string) (*PullRequest, error)
+ ListPullRequests(ctx context.Context, options ListPullRequestOptions) ([]PullRequest, error)
+ UpdatePullRequest(ctx context.Context, id string, options PullRequestOptions) (*PullRequest, error)
+ ClosePullRequest(ctx context.Context, id string) error
+ MergePullRequest(ctx context.Context, id string, options MergePullRequestOptions) error
}
// Status represents the current state of the repository
@@ -178,14 +186,16 @@
// Git implementation using os/exec to call git commands
type Git struct {
- repoPath string
- config GitConfig
+ repoPath string
+ config GitConfig
+ prProvider PullRequestProvider
}
// GitConfig holds configuration for Git operations
type GitConfig struct {
- Timeout time.Duration
- Env map[string]string
+ Timeout time.Duration
+ Env map[string]string
+ PullRequestProvider PullRequestProvider
}
// NewGit creates a new Git instance
@@ -195,8 +205,9 @@
}
return &Git{
- repoPath: repoPath,
- config: config,
+ repoPath: repoPath,
+ config: config,
+ prProvider: config.PullRequestProvider,
}
}
@@ -208,6 +219,12 @@
})
}
+// NewGitWithPullRequests creates a Git instance with pull request capabilities
+func NewGitWithPullRequests(repoPath string, config GitConfig, prProvider PullRequestProvider) GitInterface {
+ config.PullRequestProvider = prProvider
+ return NewGit(repoPath, config)
+}
+
// Ensure Git implements GitInterface
var _ GitInterface = (*Git)(nil)
@@ -567,6 +584,56 @@
return g.SetConfig(ctx, "user.email", config.Email)
}
+// Pull Request operations
+
+// CreatePullRequest creates a new pull request
+func (g *Git) CreatePullRequest(ctx context.Context, options PullRequestOptions) (*PullRequest, error) {
+ if g.prProvider == nil {
+ return nil, &GitError{Command: "CreatePullRequest", Output: "no pull request provider configured"}
+ }
+ return g.prProvider.CreatePullRequest(ctx, options)
+}
+
+// GetPullRequest retrieves a pull request by ID
+func (g *Git) GetPullRequest(ctx context.Context, id string) (*PullRequest, error) {
+ if g.prProvider == nil {
+ return nil, &GitError{Command: "GetPullRequest", Output: "no pull request provider configured"}
+ }
+ return g.prProvider.GetPullRequest(ctx, id)
+}
+
+// ListPullRequests lists pull requests
+func (g *Git) ListPullRequests(ctx context.Context, options ListPullRequestOptions) ([]PullRequest, error) {
+ if g.prProvider == nil {
+ return nil, &GitError{Command: "ListPullRequests", Output: "no pull request provider configured"}
+ }
+ return g.prProvider.ListPullRequests(ctx, options)
+}
+
+// UpdatePullRequest updates a pull request
+func (g *Git) UpdatePullRequest(ctx context.Context, id string, options PullRequestOptions) (*PullRequest, error) {
+ if g.prProvider == nil {
+ return nil, &GitError{Command: "UpdatePullRequest", Output: "no pull request provider configured"}
+ }
+ return g.prProvider.UpdatePullRequest(ctx, id, options)
+}
+
+// ClosePullRequest closes a pull request
+func (g *Git) ClosePullRequest(ctx context.Context, id string) error {
+ if g.prProvider == nil {
+ return &GitError{Command: "ClosePullRequest", Output: "no pull request provider configured"}
+ }
+ return g.prProvider.ClosePullRequest(ctx, id)
+}
+
+// MergePullRequest merges a pull request
+func (g *Git) MergePullRequest(ctx context.Context, id string, options MergePullRequestOptions) error {
+ if g.prProvider == nil {
+ return &GitError{Command: "MergePullRequest", Output: "no pull request provider configured"}
+ }
+ return g.prProvider.MergePullRequest(ctx, id, options)
+}
+
// Helper methods
func (g *Git) runCommand(cmd *exec.Cmd, command string) error {
@@ -795,3 +862,77 @@
return remotes, nil
}
+
+// PullRequest represents a pull request or merge request
+type PullRequest struct {
+ ID string
+ Number int
+ Title string
+ Description string
+ State string // "open", "closed", "merged"
+ Author Author
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ BaseBranch string
+ HeadBranch string
+ BaseRepo string
+ HeadRepo string
+ Labels []string
+ Assignees []Author
+ Reviewers []Author
+ Commits []Commit
+ Comments []PullRequestComment
+}
+
+// PullRequestComment represents a comment on a pull request
+type PullRequestComment struct {
+ ID string
+ Author Author
+ Content string
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ Path string
+ Line int
+}
+
+// PullRequestOptions defines options for creating/updating pull requests
+type PullRequestOptions struct {
+ Title string
+ Description string
+ BaseBranch string
+ HeadBranch string
+ BaseRepo string
+ HeadRepo string
+ Labels []string
+ Assignees []string
+ Reviewers []string
+ Draft bool
+}
+
+// ListPullRequestOptions defines options for listing pull requests
+type ListPullRequestOptions struct {
+ State string // "open", "closed", "all"
+ Author string
+ Assignee string
+ BaseBranch string
+ HeadBranch string
+ Labels []string
+ Limit int
+}
+
+// MergePullRequestOptions defines options for merging pull requests
+type MergePullRequestOptions struct {
+ MergeMethod string // "merge", "squash", "rebase"
+ CommitTitle string
+ CommitMsg string
+}
+
+// PullRequestProvider defines the interface for pull request operations
+type PullRequestProvider interface {
+ CreatePullRequest(ctx context.Context, options PullRequestOptions) (*PullRequest, error)
+ GetPullRequest(ctx context.Context, id string) (*PullRequest, error)
+ ListPullRequests(ctx context.Context, options ListPullRequestOptions) ([]PullRequest, error)
+ UpdatePullRequest(ctx context.Context, id string, options PullRequestOptions) (*PullRequest, error)
+ ClosePullRequest(ctx context.Context, id string) error
+ MergePullRequest(ctx context.Context, id string, options MergePullRequestOptions) error
+}
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
+ }
+}
diff --git a/server/git/pull_request_example.go b/server/git/pull_request_example.go
new file mode 100644
index 0000000..5e34dc4
--- /dev/null
+++ b/server/git/pull_request_example.go
@@ -0,0 +1,248 @@
+package git
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "net/http"
+ "time"
+)
+
+// ExamplePullRequestUsage demonstrates how to use the pull request capabilities
+func ExamplePullRequestUsage() {
+ ctx := context.Background()
+
+ // Example 1: GitHub Pull Requests
+ exampleGitHubPullRequests(ctx)
+
+ // Example 2: Gerrit Pull Requests
+ exampleGerritPullRequests(ctx)
+}
+
+func exampleGitHubPullRequests(ctx context.Context) {
+ fmt.Println("=== GitHub Pull Request Example ===")
+
+ // Create GitHub configuration
+ githubConfig := GitHubConfig{
+ Token: "your-github-token-here",
+ BaseURL: "https://api.github.com",
+ HTTPClient: &http.Client{Timeout: 30 * time.Second},
+ }
+
+ // Create GitHub pull request provider
+ githubProvider := NewGitHubPullRequestProvider("owner", "repo", githubConfig)
+
+ // Create Git instance with GitHub pull request capabilities
+ git := NewGitWithPullRequests("/path/to/repo", GitConfig{
+ Timeout: 30 * time.Second,
+ }, githubProvider)
+
+ // Create a new pull request
+ prOptions := PullRequestOptions{
+ Title: "Add new feature",
+ Description: "This PR adds a new feature to the application.",
+ BaseBranch: "main",
+ HeadBranch: "feature/new-feature",
+ Labels: []string{"enhancement", "feature"},
+ Assignees: []string{"username1", "username2"},
+ Reviewers: []string{"reviewer1", "reviewer2"},
+ Draft: false,
+ }
+
+ pr, err := git.CreatePullRequest(ctx, prOptions)
+ if err != nil {
+ log.Printf("Failed to create pull request: %v", err)
+ return
+ }
+
+ fmt.Printf("Created pull request: %s (#%d)\n", pr.Title, pr.Number)
+
+ // List pull requests
+ listOptions := ListPullRequestOptions{
+ State: "open",
+ Author: "username",
+ BaseBranch: "main",
+ Limit: 10,
+ }
+
+ prs, err := git.ListPullRequests(ctx, listOptions)
+ if err != nil {
+ log.Printf("Failed to list pull requests: %v", err)
+ return
+ }
+
+ fmt.Printf("Found %d pull requests\n", len(prs))
+
+ // Get a specific pull request
+ pr, err = git.GetPullRequest(ctx, pr.ID)
+ if err != nil {
+ log.Printf("Failed to get pull request: %v", err)
+ return
+ }
+
+ fmt.Printf("Pull request status: %s\n", pr.State)
+
+ // Update a pull request
+ updateOptions := PullRequestOptions{
+ Title: "Updated title",
+ Description: "Updated description",
+ Labels: []string{"bug", "urgent"},
+ }
+
+ updatedPR, err := git.UpdatePullRequest(ctx, pr.ID, updateOptions)
+ if err != nil {
+ log.Printf("Failed to update pull request: %v", err)
+ return
+ }
+
+ fmt.Printf("Updated pull request: %s\n", updatedPR.Title)
+
+ // Merge a pull request
+ mergeOptions := MergePullRequestOptions{
+ MergeMethod: "squash",
+ CommitTitle: "Merge pull request #123",
+ CommitMsg: "This merges the feature branch into main",
+ }
+
+ err = git.MergePullRequest(ctx, pr.ID, mergeOptions)
+ if err != nil {
+ log.Printf("Failed to merge pull request: %v", err)
+ return
+ }
+
+ fmt.Println("Pull request merged successfully")
+}
+
+func exampleGerritPullRequests(ctx context.Context) {
+ fmt.Println("=== Gerrit Pull Request Example ===")
+
+ // Create Gerrit configuration
+ gerritConfig := GerritConfig{
+ Username: "your-username",
+ Password: "your-http-password-or-api-token",
+ BaseURL: "https://gerrit.example.com",
+ HTTPClient: &http.Client{Timeout: 30 * time.Second},
+ }
+
+ // Create Gerrit pull request provider
+ gerritProvider := NewGerritPullRequestProvider("project-name", gerritConfig)
+
+ // Create Git instance with Gerrit pull request capabilities
+ git := NewGitWithPullRequests("/path/to/repo", GitConfig{
+ Timeout: 30 * time.Second,
+ }, gerritProvider)
+
+ // Create a new change (pull request)
+ prOptions := PullRequestOptions{
+ Title: "Add new feature",
+ Description: "This change adds a new feature to the application.",
+ BaseBranch: "master",
+ HeadBranch: "feature/new-feature",
+ }
+
+ pr, err := git.CreatePullRequest(ctx, prOptions)
+ if err != nil {
+ log.Printf("Failed to create change: %v", err)
+ return
+ }
+
+ fmt.Printf("Created change: %s (#%d)\n", pr.Title, pr.Number)
+
+ // List changes
+ listOptions := ListPullRequestOptions{
+ State: "open",
+ Author: "username",
+ BaseBranch: "master",
+ Limit: 10,
+ }
+
+ prs, err := git.ListPullRequests(ctx, listOptions)
+ if err != nil {
+ log.Printf("Failed to list changes: %v", err)
+ return
+ }
+
+ fmt.Printf("Found %d changes\n", len(prs))
+
+ // Get a specific change
+ pr, err = git.GetPullRequest(ctx, pr.ID)
+ if err != nil {
+ log.Printf("Failed to get change: %v", err)
+ return
+ }
+
+ fmt.Printf("Change status: %s\n", pr.State)
+
+ // Update a change
+ updateOptions := PullRequestOptions{
+ Title: "Updated title",
+ Description: "Updated description",
+ }
+
+ updatedPR, err := git.UpdatePullRequest(ctx, pr.ID, updateOptions)
+ if err != nil {
+ log.Printf("Failed to update change: %v", err)
+ return
+ }
+
+ fmt.Printf("Updated change: %s\n", updatedPR.Title)
+
+ // Submit a change (merge)
+ mergeOptions := MergePullRequestOptions{
+ CommitTitle: "Submit change",
+ CommitMsg: "This submits the change to master",
+ }
+
+ err = git.MergePullRequest(ctx, pr.ID, mergeOptions)
+ if err != nil {
+ log.Printf("Failed to submit change: %v", err)
+ return
+ }
+
+ fmt.Println("Change submitted successfully")
+}
+
+// Example of using both providers in the same application
+func ExampleMultiProviderUsage() {
+ ctx := context.Background()
+
+ // Determine which provider to use based on configuration
+ useGitHub := true // This could come from config
+
+ var git GitInterface
+
+ if useGitHub {
+ // Use GitHub
+ githubConfig := GitHubConfig{
+ Token: "github-token",
+ BaseURL: "https://api.github.com",
+ }
+ githubProvider := NewGitHubPullRequestProvider("owner", "repo", githubConfig)
+ git = NewGitWithPullRequests("/path/to/repo", GitConfig{}, githubProvider)
+ } else {
+ // Use Gerrit
+ gerritConfig := GerritConfig{
+ Username: "gerrit-username",
+ Password: "gerrit-password",
+ BaseURL: "https://gerrit.example.com",
+ }
+ gerritProvider := NewGerritPullRequestProvider("project", gerritConfig)
+ git = NewGitWithPullRequests("/path/to/repo", GitConfig{}, gerritProvider)
+ }
+
+ // Use the same interface regardless of provider
+ prOptions := PullRequestOptions{
+ Title: "Cross-platform PR",
+ Description: "This works with both GitHub and Gerrit",
+ BaseBranch: "main",
+ HeadBranch: "feature/cross-platform",
+ }
+
+ pr, err := git.CreatePullRequest(ctx, prOptions)
+ if err != nil {
+ log.Printf("Failed to create pull request: %v", err)
+ return
+ }
+
+ fmt.Printf("Created pull request: %s\n", pr.Title)
+}