ikurotime / gitgud

public
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}