| package git |
| |
| import ( |
| "context" |
| "fmt" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strconv" |
| "strings" |
| "time" |
| ) |
| |
| // 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 |
| } |
| |
| // 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 |
| config GitConfig |
| prProvider PullRequestProvider |
| } |
| |
| // GitConfig holds configuration for Git operations |
| type GitConfig struct { |
| Timeout time.Duration |
| Env map[string]string |
| PullRequestProvider PullRequestProvider |
| } |
| |
| // NewGit creates a new Git instance |
| func NewGit(repoPath string, config GitConfig) GitInterface { |
| if config.Timeout == 0 { |
| config.Timeout = 30 * time.Second |
| } |
| |
| return &Git{ |
| repoPath: repoPath, |
| config: config, |
| prProvider: config.PullRequestProvider, |
| } |
| } |
| |
| // DefaultGit creates a Git instance with default configuration |
| func DefaultGit(repoPath string) GitInterface { |
| return NewGit(repoPath, GitConfig{ |
| Timeout: 30 * time.Second, |
| Env: make(map[string]string), |
| }) |
| } |
| |
| // 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) |
| |
| // 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...) |
| 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) |
| } |
| |
| // 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 |
| } |
| |
| // 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 |
| } |