blob: 7e7ba4442f57296be8bf4df4d51ba74fef36c839 [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...)
iomodo97555d02025-07-27 15:07:14 +0400380 g.logger.Info("Adding files", slog.String("paths", strings.Join(paths, ", ")))
iomodo5a7e4e72025-07-25 13:21:41 +0400381 cmd.Dir = g.repoPath
382 return g.runCommand(cmd, "add")
383}
384
385// AddAll stages all changes
386func (g *Git) AddAll(ctx context.Context) error {
387 cmd := exec.CommandContext(ctx, "git", "add", "-A")
388 cmd.Dir = g.repoPath
389 return g.runCommand(cmd, "add")
390}
391
392// Commit creates a new commit
393func (g *Git) Commit(ctx context.Context, message string, options CommitOptions) error {
394 args := []string{"commit", "-m", message}
395
396 if options.Author != nil {
397 args = append(args, fmt.Sprintf("--author=%s <%s>", options.Author.Name, options.Author.Email))
398 }
399
400 if options.Sign {
401 args = append(args, "-S")
402 }
403
404 if options.AllowEmpty {
405 args = append(args, "--allow-empty")
406 }
407
408 cmd := exec.CommandContext(ctx, "git", args...)
409 cmd.Dir = g.repoPath
410 return g.runCommand(cmd, "commit")
411}
412
413// ListRemotes returns all remotes
414func (g *Git) ListRemotes(ctx context.Context) ([]Remote, error) {
415 cmd := exec.CommandContext(ctx, "git", "remote", "-v")
416 cmd.Dir = g.repoPath
417 output, err := g.runCommandWithOutput(cmd, "remote")
418 if err != nil {
419 return nil, err
420 }
421
422 return g.parseRemotes(output)
423}
424
425// AddRemote adds a new remote
426func (g *Git) AddRemote(ctx context.Context, name, url string) error {
427 cmd := exec.CommandContext(ctx, "git", "remote", "add", name, url)
428 cmd.Dir = g.repoPath
429 return g.runCommand(cmd, "remote")
430}
431
432// RemoveRemote removes a remote
433func (g *Git) RemoveRemote(ctx context.Context, name string) error {
434 cmd := exec.CommandContext(ctx, "git", "remote", "remove", name)
435 cmd.Dir = g.repoPath
436 return g.runCommand(cmd, "remote")
437}
438
439// Fetch fetches from a remote
440func (g *Git) Fetch(ctx context.Context, remote string, options FetchOptions) error {
441 args := []string{"fetch"}
442
443 if options.All {
444 args = append(args, "--all")
445 } else if remote != "" {
446 args = append(args, remote)
447 }
448
449 if options.Tags {
450 args = append(args, "--tags")
451 }
452
453 if options.Depth > 0 {
454 args = append(args, fmt.Sprintf("--depth=%d", options.Depth))
455 }
456
457 if options.Prune {
458 args = append(args, "--prune")
459 }
460
461 cmd := exec.CommandContext(ctx, "git", args...)
462 cmd.Dir = g.repoPath
463 return g.runCommand(cmd, "fetch")
464}
465
466// Pull pulls from a remote
467func (g *Git) Pull(ctx context.Context, remote, branch string) error {
468 args := []string{"pull"}
469 if remote != "" {
470 args = append(args, remote)
471 if branch != "" {
472 args = append(args, branch)
473 }
474 }
475
476 cmd := exec.CommandContext(ctx, "git", args...)
477 cmd.Dir = g.repoPath
478 return g.runCommand(cmd, "pull")
479}
480
481// Push pushes to a remote
482func (g *Git) Push(ctx context.Context, remote, branch string, options PushOptions) error {
483 args := []string{"push"}
484
485 if options.Force {
486 args = append(args, "--force")
487 }
488
489 if options.Tags {
490 args = append(args, "--tags")
491 }
492
493 if options.SetUpstream {
494 args = append(args, "--set-upstream")
495 }
496
497 if remote != "" {
498 args = append(args, remote)
499 if branch != "" {
500 args = append(args, branch)
501 }
502 }
503
504 cmd := exec.CommandContext(ctx, "git", args...)
505 cmd.Dir = g.repoPath
506 return g.runCommand(cmd, "push")
507}
508
509// Merge merges a branch into current branch
510func (g *Git) Merge(ctx context.Context, ref string, options MergeOptions) error {
511 args := []string{"merge"}
512
513 if options.NoFF {
514 args = append(args, "--no-ff")
515 }
516
517 if options.Message != "" {
518 args = append(args, "-m", options.Message)
519 }
520
521 if options.Strategy != "" {
522 args = append(args, "-s", options.Strategy)
523 }
524
525 args = append(args, ref)
526
527 cmd := exec.CommandContext(ctx, "git", args...)
528 cmd.Dir = g.repoPath
529 return g.runCommand(cmd, "merge")
530}
531
532// MergeBase finds the common ancestor of two commits
533func (g *Git) MergeBase(ctx context.Context, ref1, ref2 string) (string, error) {
534 cmd := exec.CommandContext(ctx, "git", "merge-base", ref1, ref2)
535 cmd.Dir = g.repoPath
536 output, err := g.runCommandWithOutput(cmd, "merge-base")
537 if err != nil {
538 return "", err
539 }
540
541 return strings.TrimSpace(output), nil
542}
543
544// GetConfig gets a Git configuration value
545func (g *Git) GetConfig(ctx context.Context, key string) (string, error) {
546 cmd := exec.CommandContext(ctx, "git", "config", "--get", key)
547 cmd.Dir = g.repoPath
548 output, err := g.runCommandWithOutput(cmd, "config")
549 if err != nil {
550 return "", err
551 }
552
553 return strings.TrimSpace(output), nil
554}
555
556// SetConfig sets a Git configuration value
557func (g *Git) SetConfig(ctx context.Context, key, value string) error {
558 cmd := exec.CommandContext(ctx, "git", "config", key, value)
559 cmd.Dir = g.repoPath
560 return g.runCommand(cmd, "config")
561}
562
563// GetUserConfig gets user configuration
564func (g *Git) GetUserConfig(ctx context.Context) (*UserConfig, error) {
565 name, err := g.GetConfig(ctx, "user.name")
566 if err != nil {
567 return nil, err
568 }
569
570 email, err := g.GetConfig(ctx, "user.email")
571 if err != nil {
572 return nil, err
573 }
574
575 return &UserConfig{
576 Name: name,
577 Email: email,
578 }, nil
579}
580
581// SetUserConfig sets user configuration
582func (g *Git) SetUserConfig(ctx context.Context, config UserConfig) error {
583 if err := g.SetConfig(ctx, "user.name", config.Name); err != nil {
584 return err
585 }
586
587 return g.SetConfig(ctx, "user.email", config.Email)
588}
589
iomodo1d173602025-07-26 15:35:57 +0400590// Pull Request operations
591
592// CreatePullRequest creates a new pull request
593func (g *Git) CreatePullRequest(ctx context.Context, options PullRequestOptions) (*PullRequest, error) {
594 if g.prProvider == nil {
595 return nil, &GitError{Command: "CreatePullRequest", Output: "no pull request provider configured"}
596 }
597 return g.prProvider.CreatePullRequest(ctx, options)
598}
599
600// GetPullRequest retrieves a pull request by ID
601func (g *Git) GetPullRequest(ctx context.Context, id string) (*PullRequest, error) {
602 if g.prProvider == nil {
603 return nil, &GitError{Command: "GetPullRequest", Output: "no pull request provider configured"}
604 }
605 return g.prProvider.GetPullRequest(ctx, id)
606}
607
608// ListPullRequests lists pull requests
609func (g *Git) ListPullRequests(ctx context.Context, options ListPullRequestOptions) ([]PullRequest, error) {
610 if g.prProvider == nil {
611 return nil, &GitError{Command: "ListPullRequests", Output: "no pull request provider configured"}
612 }
613 return g.prProvider.ListPullRequests(ctx, options)
614}
615
616// UpdatePullRequest updates a pull request
617func (g *Git) UpdatePullRequest(ctx context.Context, id string, options PullRequestOptions) (*PullRequest, error) {
618 if g.prProvider == nil {
619 return nil, &GitError{Command: "UpdatePullRequest", Output: "no pull request provider configured"}
620 }
621 return g.prProvider.UpdatePullRequest(ctx, id, options)
622}
623
624// ClosePullRequest closes a pull request
625func (g *Git) ClosePullRequest(ctx context.Context, id string) error {
626 if g.prProvider == nil {
627 return &GitError{Command: "ClosePullRequest", Output: "no pull request provider configured"}
628 }
629 return g.prProvider.ClosePullRequest(ctx, id)
630}
631
632// MergePullRequest merges a pull request
633func (g *Git) MergePullRequest(ctx context.Context, id string, options MergePullRequestOptions) error {
634 if g.prProvider == nil {
635 return &GitError{Command: "MergePullRequest", Output: "no pull request provider configured"}
636 }
637 return g.prProvider.MergePullRequest(ctx, id, options)
638}
639
iomodo5a7e4e72025-07-25 13:21:41 +0400640// Helper methods
641
642func (g *Git) runCommand(cmd *exec.Cmd, command string) error {
643 output, err := cmd.CombinedOutput()
644 if err != nil {
645 return &GitError{
646 Command: command,
647 Output: string(output),
648 Err: err,
649 }
650 }
651 return nil
652}
653
654func (g *Git) runCommandWithOutput(cmd *exec.Cmd, command string) (string, error) {
655 output, err := cmd.CombinedOutput()
656 if err != nil {
657 return "", &GitError{
658 Command: command,
659 Output: string(output),
660 Err: err,
661 }
662 }
663 return string(output), nil
664}
665
666func (g *Git) isValidRepo(path string) bool {
667 gitDir := filepath.Join(path, ".git")
668 info, err := os.Stat(gitDir)
669 return err == nil && info.IsDir()
670}
671
672func (g *Git) parseStatus(output string) (*Status, error) {
673 lines := strings.Split(strings.TrimSpace(output), "\n")
674 status := &Status{
675 Staged: []FileStatus{},
676 Unstaged: []FileStatus{},
677 Untracked: []string{},
678 Conflicts: []string{},
679 }
680
681 for _, line := range lines {
682 if strings.HasPrefix(line, "## ") {
683 // Parse branch info
684 parts := strings.Fields(line[3:])
685 if len(parts) > 0 {
iomodoccbc0092025-07-25 20:29:47 +0400686 // Extract local branch name from tracking information
687 // Format can be: "main" or "main...origin/master"
688 branchInfo := parts[0]
689 if strings.Contains(branchInfo, "...") {
690 // Split on "..." and take the local branch name
691 branchParts := strings.Split(branchInfo, "...")
692 status.Branch = branchParts[0]
693 } else {
694 status.Branch = branchInfo
695 }
iomodo5a7e4e72025-07-25 13:21:41 +0400696 }
697 continue
698 }
699
700 if len(line) < 3 {
701 continue
702 }
703
704 // Parse file status
705 staged := line[0:1]
706 unstaged := line[1:2]
707 path := strings.TrimSpace(line[3:])
708
709 if staged != " " {
710 status.Staged = append(status.Staged, FileStatus{
711 Path: path,
712 Status: g.parseStatusCode(staged),
713 Staged: true,
714 })
715 }
716
717 if unstaged != " " {
718 status.Unstaged = append(status.Unstaged, FileStatus{
719 Path: path,
720 Status: g.parseStatusCode(unstaged),
721 Staged: false,
722 })
723 }
724
725 if staged == " " && unstaged == "?" {
726 status.Untracked = append(status.Untracked, path)
727 }
728 }
729
730 status.IsClean = len(status.Staged) == 0 && len(status.Unstaged) == 0 && len(status.Untracked) == 0
731 return status, nil
732}
733
734func (g *Git) parseStatusCode(code string) string {
735 switch code {
736 case "M":
737 return "modified"
738 case "A":
739 return "added"
740 case "D":
741 return "deleted"
742 case "R":
743 return "renamed"
744 case "C":
745 return "copied"
746 case "U":
747 return "unmerged"
748 default:
749 return "unknown"
750 }
751}
752
753func (g *Git) parseLog(output string) ([]Commit, error) {
754 lines := strings.Split(strings.TrimSpace(output), "\n")
755 var commits []Commit
756
757 // Each commit takes 9 lines in the format:
758 // Hash, AuthorName, AuthorEmail, AuthorTime, Subject, CommitterName, CommitterEmail, CommitterTime, Parents
759 for i := 0; i < len(lines); i += 9 {
760 if i+8 >= len(lines) {
761 break
762 }
763
764 hash := lines[i]
765 authorName := lines[i+1]
766 authorEmail := lines[i+2]
767 authorTimeStr := lines[i+3]
768 subject := lines[i+4]
769 committerName := lines[i+5]
770 committerEmail := lines[i+6]
771 committerTimeStr := lines[i+7]
772 parentsStr := lines[i+8]
773
774 // Parse timestamps
775 authorTime, _ := strconv.ParseInt(authorTimeStr, 10, 64)
776 committerTime, _ := strconv.ParseInt(committerTimeStr, 10, 64)
777
778 // Parse parents
779 var parents []string
780 if parentsStr != "" {
781 parents = strings.Fields(parentsStr)
782 }
783
784 commit := Commit{
785 Hash: hash,
786 Author: Author{
787 Name: authorName,
788 Email: authorEmail,
789 Time: time.Unix(authorTime, 0),
790 },
791 Committer: Author{
792 Name: committerName,
793 Email: committerEmail,
794 Time: time.Unix(committerTime, 0),
795 },
796 Message: subject,
797 Parents: parents,
798 Timestamp: time.Unix(authorTime, 0),
799 }
800
801 commits = append(commits, commit)
802 }
803
804 return commits, nil
805}
806
807func (g *Git) parseBranches(output string) ([]Branch, error) {
808 lines := strings.Split(strings.TrimSpace(output), "\n")
809 var branches []Branch
810
811 for _, line := range lines {
812 if strings.TrimSpace(line) == "" {
813 continue
814 }
815
816 parts := strings.Split(line, "\t")
817 if len(parts) < 3 {
818 continue
819 }
820
821 branch := Branch{
822 Name: parts[0],
823 Commit: parts[1],
824 Message: parts[2],
825 IsRemote: strings.HasPrefix(parts[0], "remotes/"),
826 }
827
828 // Check if this is the current branch
829 if !branch.IsRemote {
830 branch.IsCurrent = strings.HasPrefix(line, "*")
831 }
832
833 branches = append(branches, branch)
834 }
835
836 return branches, nil
837}
838
839func (g *Git) parseRemotes(output string) ([]Remote, error) {
840 lines := strings.Split(strings.TrimSpace(output), "\n")
841 var remotes []Remote
842 seen := make(map[string]bool)
843
844 for _, line := range lines {
845 if strings.TrimSpace(line) == "" {
846 continue
847 }
848
849 parts := strings.Fields(line)
850 if len(parts) < 3 {
851 continue
852 }
853
854 name := parts[0]
855 if seen[name] {
856 continue
857 }
858
859 remotes = append(remotes, Remote{
860 Name: name,
861 URL: parts[1],
862 })
863 seen[name] = true
864 }
865
866 return remotes, nil
867}
iomodo1d173602025-07-26 15:35:57 +0400868
869// PullRequest represents a pull request or merge request
870type PullRequest struct {
871 ID string
872 Number int
873 Title string
874 Description string
875 State string // "open", "closed", "merged"
876 Author Author
877 CreatedAt time.Time
878 UpdatedAt time.Time
879 BaseBranch string
880 HeadBranch string
881 BaseRepo string
882 HeadRepo string
883 Labels []string
884 Assignees []Author
885 Reviewers []Author
886 Commits []Commit
887 Comments []PullRequestComment
888}
889
890// PullRequestComment represents a comment on a pull request
891type PullRequestComment struct {
892 ID string
893 Author Author
894 Content string
895 CreatedAt time.Time
896 UpdatedAt time.Time
897 Path string
898 Line int
899}
900
901// PullRequestOptions defines options for creating/updating pull requests
902type PullRequestOptions struct {
903 Title string
904 Description string
905 BaseBranch string
906 HeadBranch string
907 BaseRepo string
908 HeadRepo string
909 Labels []string
910 Assignees []string
911 Reviewers []string
912 Draft bool
913}
914
915// ListPullRequestOptions defines options for listing pull requests
916type ListPullRequestOptions struct {
917 State string // "open", "closed", "all"
918 Author string
919 Assignee string
920 BaseBranch string
921 HeadBranch string
922 Labels []string
923 Limit int
924}
925
926// MergePullRequestOptions defines options for merging pull requests
927type MergePullRequestOptions struct {
928 MergeMethod string // "merge", "squash", "rebase"
929 CommitTitle string
930 CommitMsg string
931}
932
933// PullRequestProvider defines the interface for pull request operations
934type PullRequestProvider interface {
935 CreatePullRequest(ctx context.Context, options PullRequestOptions) (*PullRequest, error)
936 GetPullRequest(ctx context.Context, id string) (*PullRequest, error)
937 ListPullRequests(ctx context.Context, options ListPullRequestOptions) ([]PullRequest, error)
938 UpdatePullRequest(ctx context.Context, id string, options PullRequestOptions) (*PullRequest, error)
939 ClosePullRequest(ctx context.Context, id string) error
940 MergePullRequest(ctx context.Context, id string, options MergePullRequestOptions) error
941}