blob: 4cd7508886e69e1533a8bbf55298d041e69ab76b [file] [log] [blame]
package git
import (
"context"
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/iomodo/staff/config"
)
// GitInterface defines the contract for Git operations
type GitInterface interface {
// Repository operations
Init(ctx context.Context, path string) error
Clone(ctx context.Context, url, path string) error
IsRepository(ctx context.Context, path string) (bool, error)
// Status and information
Status(ctx context.Context) (*Status, error)
Log(ctx context.Context, options LogOptions) ([]Commit, error)
Show(ctx context.Context, ref string) (*Commit, error)
// Branch operations
ListBranches(ctx context.Context) ([]Branch, error)
CreateBranch(ctx context.Context, name string, startPoint string) error
Checkout(ctx context.Context, ref string) error
DeleteBranch(ctx context.Context, name string, force bool) error
GetCurrentBranch(ctx context.Context) (string, error)
// Commit operations
Add(ctx context.Context, paths []string) error
AddAll(ctx context.Context) error
Commit(ctx context.Context, message string, options CommitOptions) error
// Remote operations
ListRemotes(ctx context.Context) ([]Remote, error)
AddRemote(ctx context.Context, name, url string) error
RemoveRemote(ctx context.Context, name string) error
Fetch(ctx context.Context, remote string, options FetchOptions) error
Pull(ctx context.Context, remote, branch string) error
Push(ctx context.Context, remote, branch string, options PushOptions) error
// Merge operations
Merge(ctx context.Context, ref string, options MergeOptions) error
MergeBase(ctx context.Context, ref1, ref2 string) (string, error)
// Configuration
GetConfig(ctx context.Context, key string) (string, error)
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
// Clone Manager Operations
GetAgentClonePath(agentName string) (string, error)
RefreshAgentClone(agentName string) error
CleanupAgentClone(agentName string) error
CleanupAllClones() error
}
// Status represents the current state of the repository
type Status struct {
Branch string
IsClean bool
Staged []FileStatus
Unstaged []FileStatus
Untracked []string
Conflicts []string
}
// FileStatus represents the status of a file
type FileStatus struct {
Path string
Status string // "modified", "added", "deleted", "renamed", etc.
Staged bool
}
// Commit represents a Git commit
type Commit struct {
Hash string
Author Author
Committer Author
Message string
Parents []string
Timestamp time.Time
Files []CommitFile
}
// Author represents a Git author or committer
type Author struct {
Name string
Email string
Time time.Time
}
// CommitFile represents a file in a commit
type CommitFile struct {
Path string
Status string // "added", "modified", "deleted", "renamed"
Additions int
Deletions int
}
// Branch represents a Git branch
type Branch struct {
Name string
IsCurrent bool
IsRemote bool
Commit string
Message string
}
// Remote represents a Git remote
type Remote struct {
Name string
URL string
}
// UserConfig represents Git user configuration
type UserConfig struct {
Name string
Email string
}
// LogOptions defines options for log operations
type LogOptions struct {
MaxCount int
Since time.Time
Until time.Time
Author string
Path string
Oneline bool
}
// CommitOptions defines options for commit operations
type CommitOptions struct {
Author *Author
Committer *Author
Sign bool
AllowEmpty bool
}
// FetchOptions defines options for fetch operations
type FetchOptions struct {
All bool
Tags bool
Depth int
Prune bool
}
// PushOptions defines options for push operations
type PushOptions struct {
Force bool
Tags bool
SetUpstream bool
}
// MergeOptions defines options for merge operations
type MergeOptions struct {
NoFF bool
Message string
Strategy string
}
// GitError represents a Git-specific error
type GitError struct {
Command string
Output string
Err error
}
func (e *GitError) Error() string {
if e.Err != nil {
return fmt.Sprintf("git %s failed: %v\nOutput: %s", e.Command, e.Err, e.Output)
}
return fmt.Sprintf("git %s failed\nOutput: %s", e.Command, e.Output)
}
func (e *GitError) Unwrap() error {
return e.Err
}
// Git implementation using os/exec to call git commands
type Git struct {
repoPath string
prProvider PullRequestProvider
cloneManager *CloneManager
logger *slog.Logger
}
// NewGit creates a new Git instance
func New(cfg *config.Config, logger *slog.Logger) GitInterface {
var prProvider PullRequestProvider
var repoURL string
switch cfg.GetPrimaryGitProvider() {
case "github":
githubConfig := GitHubConfig{
Token: cfg.GitHub.Token,
Logger: logger,
}
prProvider = NewGitHubPullRequestProvider(cfg.GitHub.Owner, cfg.GitHub.Repo, githubConfig)
repoURL = fmt.Sprintf("https://github.com/%s/%s.git", cfg.GitHub.Owner, cfg.GitHub.Repo)
logger.Info("Using GitHub as pull request provider",
slog.String("owner", cfg.GitHub.Owner),
slog.String("repo", cfg.GitHub.Repo))
case "gerrit":
gerritConfig := GerritConfig{
Username: cfg.Gerrit.Username,
Password: cfg.Gerrit.Password,
BaseURL: cfg.Gerrit.BaseURL,
Logger: logger,
}
prProvider = NewGerritPullRequestProvider(cfg.Gerrit.Project, gerritConfig)
repoURL = fmt.Sprintf("%s/%s", cfg.Gerrit.BaseURL, cfg.Gerrit.Project)
logger.Info("Using Gerrit as pull request provider",
slog.String("base_url", cfg.Gerrit.BaseURL),
slog.String("project", cfg.Gerrit.Project))
default:
panic("no valid Git provider configured")
}
cloneManager := NewCloneManager(repoURL, cfg.Git.WorkspacePath)
return &Git{
repoPath: cfg.Git.RepoPath,
prProvider: prProvider,
cloneManager: cloneManager,
logger: logger,
}
}
// Ensure Git implements GitInterface
var _ GitInterface = (*Git)(nil)
// Init initializes a new Git repository
func (g *Git) Init(ctx context.Context, path string) error {
cmd := exec.CommandContext(ctx, "git", "init")
cmd.Dir = path
return g.runCommand(cmd, "init")
}
// Clone clones a repository from URL to path
func (g *Git) Clone(ctx context.Context, url, path string) error {
cmd := exec.CommandContext(ctx, "git", "clone", url, path)
return g.runCommand(cmd, "clone")
}
// IsRepository checks if a path is a valid Git repository
func (g *Git) IsRepository(ctx context.Context, path string) (bool, error) {
return g.isValidRepo(path), nil
}
// Status returns the current status of the repository
func (g *Git) Status(ctx context.Context) (*Status, error) {
cmd := exec.CommandContext(ctx, "git", "status", "--porcelain", "--branch")
cmd.Dir = g.repoPath
output, err := g.runCommandWithOutput(cmd, "status")
if err != nil {
return nil, err
}
return g.parseStatus(output)
}
// Log returns commit history
func (g *Git) Log(ctx context.Context, options LogOptions) ([]Commit, error) {
args := []string{"log", "--format=%H%n%an%n%ae%n%at%n%s%n%cn%n%ce%n%ct%n%P"}
if options.MaxCount > 0 {
args = append(args, fmt.Sprintf("-%d", options.MaxCount))
}
if !options.Since.IsZero() {
args = append(args, fmt.Sprintf("--since=%s", options.Since.Format(time.RFC3339)))
}
if !options.Until.IsZero() {
args = append(args, fmt.Sprintf("--until=%s", options.Until.Format(time.RFC3339)))
}
if options.Author != "" {
args = append(args, fmt.Sprintf("--author=%s", options.Author))
}
if options.Path != "" {
args = append(args, "--", options.Path)
}
if options.Oneline {
args = append(args, "--oneline")
}
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = g.repoPath
output, err := g.runCommandWithOutput(cmd, "log")
if err != nil {
return nil, err
}
return g.parseLog(output)
}
// Show shows commit details
func (g *Git) Show(ctx context.Context, ref string) (*Commit, error) {
cmd := exec.CommandContext(ctx, "git", "show", "--format=json", ref)
cmd.Dir = g.repoPath
output, err := g.runCommandWithOutput(cmd, "show")
if err != nil {
return nil, err
}
commits, err := g.parseLog(output)
if err != nil || len(commits) == 0 {
return nil, &GitError{Command: "show", Output: "failed to parse commit"}
}
return &commits[0], nil
}
// ListBranches returns all branches
func (g *Git) ListBranches(ctx context.Context) ([]Branch, error) {
cmd := exec.CommandContext(ctx, "git", "branch", "-a", "--format=%(refname:short)%09%(objectname)%09%(contents:subject)")
cmd.Dir = g.repoPath
output, err := g.runCommandWithOutput(cmd, "branch")
if err != nil {
return nil, err
}
return g.parseBranches(output)
}
// CreateBranch creates a new branch
func (g *Git) CreateBranch(ctx context.Context, name string, startPoint string) error {
args := []string{"branch", name}
if startPoint != "" {
args = append(args, startPoint)
}
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = g.repoPath
return g.runCommand(cmd, "branch")
}
// Checkout switches to a branch or commit
func (g *Git) Checkout(ctx context.Context, ref string) error {
cmd := exec.CommandContext(ctx, "git", "checkout", ref)
cmd.Dir = g.repoPath
return g.runCommand(cmd, "checkout")
}
// DeleteBranch deletes a branch
func (g *Git) DeleteBranch(ctx context.Context, name string, force bool) error {
args := []string{"branch"}
if force {
args = append(args, "-D")
} else {
args = append(args, "-d")
}
args = append(args, name)
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = g.repoPath
return g.runCommand(cmd, "branch")
}
// GetCurrentBranch returns the current branch name
func (g *Git) GetCurrentBranch(ctx context.Context) (string, error) {
cmd := exec.CommandContext(ctx, "git", "rev-parse", "--abbrev-ref", "HEAD")
cmd.Dir = g.repoPath
output, err := g.runCommandWithOutput(cmd, "rev-parse")
if err != nil {
return "", err
}
return strings.TrimSpace(output), nil
}
// Add stages files for commit
func (g *Git) Add(ctx context.Context, paths []string) error {
args := append([]string{"add"}, paths...)
cmd := exec.CommandContext(ctx, "git", args...)
g.logger.Info("Adding files", slog.String("paths", strings.Join(paths, ", ")))
cmd.Dir = g.repoPath
return g.runCommand(cmd, "add")
}
// AddAll stages all changes
func (g *Git) AddAll(ctx context.Context) error {
cmd := exec.CommandContext(ctx, "git", "add", "-A")
cmd.Dir = g.repoPath
return g.runCommand(cmd, "add")
}
// Commit creates a new commit
func (g *Git) Commit(ctx context.Context, message string, options CommitOptions) error {
args := []string{"commit", "-m", message}
if options.Author != nil {
args = append(args, fmt.Sprintf("--author=%s <%s>", options.Author.Name, options.Author.Email))
}
if options.Sign {
args = append(args, "-S")
}
if options.AllowEmpty {
args = append(args, "--allow-empty")
}
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = g.repoPath
return g.runCommand(cmd, "commit")
}
// ListRemotes returns all remotes
func (g *Git) ListRemotes(ctx context.Context) ([]Remote, error) {
cmd := exec.CommandContext(ctx, "git", "remote", "-v")
cmd.Dir = g.repoPath
output, err := g.runCommandWithOutput(cmd, "remote")
if err != nil {
return nil, err
}
return g.parseRemotes(output)
}
// AddRemote adds a new remote
func (g *Git) AddRemote(ctx context.Context, name, url string) error {
cmd := exec.CommandContext(ctx, "git", "remote", "add", name, url)
cmd.Dir = g.repoPath
return g.runCommand(cmd, "remote")
}
// RemoveRemote removes a remote
func (g *Git) RemoveRemote(ctx context.Context, name string) error {
cmd := exec.CommandContext(ctx, "git", "remote", "remove", name)
cmd.Dir = g.repoPath
return g.runCommand(cmd, "remote")
}
// Fetch fetches from a remote
func (g *Git) Fetch(ctx context.Context, remote string, options FetchOptions) error {
args := []string{"fetch"}
if options.All {
args = append(args, "--all")
} else if remote != "" {
args = append(args, remote)
}
if options.Tags {
args = append(args, "--tags")
}
if options.Depth > 0 {
args = append(args, fmt.Sprintf("--depth=%d", options.Depth))
}
if options.Prune {
args = append(args, "--prune")
}
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = g.repoPath
return g.runCommand(cmd, "fetch")
}
// Pull pulls from a remote
func (g *Git) Pull(ctx context.Context, remote, branch string) error {
args := []string{"pull"}
if remote != "" {
args = append(args, remote)
if branch != "" {
args = append(args, branch)
}
}
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = g.repoPath
return g.runCommand(cmd, "pull")
}
// Push pushes to a remote
func (g *Git) Push(ctx context.Context, remote, branch string, options PushOptions) error {
args := []string{"push"}
if options.Force {
args = append(args, "--force")
}
if options.Tags {
args = append(args, "--tags")
}
if options.SetUpstream {
args = append(args, "--set-upstream")
}
if remote != "" {
args = append(args, remote)
if branch != "" {
args = append(args, branch)
}
}
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = g.repoPath
return g.runCommand(cmd, "push")
}
// Merge merges a branch into current branch
func (g *Git) Merge(ctx context.Context, ref string, options MergeOptions) error {
args := []string{"merge"}
if options.NoFF {
args = append(args, "--no-ff")
}
if options.Message != "" {
args = append(args, "-m", options.Message)
}
if options.Strategy != "" {
args = append(args, "-s", options.Strategy)
}
args = append(args, ref)
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = g.repoPath
return g.runCommand(cmd, "merge")
}
// MergeBase finds the common ancestor of two commits
func (g *Git) MergeBase(ctx context.Context, ref1, ref2 string) (string, error) {
cmd := exec.CommandContext(ctx, "git", "merge-base", ref1, ref2)
cmd.Dir = g.repoPath
output, err := g.runCommandWithOutput(cmd, "merge-base")
if err != nil {
return "", err
}
return strings.TrimSpace(output), nil
}
// GetConfig gets a Git configuration value
func (g *Git) GetConfig(ctx context.Context, key string) (string, error) {
cmd := exec.CommandContext(ctx, "git", "config", "--get", key)
cmd.Dir = g.repoPath
output, err := g.runCommandWithOutput(cmd, "config")
if err != nil {
return "", err
}
return strings.TrimSpace(output), nil
}
// SetConfig sets a Git configuration value
func (g *Git) SetConfig(ctx context.Context, key, value string) error {
cmd := exec.CommandContext(ctx, "git", "config", key, value)
cmd.Dir = g.repoPath
return g.runCommand(cmd, "config")
}
// GetUserConfig gets user configuration
func (g *Git) GetUserConfig(ctx context.Context) (*UserConfig, error) {
name, err := g.GetConfig(ctx, "user.name")
if err != nil {
return nil, err
}
email, err := g.GetConfig(ctx, "user.email")
if err != nil {
return nil, err
}
return &UserConfig{
Name: name,
Email: email,
}, nil
}
// SetUserConfig sets user configuration
func (g *Git) SetUserConfig(ctx context.Context, config UserConfig) error {
if err := g.SetConfig(ctx, "user.name", config.Name); err != nil {
return err
}
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)
}
// Clone manage methods
func (g *Git) GetAgentClonePath(agentName string) (string, error) {
if g.cloneManager == nil {
return "", &GitError{Command: "GetAgentClonePath", Output: "no clone manager configured"}
}
return g.cloneManager.GetAgentClonePath(agentName)
}
func (g *Git) RefreshAgentClone(agentName string) error {
if g.cloneManager == nil {
return &GitError{Command: "RefreshAgentClone", Output: "no clone manager configured"}
}
return g.cloneManager.RefreshAgentClone(agentName)
}
func (g *Git) CleanupAgentClone(agentName string) error {
if g.cloneManager == nil {
return &GitError{Command: "CleanupAgentClone", Output: "no clone manager configured"}
}
return g.cloneManager.CleanupAgentClone(agentName)
}
func (g *Git) CleanupAllClones() error {
if g.cloneManager == nil {
return &GitError{Command: "CleanupAllClones", Output: "no clone manager configured"}
}
return g.cloneManager.CleanupAllClones()
}
// Helper methods
func (g *Git) runCommand(cmd *exec.Cmd, command string) error {
output, err := cmd.CombinedOutput()
if err != nil {
return &GitError{
Command: command,
Output: string(output),
Err: err,
}
}
return nil
}
func (g *Git) runCommandWithOutput(cmd *exec.Cmd, command string) (string, error) {
output, err := cmd.CombinedOutput()
if err != nil {
return "", &GitError{
Command: command,
Output: string(output),
Err: err,
}
}
return string(output), nil
}
func (g *Git) isValidRepo(path string) bool {
gitDir := filepath.Join(path, ".git")
info, err := os.Stat(gitDir)
return err == nil && info.IsDir()
}
func (g *Git) parseStatus(output string) (*Status, error) {
lines := strings.Split(strings.TrimSpace(output), "\n")
status := &Status{
Staged: []FileStatus{},
Unstaged: []FileStatus{},
Untracked: []string{},
Conflicts: []string{},
}
for _, line := range lines {
if strings.HasPrefix(line, "## ") {
// Parse branch info
parts := strings.Fields(line[3:])
if len(parts) > 0 {
// Extract local branch name from tracking information
// Format can be: "main" or "main...origin/master"
branchInfo := parts[0]
if strings.Contains(branchInfo, "...") {
// Split on "..." and take the local branch name
branchParts := strings.Split(branchInfo, "...")
status.Branch = branchParts[0]
} else {
status.Branch = branchInfo
}
}
continue
}
if len(line) < 3 {
continue
}
// Parse file status
staged := line[0:1]
unstaged := line[1:2]
path := strings.TrimSpace(line[3:])
if staged != " " {
status.Staged = append(status.Staged, FileStatus{
Path: path,
Status: g.parseStatusCode(staged),
Staged: true,
})
}
if unstaged != " " {
status.Unstaged = append(status.Unstaged, FileStatus{
Path: path,
Status: g.parseStatusCode(unstaged),
Staged: false,
})
}
if staged == " " && unstaged == "?" {
status.Untracked = append(status.Untracked, path)
}
}
status.IsClean = len(status.Staged) == 0 && len(status.Unstaged) == 0 && len(status.Untracked) == 0
return status, nil
}
func (g *Git) parseStatusCode(code string) string {
switch code {
case "M":
return "modified"
case "A":
return "added"
case "D":
return "deleted"
case "R":
return "renamed"
case "C":
return "copied"
case "U":
return "unmerged"
default:
return "unknown"
}
}
func (g *Git) parseLog(output string) ([]Commit, error) {
lines := strings.Split(strings.TrimSpace(output), "\n")
var commits []Commit
// Each commit takes 9 lines in the format:
// Hash, AuthorName, AuthorEmail, AuthorTime, Subject, CommitterName, CommitterEmail, CommitterTime, Parents
for i := 0; i < len(lines); i += 9 {
if i+8 >= len(lines) {
break
}
hash := lines[i]
authorName := lines[i+1]
authorEmail := lines[i+2]
authorTimeStr := lines[i+3]
subject := lines[i+4]
committerName := lines[i+5]
committerEmail := lines[i+6]
committerTimeStr := lines[i+7]
parentsStr := lines[i+8]
// Parse timestamps
authorTime, _ := strconv.ParseInt(authorTimeStr, 10, 64)
committerTime, _ := strconv.ParseInt(committerTimeStr, 10, 64)
// Parse parents
var parents []string
if parentsStr != "" {
parents = strings.Fields(parentsStr)
}
commit := Commit{
Hash: hash,
Author: Author{
Name: authorName,
Email: authorEmail,
Time: time.Unix(authorTime, 0),
},
Committer: Author{
Name: committerName,
Email: committerEmail,
Time: time.Unix(committerTime, 0),
},
Message: subject,
Parents: parents,
Timestamp: time.Unix(authorTime, 0),
}
commits = append(commits, commit)
}
return commits, nil
}
func (g *Git) parseBranches(output string) ([]Branch, error) {
lines := strings.Split(strings.TrimSpace(output), "\n")
var branches []Branch
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
parts := strings.Split(line, "\t")
if len(parts) < 3 {
continue
}
branch := Branch{
Name: parts[0],
Commit: parts[1],
Message: parts[2],
IsRemote: strings.HasPrefix(parts[0], "remotes/"),
}
// Check if this is the current branch
if !branch.IsRemote {
branch.IsCurrent = strings.HasPrefix(line, "*")
}
branches = append(branches, branch)
}
return branches, nil
}
func (g *Git) parseRemotes(output string) ([]Remote, error) {
lines := strings.Split(strings.TrimSpace(output), "\n")
var remotes []Remote
seen := make(map[string]bool)
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue
}
parts := strings.Fields(line)
if len(parts) < 3 {
continue
}
name := parts[0]
if seen[name] {
continue
}
remotes = append(remotes, Remote{
Name: name,
URL: parts[1],
})
seen[name] = true
}
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
URL string
}
// 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
}