main / internal/infra/git/cli_git.go
2.7 KB · Go Raw
1package git
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "os/exec"
8 "path/filepath"
9 "strings"
10
11 "gitgud/internal/domain"
12)
13
14type CLIGit struct {
15 reposDir string
16}
17
18func NewCLIGit(reposDir string) CLIGit {
19 return CLIGit{reposDir: reposDir}
20}
21
22func (g CLIGit) InitBare(ctx context.Context, owner, name, defaultBranch string) error {
23 path, err := g.repoPath(owner, name)
24 if err != nil {
25 return err
26 }
27 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
28 return err
29 }
30 cmd := exec.CommandContext(ctx, "git", "init", "--bare",
31 "--initial-branch="+defaultBranch, path)
32 if out, err := cmd.CombinedOutput(); err != nil {
33 return fmt.Errorf("git init: %v: %s", err, out)
34 }
35
36 cmd = exec.CommandContext(ctx, "git", "-C", path, "config", "http.receivepack", "true")
37 if out, err := cmd.CombinedOutput(); err != nil {
38 return fmt.Errorf("git config http.receivepack: %v: %s", err, out)
39 }
40 return nil
41}
42
43func (g CLIGit) Merge(ctx context.Context, owner, name, base, head, message, authorName, authorEmail string) error {
44 bare, err := g.repoPath(owner, name)
45 if err != nil {
46 return err
47 }
48 tmp, err := os.MkdirTemp("", "gitgud-merge-*")
49 if err != nil {
50 return err
51 }
52 defer os.RemoveAll(tmp)
53
54 run := func(args ...string) (string, error) {
55 cmd := exec.CommandContext(ctx, "git", args...)
56 cmd.Dir = tmp
57 cmd.Env = append(os.Environ(),
58 "GIT_AUTHOR_NAME="+authorName, "GIT_AUTHOR_EMAIL="+authorEmail,
59 "GIT_COMMITTER_NAME="+authorName, "GIT_COMMITTER_EMAIL="+authorEmail)
60 out, err := cmd.CombinedOutput()
61 return string(out), err
62 }
63
64 if out, err := run("clone", bare, "."); err != nil {
65 return fmt.Errorf("clone: %v: %s", err, out)
66 }
67 if out, err := run("checkout", base); err != nil {
68 return fmt.Errorf("checkout %s: %v: %s", base, err, out)
69 }
70 if _, err := run("merge", "--no-ff", "-m", message, "origin/"+head); err != nil {
71 return domain.ErrConflict
72 }
73 if out, err := run("push", "origin", base); err != nil {
74 return fmt.Errorf("push: %v: %s", err, out)
75 }
76 return nil
77}
78
79func (g CLIGit) RemovePath(ctx context.Context, owner, name string) error {
80 path, err := g.repoPath(owner, name)
81 if err != nil {
82 return err
83 }
84 return os.RemoveAll(path)
85}
86
87func (g CLIGit) repoPath(owner, name string) (string, error) {
88 return repoPath(g.reposDir, owner, name)
89}
90
91func repoPath(reposDir, owner, name string) (string, error) {
92 if err := safeSegment(owner); err != nil {
93 return "", err
94 }
95 if err := safeSegment(name); err != nil {
96 return "", err
97 }
98 return filepath.Join(reposDir, owner, name+".git"), nil
99}
100
101func safeSegment(s string) error {
102 if s == "" || s == "." || s == ".." ||
103 strings.HasPrefix(s, ".") || strings.ContainsAny(s, `/\`) {
104 return fmt.Errorf("invalid path segment %q", s)
105 }
106 return nil
107}