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