blob: 9aa1ae2d0eb2c800fe5434983c6b285da36378fe [file] [log] [blame]
iomodo5a7e4e72025-07-25 13:21:41 +04001package git
2
3import (
4 "context"
5 "fmt"
iomodo0c203b12025-07-26 19:44:57 +04006 "log/slog"
iomodo5a7e4e72025-07-25 13:21:41 +04007 "os"
8 "os/exec"
9 "path/filepath"
10 "strconv"
11 "strings"
12 "time"
13)
14
15// GitInterface defines the contract for Git operations
16type GitInterface interface {
17 // Repository operations
18 Init(ctx context.Context, path string) error
19 Clone(ctx context.Context, url, path string) error
20 IsRepository(ctx context.Context, path string) (bool, error)
21
22 // Status and information
23 Status(ctx context.Context) (*Status, error)
24 Log(ctx context.Context, options LogOptions) ([]Commit, error)
25 Show(ctx context.Context, ref string) (*Commit, error)
26
27 // Branch operations
28 ListBranches(ctx context.Context) ([]Branch, error)
29 CreateBranch(ctx context.Context, name string, startPoint string) error
30 Checkout(ctx context.Context, ref string) error
31 DeleteBranch(ctx context.Context, name string, force bool) error
32 GetCurrentBranch(ctx context.Context) (string, error)
33
34 // Commit operations
35 Add(ctx context.Context, paths []string) error
36 AddAll(ctx context.Context) error
37 Commit(ctx context.Context, message string, options CommitOptions) error
38
39 // Remote operations
40 ListRemotes(ctx context.Context) ([]Remote, error)
41 AddRemote(ctx context.Context, name, url string) error
42 RemoveRemote(ctx context.Context, name string) error
43 Fetch(ctx context.Context, remote string, options FetchOptions) error
44 Pull(ctx context.Context, remote, branch string) error
45 Push(ctx context.Context, remote, branch string, options PushOptions) error
46
47 // Merge operations
48 Merge(ctx context.Context, ref string, options MergeOptions) error
49 MergeBase(ctx context.Context, ref1, ref2 string) (string, error)
50
51 // Configuration
52 GetConfig(ctx context.Context, key string) (string, error)
53 SetConfig(ctx context.Context, key, value string) error
54 GetUserConfig(ctx context.Context) (*UserConfig, error)
55 SetUserConfig(ctx context.Context, config UserConfig) error
iomodo1d173602025-07-26 15:35:57 +040056
57 // Pull Request operations
58 CreatePullRequest(ctx context.Context, options PullRequestOptions) (*PullRequest, error)
59 GetPullRequest(ctx context.Context, id string) (*PullRequest, error)
60 ListPullRequests(ctx context.Context, options ListPullRequestOptions) ([]PullRequest, error)
61 UpdatePullRequest(ctx context.Context, id string, options PullRequestOptions) (*PullRequest, error)
62 ClosePullRequest(ctx context.Context, id string) error
63 MergePullRequest(ctx context.Context, id string, options MergePullRequestOptions) error
iomodo5a7e4e72025-07-25 13:21:41 +040064}
65
66// Status represents the current state of the repository
67type Status struct {
68 Branch string
69 IsClean bool
70 Staged []FileStatus
71 Unstaged []FileStatus
72 Untracked []string
73 Conflicts []string
74}
75
76// FileStatus represents the status of a file
77type FileStatus struct {
78 Path string
79 Status string // "modified", "added", "deleted", "renamed", etc.
80 Staged bool
81}
82
83// Commit represents a Git commit
84type Commit struct {
85 Hash string
86 Author Author
87 Committer Author
88 Message string
89 Parents []string
90 Timestamp time.Time
91 Files []CommitFile
92}
93
94// Author represents a Git author or committer
95type Author struct {
96 Name string
97 Email string
98 Time time.Time
99}
100
101// CommitFile represents a file in a commit
102type CommitFile struct {
103 Path string
104 Status string // "added", "modified", "deleted", "renamed"
105 Additions int
106 Deletions int
107}
108
109// Branch represents a Git branch
110type Branch struct {
111 Name string
112 IsCurrent bool
113 IsRemote bool
114 Commit string
115 Message string
116}
117
118// Remote represents a Git remote
119type Remote struct {
120 Name string
121 URL string
122}
123
124// UserConfig represents Git user configuration
125type UserConfig struct {
126 Name string
127 Email string
128}
129
130// LogOptions defines options for log operations
131type LogOptions struct {
132 MaxCount int
133 Since time.Time
134 Until time.Time
135 Author string
136 Path string
137 Oneline bool
138}
139
140// CommitOptions defines options for commit operations
141type CommitOptions struct {
142 Author *Author
143 Committer *Author
144 Sign bool
145 AllowEmpty bool
146}
147
148// FetchOptions defines options for fetch operations
149type FetchOptions struct {
150 All bool
151 Tags bool
152 Depth int
153 Prune bool
154}
155
156// PushOptions defines options for push operations
157type PushOptions struct {
158 Force bool
159 Tags bool
160 SetUpstream bool
161}
162
163// MergeOptions defines options for merge operations
164type MergeOptions struct {
165 NoFF bool
166 Message string
167 Strategy string
168}
169
170// GitError represents a Git-specific error
171type GitError struct {
172 Command string
173 Output string
174 Err error
175}
176
177func (e *GitError) Error() string {
178 if e.Err != nil {
179 return fmt.Sprintf("git %s failed: %v\nOutput: %s", e.Command, e.Err, e.Output)
180 }
181 return fmt.Sprintf("git %s failed\nOutput: %s", e.Command, e.Output)
182}
183
184func (e *GitError) Unwrap() error {
185 return e.Err
186}
187
188// Git implementation using os/exec to call git commands
189type Git struct {
iomodo1d173602025-07-26 15:35:57 +0400190 repoPath string
191 config GitConfig
192 prProvider PullRequestProvider
iomodo0c203b12025-07-26 19:44:57 +0400193 logger *slog.Logger
iomodo5a7e4e72025-07-25 13:21:41 +0400194}
195
196// GitConfig holds configuration for Git operations
197type GitConfig struct {
iomodo1d173602025-07-26 15:35:57 +0400198 Timeout time.Duration
199 Env map[string]string
200 PullRequestProvider PullRequestProvider
iomodo5a7e4e72025-07-25 13:21:41 +0400201}
202
203// NewGit creates a new Git instance
iomodo0c203b12025-07-26 19:44:57 +0400204func NewGit(repoPath string, config GitConfig, logger *slog.Logger) GitInterface {
iomodo5a7e4e72025-07-25 13:21:41 +0400205 if config.Timeout == 0 {
206 config.Timeout = 30 * time.Second
207 }
208
209 return &Git{
iomodo1d173602025-07-26 15:35:57 +0400210 repoPath: repoPath,
211 config: config,
212 prProvider: config.PullRequestProvider,
iomodo0c203b12025-07-26 19:44:57 +0400213 logger: logger,
iomodo5a7e4e72025-07-25 13:21:41 +0400214 }
215}
216
217// DefaultGit creates a Git instance with default configuration
218func DefaultGit(repoPath string) GitInterface {
219 return NewGit(repoPath, GitConfig{
220 Timeout: 30 * time.Second,
iomodo0c203b12025-07-26 19:44:57 +0400221 }, slog.Default())
iomodo5a7e4e72025-07-25 13:21:41 +0400222}
223
iomodo1d173602025-07-26 15:35:57 +0400224// NewGitWithPullRequests creates a Git instance with pull request capabilities
iomodo0c203b12025-07-26 19:44:57 +0400225func NewGitWithPullRequests(repoPath string, config GitConfig, prProvider PullRequestProvider, logger *slog.Logger) GitInterface {
iomodo1d173602025-07-26 15:35:57 +0400226 config.PullRequestProvider = prProvider
iomodo0c203b12025-07-26 19:44:57 +0400227 return NewGit(repoPath, config, logger)
iomodo1d173602025-07-26 15:35:57 +0400228}
229
iomodo5a7e4e72025-07-25 13:21:41 +0400230// Ensure Git implements GitInterface
231var _ GitInterface = (*Git)(nil)
232
233// Init initializes a new Git repository
234func (g *Git) Init(ctx context.Context, path string) error {
235 cmd := exec.CommandContext(ctx, "git", "init")
236 cmd.Dir = path
237 return g.runCommand(cmd, "init")
238}
239
240// Clone clones a repository from URL to path
241func (g *Git) Clone(ctx context.Context, url, path string) error {
242 cmd := exec.CommandContext(ctx, "git", "clone", url, path)
243 return g.runCommand(cmd, "clone")
244}
245
246// IsRepository checks if a path is a valid Git repository
247func (g *Git) IsRepository(ctx context.Context, path string) (bool, error) {
248 return g.isValidRepo(path), nil
249}
250
251// Status returns the current status of the repository
252func (g *Git) Status(ctx context.Context) (*Status, error) {
253 cmd := exec.CommandContext(ctx, "git", "status", "--porcelain", "--branch")
254 cmd.Dir = g.repoPath
255 output, err := g.runCommandWithOutput(cmd, "status")
256 if err != nil {
257 return nil, err
258 }
259
260 return g.parseStatus(output)
261}
262
263// Log returns commit history
264func (g *Git) Log(ctx context.Context, options LogOptions) ([]Commit, error) {
265 args := []string{"log", "--format=%H%n%an%n%ae%n%at%n%s%n%cn%n%ce%n%ct%n%P"}
266
267 if options.MaxCount > 0 {
268 args = append(args, fmt.Sprintf("-%d", options.MaxCount))
269 }
270
271 if !options.Since.IsZero() {
272 args = append(args, fmt.Sprintf("--since=%s", options.Since.Format(time.RFC3339)))
273 }
274
275 if !options.Until.IsZero() {
276 args = append(args, fmt.Sprintf("--until=%s", options.Until.Format(time.RFC3339)))
277 }
278
279 if options.Author != "" {
280 args = append(args, fmt.Sprintf("--author=%s", options.Author))
281 }
282
283 if options.Path != "" {
284 args = append(args, "--", options.Path)
285 }
286
287 if options.Oneline {
288 args = append(args, "--oneline")
289 }
290
291 cmd := exec.CommandContext(ctx, "git", args...)
292 cmd.Dir = g.repoPath
293 output, err := g.runCommandWithOutput(cmd, "log")
294 if err != nil {
295 return nil, err
296 }
297
298 return g.parseLog(output)
299}
300
301// Show shows commit details
302func (g *Git) Show(ctx context.Context, ref string) (*Commit, error) {
303 cmd := exec.CommandContext(ctx, "git", "show", "--format=json", ref)
304 cmd.Dir = g.repoPath
305 output, err := g.runCommandWithOutput(cmd, "show")
306 if err != nil {
307 return nil, err
308 }
309
310 commits, err := g.parseLog(output)
311 if err != nil || len(commits) == 0 {
312 return nil, &GitError{Command: "show", Output: "failed to parse commit"}
313 }
314
315 return &commits[0], nil
316}
317
318// ListBranches returns all branches
319func (g *Git) ListBranches(ctx context.Context) ([]Branch, error) {
320 cmd := exec.CommandContext(ctx, "git", "branch", "-a", "--format=%(refname:short)%09%(objectname)%09%(contents:subject)")
321 cmd.Dir = g.repoPath
322 output, err := g.runCommandWithOutput(cmd, "branch")
323 if err != nil {
324 return nil, err
325 }
326
327 return g.parseBranches(output)
328}
329
330// CreateBranch creates a new branch
331func (g *Git) CreateBranch(ctx context.Context, name string, startPoint string) error {
332 args := []string{"branch", name}
333 if startPoint != "" {
334 args = append(args, startPoint)
335 }
336
337 cmd := exec.CommandContext(ctx, "git", args...)
338 cmd.Dir = g.repoPath
339 return g.runCommand(cmd, "branch")
340}
341
342// Checkout switches to a branch or commit
343func (g *Git) Checkout(ctx context.Context, ref string) error {
344 cmd := exec.CommandContext(ctx, "git", "checkout", ref)
345 cmd.Dir = g.repoPath
346 return g.runCommand(cmd, "checkout")
347}
348
349// DeleteBranch deletes a branch
350func (g *Git) DeleteBranch(ctx context.Context, name string, force bool) error {
351 args := []string{"branch"}
352 if force {
353 args = append(args, "-D")
354 } else {
355 args = append(args, "-d")
356 }
357 args = append(args, name)
358
359 cmd := exec.CommandContext(ctx, "git", args...)
360 cmd.Dir = g.repoPath
361 return g.runCommand(cmd, "branch")
362}
363
364// GetCurrentBranch returns the current branch name
365func (g *Git) GetCurrentBranch(ctx context.Context) (string, error) {
366 cmd := exec.CommandContext(ctx, "git", "rev-parse", "--abbrev-ref", "HEAD")
367 cmd.Dir = g.repoPath
368 output, err := g.runCommandWithOutput(cmd, "rev-parse")
369 if err != nil {
370 return "", err
371 }
372
373 return strings.TrimSpace(output), nil
374}
375
376// Add stages files for commit
377func (g *Git) Add(ctx context.Context, paths []string) error {
378 args := append([]string{"add"}, paths...)
379 cmd := exec.CommandContext(ctx, "git", args...)
380 cmd.Dir = g.repoPath
381 return g.runCommand(cmd, "add")
382}
383
384// AddAll stages all changes
385func (g *Git) AddAll(ctx context.Context) error {
386 cmd := exec.CommandContext(ctx, "git", "add", "-A")
387 cmd.Dir = g.repoPath
388 return g.runCommand(cmd, "add")
389}
390
391// Commit creates a new commit
392func (g *Git) Commit(ctx context.Context, message string, options CommitOptions) error {
393 args := []string{"commit", "-m", message}
394
395 if options.Author != nil {
396 args = append(args, fmt.Sprintf("--author=%s <%s>", options.Author.Name, options.Author.Email))
397 }
398
399 if options.Sign {
400 args = append(args, "-S")
401 }
402
403 if options.AllowEmpty {
404 args = append(args, "--allow-empty")
405 }
406
407 cmd := exec.CommandContext(ctx, "git", args...)
408 cmd.Dir = g.repoPath
409 return g.runCommand(cmd, "commit")
410}
411
412// ListRemotes returns all remotes
413func (g *Git) ListRemotes(ctx context.Context) ([]Remote, error) {
414 cmd := exec.CommandContext(ctx, "git", "remote", "-v")
415 cmd.Dir = g.repoPath
416 output, err := g.runCommandWithOutput(cmd, "remote")
417 if err != nil {
418 return nil, err
419 }
420
421 return g.parseRemotes(output)
422}
423
424// AddRemote adds a new remote
425func (g *Git) AddRemote(ctx context.Context, name, url string) error {
426 cmd := exec.CommandContext(ctx, "git", "remote", "add", name, url)
427 cmd.Dir = g.repoPath
428 return g.runCommand(cmd, "remote")
429}
430
431// RemoveRemote removes a remote
432func (g *Git) RemoveRemote(ctx context.Context, name string) error {
433 cmd := exec.CommandContext(ctx, "git", "remote", "remove", name)
434 cmd.Dir = g.repoPath
435 return g.runCommand(cmd, "remote")
436}
437
438// Fetch fetches from a remote
439func (g *Git) Fetch(ctx context.Context, remote string, options FetchOptions) error {
440 args := []string{"fetch"}
441
442 if options.All {
443 args = append(args, "--all")
444 } else if remote != "" {
445 args = append(args, remote)
446 }
447
448 if options.Tags {
449 args = append(args, "--tags")
450 }
451
452 if options.Depth > 0 {
453 args = append(args, fmt.Sprintf("--depth=%d", options.Depth))
454 }
455
456 if options.Prune {
457 args = append(args, "--prune")
458 }
459
460 cmd := exec.CommandContext(ctx, "git", args...)
461 cmd.Dir = g.repoPath
462 return g.runCommand(cmd, "fetch")
463}
464
465// Pull pulls from a remote
466func (g *Git) Pull(ctx context.Context, remote, branch string) error {
467 args := []string{"pull"}
468 if remote != "" {
469 args = append(args, remote)
470 if branch != "" {
471 args = append(args, branch)
472 }
473 }
474
475 cmd := exec.CommandContext(ctx, "git", args...)
476 cmd.Dir = g.repoPath
477 return g.runCommand(cmd, "pull")
478}
479
480// Push pushes to a remote
481func (g *Git) Push(ctx context.Context, remote, branch string, options PushOptions) error {
482 args := []string{"push"}
483
484 if options.Force {
485 args = append(args, "--force")
486 }
487
488 if options.Tags {
489 args = append(args, "--tags")
490 }
491
492 if options.SetUpstream {
493 args = append(args, "--set-upstream")
494 }
495
496 if remote != "" {
497 args = append(args, remote)
498 if branch != "" {
499 args = append(args, branch)
500 }
501 }
502
503 cmd := exec.CommandContext(ctx, "git", args...)
504 cmd.Dir = g.repoPath
505 return g.runCommand(cmd, "push")
506}
507
508// Merge merges a branch into current branch
509func (g *Git) Merge(ctx context.Context, ref string, options MergeOptions) error {
510 args := []string{"merge"}
511
512 if options.NoFF {
513 args = append(args, "--no-ff")
514 }
515
516 if options.Message != "" {
517 args = append(args, "-m", options.Message)
518 }
519
520 if options.Strategy != "" {
521 args = append(args, "-s", options.Strategy)
522 }
523
524 args = append(args, ref)
525
526 cmd := exec.CommandContext(ctx, "git", args...)
527 cmd.Dir = g.repoPath
528 return g.runCommand(cmd, "merge")
529}
530
531// MergeBase finds the common ancestor of two commits
532func (g *Git) MergeBase(ctx context.Context, ref1, ref2 string) (string, error) {
533 cmd := exec.CommandContext(ctx, "git", "merge-base", ref1, ref2)
534 cmd.Dir = g.repoPath
535 output, err := g.runCommandWithOutput(cmd, "merge-base")
536 if err != nil {
537 return "", err
538 }
539
540 return strings.TrimSpace(output), nil
541}
542
543// GetConfig gets a Git configuration value
544func (g *Git) GetConfig(ctx context.Context, key string) (string, error) {
545 cmd := exec.CommandContext(ctx, "git", "config", "--get", key)
546 cmd.Dir = g.repoPath
547 output, err := g.runCommandWithOutput(cmd, "config")
548 if err != nil {
549 return "", err
550 }
551
552 return strings.TrimSpace(output), nil
553}
554
555// SetConfig sets a Git configuration value
556func (g *Git) SetConfig(ctx context.Context, key, value string) error {
557 cmd := exec.CommandContext(ctx, "git", "config", key, value)
558 cmd.Dir = g.repoPath
559 return g.runCommand(cmd, "config")
560}
561
562// GetUserConfig gets user configuration
563func (g *Git) GetUserConfig(ctx context.Context) (*UserConfig, error) {
564 name, err := g.GetConfig(ctx, "user.name")
565 if err != nil {
566 return nil, err
567 }
568
569 email, err := g.GetConfig(ctx, "user.email")
570 if err != nil {
571 return nil, err
572 }
573
574 return &UserConfig{
575 Name: name,
576 Email: email,
577 }, nil
578}
579
580// SetUserConfig sets user configuration
581func (g *Git) SetUserConfig(ctx context.Context, config UserConfig) error {
582 if err := g.SetConfig(ctx, "user.name", config.Name); err != nil {
583 return err
584 }
585
586 return g.SetConfig(ctx, "user.email", config.Email)
587}
588
iomodo1d173602025-07-26 15:35:57 +0400589// Pull Request operations
590
591// CreatePullRequest creates a new pull request
592func (g *Git) CreatePullRequest(ctx context.Context, options PullRequestOptions) (*PullRequest, error) {
593 if g.prProvider == nil {
594 return nil, &GitError{Command: "CreatePullRequest", Output: "no pull request provider configured"}
595 }
596 return g.prProvider.CreatePullRequest(ctx, options)
597}
598
599// GetPullRequest retrieves a pull request by ID
600func (g *Git) GetPullRequest(ctx context.Context, id string) (*PullRequest, error) {
601 if g.prProvider == nil {
602 return nil, &GitError{Command: "GetPullRequest", Output: "no pull request provider configured"}
603 }
604 return g.prProvider.GetPullRequest(ctx, id)
605}
606
607// ListPullRequests lists pull requests
608func (g *Git) ListPullRequests(ctx context.Context, options ListPullRequestOptions) ([]PullRequest, error) {
609 if g.prProvider == nil {
610 return nil, &GitError{Command: "ListPullRequests", Output: "no pull request provider configured"}
611 }
612 return g.prProvider.ListPullRequests(ctx, options)
613}
614
615// UpdatePullRequest updates a pull request
616func (g *Git) UpdatePullRequest(ctx context.Context, id string, options PullRequestOptions) (*PullRequest, error) {
617 if g.prProvider == nil {
618 return nil, &GitError{Command: "UpdatePullRequest", Output: "no pull request provider configured"}
619 }
620 return g.prProvider.UpdatePullRequest(ctx, id, options)
621}
622
623// ClosePullRequest closes a pull request
624func (g *Git) ClosePullRequest(ctx context.Context, id string) error {
625 if g.prProvider == nil {
626 return &GitError{Command: "ClosePullRequest", Output: "no pull request provider configured"}
627 }
628 return g.prProvider.ClosePullRequest(ctx, id)
629}
630
631// MergePullRequest merges a pull request
632func (g *Git) MergePullRequest(ctx context.Context, id string, options MergePullRequestOptions) error {
633 if g.prProvider == nil {
634 return &GitError{Command: "MergePullRequest", Output: "no pull request provider configured"}
635 }
636 return g.prProvider.MergePullRequest(ctx, id, options)
637}
638
iomodo5a7e4e72025-07-25 13:21:41 +0400639// Helper methods
640
641func (g *Git) runCommand(cmd *exec.Cmd, command string) error {
642 output, err := cmd.CombinedOutput()
643 if err != nil {
644 return &GitError{
645 Command: command,
646 Output: string(output),
647 Err: err,
648 }
649 }
650 return nil
651}
652
653func (g *Git) runCommandWithOutput(cmd *exec.Cmd, command string) (string, error) {
654 output, err := cmd.CombinedOutput()
655 if err != nil {
656 return "", &GitError{
657 Command: command,
658 Output: string(output),
659 Err: err,
660 }
661 }
662 return string(output), nil
663}
664
665func (g *Git) isValidRepo(path string) bool {
666 gitDir := filepath.Join(path, ".git")
667 info, err := os.Stat(gitDir)
668 return err == nil && info.IsDir()
669}
670
671func (g *Git) parseStatus(output string) (*Status, error) {
672 lines := strings.Split(strings.TrimSpace(output), "\n")
673 status := &Status{
674 Staged: []FileStatus{},
675 Unstaged: []FileStatus{},
676 Untracked: []string{},
677 Conflicts: []string{},
678 }
679
680 for _, line := range lines {
681 if strings.HasPrefix(line, "## ") {
682 // Parse branch info
683 parts := strings.Fields(line[3:])
684 if len(parts) > 0 {
iomodoccbc0092025-07-25 20:29:47 +0400685 // Extract local branch name from tracking information
686 // Format can be: "main" or "main...origin/master"
687 branchInfo := parts[0]
688 if strings.Contains(branchInfo, "...") {
689 // Split on "..." and take the local branch name
690 branchParts := strings.Split(branchInfo, "...")
691 status.Branch = branchParts[0]
692 } else {
693 status.Branch = branchInfo
694 }
iomodo5a7e4e72025-07-25 13:21:41 +0400695 }
696 continue
697 }
698
699 if len(line) < 3 {
700 continue
701 }
702
703 // Parse file status
704 staged := line[0:1]
705 unstaged := line[1:2]
706 path := strings.TrimSpace(line[3:])
707
708 if staged != " " {
709 status.Staged = append(status.Staged, FileStatus{
710 Path: path,
711 Status: g.parseStatusCode(staged),
712 Staged: true,
713 })
714 }
715
716 if unstaged != " " {
717 status.Unstaged = append(status.Unstaged, FileStatus{
718 Path: path,
719 Status: g.parseStatusCode(unstaged),
720 Staged: false,
721 })
722 }
723
724 if staged == " " && unstaged == "?" {
725 status.Untracked = append(status.Untracked, path)
726 }
727 }
728
729 status.IsClean = len(status.Staged) == 0 && len(status.Unstaged) == 0 && len(status.Untracked) == 0
730 return status, nil
731}
732
733func (g *Git) parseStatusCode(code string) string {
734 switch code {
735 case "M":
736 return "modified"
737 case "A":
738 return "added"
739 case "D":
740 return "deleted"
741 case "R":
742 return "renamed"
743 case "C":
744 return "copied"
745 case "U":
746 return "unmerged"
747 default:
748 return "unknown"
749 }
750}
751
752func (g *Git) parseLog(output string) ([]Commit, error) {
753 lines := strings.Split(strings.TrimSpace(output), "\n")
754 var commits []Commit
755
756 // Each commit takes 9 lines in the format:
757 // Hash, AuthorName, AuthorEmail, AuthorTime, Subject, CommitterName, CommitterEmail, CommitterTime, Parents
758 for i := 0; i < len(lines); i += 9 {
759 if i+8 >= len(lines) {
760 break
761 }
762
763 hash := lines[i]
764 authorName := lines[i+1]
765 authorEmail := lines[i+2]
766 authorTimeStr := lines[i+3]
767 subject := lines[i+4]
768 committerName := lines[i+5]
769 committerEmail := lines[i+6]
770 committerTimeStr := lines[i+7]
771 parentsStr := lines[i+8]
772
773 // Parse timestamps
774 authorTime, _ := strconv.ParseInt(authorTimeStr, 10, 64)
775 committerTime, _ := strconv.ParseInt(committerTimeStr, 10, 64)
776
777 // Parse parents
778 var parents []string
779 if parentsStr != "" {
780 parents = strings.Fields(parentsStr)
781 }
782
783 commit := Commit{
784 Hash: hash,
785 Author: Author{
786 Name: authorName,
787 Email: authorEmail,
788 Time: time.Unix(authorTime, 0),
789 },
790 Committer: Author{
791 Name: committerName,
792 Email: committerEmail,
793 Time: time.Unix(committerTime, 0),
794 },
795 Message: subject,
796 Parents: parents,
797 Timestamp: time.Unix(authorTime, 0),
798 }
799
800 commits = append(commits, commit)
801 }
802
803 return commits, nil
804}
805
806func (g *Git) parseBranches(output string) ([]Branch, error) {
807 lines := strings.Split(strings.TrimSpace(output), "\n")
808 var branches []Branch
809
810 for _, line := range lines {
811 if strings.TrimSpace(line) == "" {
812 continue
813 }
814
815 parts := strings.Split(line, "\t")
816 if len(parts) < 3 {
817 continue
818 }
819
820 branch := Branch{
821 Name: parts[0],
822 Commit: parts[1],
823 Message: parts[2],
824 IsRemote: strings.HasPrefix(parts[0], "remotes/"),
825 }
826
827 // Check if this is the current branch
828 if !branch.IsRemote {
829 branch.IsCurrent = strings.HasPrefix(line, "*")
830 }
831
832 branches = append(branches, branch)
833 }
834
835 return branches, nil
836}
837
838func (g *Git) parseRemotes(output string) ([]Remote, error) {
839 lines := strings.Split(strings.TrimSpace(output), "\n")
840 var remotes []Remote
841 seen := make(map[string]bool)
842
843 for _, line := range lines {
844 if strings.TrimSpace(line) == "" {
845 continue
846 }
847
848 parts := strings.Fields(line)
849 if len(parts) < 3 {
850 continue
851 }
852
853 name := parts[0]
854 if seen[name] {
855 continue
856 }
857
858 remotes = append(remotes, Remote{
859 Name: name,
860 URL: parts[1],
861 })
862 seen[name] = true
863 }
864
865 return remotes, nil
866}
iomodo1d173602025-07-26 15:35:57 +0400867
868// PullRequest represents a pull request or merge request
869type PullRequest struct {
870 ID string
871 Number int
872 Title string
873 Description string
874 State string // "open", "closed", "merged"
875 Author Author
876 CreatedAt time.Time
877 UpdatedAt time.Time
878 BaseBranch string
879 HeadBranch string
880 BaseRepo string
881 HeadRepo string
882 Labels []string
883 Assignees []Author
884 Reviewers []Author
885 Commits []Commit
886 Comments []PullRequestComment
887}
888
889// PullRequestComment represents a comment on a pull request
890type PullRequestComment struct {
891 ID string
892 Author Author
893 Content string
894 CreatedAt time.Time
895 UpdatedAt time.Time
896 Path string
897 Line int
898}
899
900// PullRequestOptions defines options for creating/updating pull requests
901type PullRequestOptions struct {
902 Title string
903 Description string
904 BaseBranch string
905 HeadBranch string
906 BaseRepo string
907 HeadRepo string
908 Labels []string
909 Assignees []string
910 Reviewers []string
911 Draft bool
912}
913
914// ListPullRequestOptions defines options for listing pull requests
915type ListPullRequestOptions struct {
916 State string // "open", "closed", "all"
917 Author string
918 Assignee string
919 BaseBranch string
920 HeadBranch string
921 Labels []string
922 Limit int
923}
924
925// MergePullRequestOptions defines options for merging pull requests
926type MergePullRequestOptions struct {
927 MergeMethod string // "merge", "squash", "rebase"
928 CommitTitle string
929 CommitMsg string
930}
931
932// PullRequestProvider defines the interface for pull request operations
933type PullRequestProvider interface {
934 CreatePullRequest(ctx context.Context, options PullRequestOptions) (*PullRequest, error)
935 GetPullRequest(ctx context.Context, id string) (*PullRequest, error)
936 ListPullRequests(ctx context.Context, options ListPullRequestOptions) ([]PullRequest, error)
937 UpdatePullRequest(ctx context.Context, id string, options PullRequestOptions) (*PullRequest, error)
938 ClosePullRequest(ctx context.Context, id string) error
939 MergePullRequest(ctx context.Context, id string, options MergePullRequestOptions) error
940}