- Update control plane phase roadmap - Move repo-registry milestone to archive - Implement git engine command execution - Add postgres repository storage layer - Update contracts documentation - Add test cases for storage and git engine
254 lines
6.1 KiB
Go
254 lines
6.1 KiB
Go
package gitengine
|
|
|
|
import (
|
|
"errors"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
var ErrInvalidGitInput = errors.New("invalid git input")
|
|
|
|
type CommandRunner interface {
|
|
Run(workdir string, args ...string) (string, error)
|
|
}
|
|
|
|
type CLI struct{}
|
|
|
|
func (CLI) Run(workdir string, args ...string) (string, error) {
|
|
if len(args) == 0 {
|
|
return "", ErrInvalidGitInput
|
|
}
|
|
cmd := exec.Command("git", args...)
|
|
if strings.TrimSpace(workdir) != "" {
|
|
cmd.Dir = workdir
|
|
}
|
|
out, err := cmd.CombinedOutput()
|
|
return string(out), err
|
|
}
|
|
|
|
type StatusResult struct {
|
|
Clean bool
|
|
Output string
|
|
}
|
|
|
|
type CloneOptions struct {
|
|
RemoteURL string
|
|
WorktreePath string
|
|
Branch string
|
|
}
|
|
|
|
type CommitOptions struct {
|
|
Message string
|
|
All bool
|
|
}
|
|
|
|
type DiffOptions struct {
|
|
Staged bool
|
|
Pathspecs []string
|
|
}
|
|
|
|
type ChangedFile struct {
|
|
Path string
|
|
ChangeType string
|
|
}
|
|
|
|
func Clone(runner CommandRunner, opts CloneOptions) error {
|
|
remoteURL := strings.TrimSpace(opts.RemoteURL)
|
|
worktreePath := strings.TrimSpace(opts.WorktreePath)
|
|
if remoteURL == "" || worktreePath == "" {
|
|
return ErrInvalidGitInput
|
|
}
|
|
args := []string{"clone"}
|
|
if branch := strings.TrimSpace(opts.Branch); branch != "" {
|
|
args = append(args, "--branch", branch)
|
|
}
|
|
args = append(args, remoteURL, worktreePath)
|
|
_, err := runner.Run("", args...)
|
|
return err
|
|
}
|
|
|
|
func Fetch(runner CommandRunner, workdir, remote string) error {
|
|
if strings.TrimSpace(workdir) == "" {
|
|
return ErrInvalidGitInput
|
|
}
|
|
remote = strings.TrimSpace(remote)
|
|
if remote == "" {
|
|
remote = "origin"
|
|
}
|
|
_, err := runner.Run(workdir, "fetch", "--prune", remote)
|
|
return err
|
|
}
|
|
|
|
type CheckoutOptions struct {
|
|
Branch string
|
|
StartPoint string
|
|
Reset bool
|
|
}
|
|
|
|
func Checkout(runner CommandRunner, workdir, branch string) error {
|
|
return CheckoutBranch(runner, workdir, CheckoutOptions{Branch: branch})
|
|
}
|
|
|
|
func CheckoutBranch(runner CommandRunner, workdir string, opts CheckoutOptions) error {
|
|
workdir = strings.TrimSpace(workdir)
|
|
branch := strings.TrimSpace(opts.Branch)
|
|
if workdir == "" || branch == "" {
|
|
return ErrInvalidGitInput
|
|
}
|
|
startPoint := strings.TrimSpace(opts.StartPoint)
|
|
if startPoint == "" {
|
|
_, err := runner.Run(workdir, "checkout", branch)
|
|
return err
|
|
}
|
|
flag := "-b"
|
|
if opts.Reset {
|
|
flag = "-B"
|
|
}
|
|
_, err := runner.Run(workdir, "checkout", flag, branch, startPoint)
|
|
return err
|
|
}
|
|
|
|
func Status(runner CommandRunner, workdir string) (StatusResult, error) {
|
|
if strings.TrimSpace(workdir) == "" {
|
|
return StatusResult{}, ErrInvalidGitInput
|
|
}
|
|
output, err := runner.Run(workdir, "status", "--porcelain")
|
|
if err != nil {
|
|
return StatusResult{}, err
|
|
}
|
|
return StatusResult{
|
|
Clean: strings.TrimSpace(output) == "",
|
|
Output: output,
|
|
}, nil
|
|
}
|
|
|
|
func Diff(runner CommandRunner, workdir string, opts DiffOptions) (string, error) {
|
|
if strings.TrimSpace(workdir) == "" {
|
|
return "", ErrInvalidGitInput
|
|
}
|
|
args := []string{"diff"}
|
|
if opts.Staged {
|
|
args = append(args, "--cached")
|
|
}
|
|
pathspecs := make([]string, 0, len(opts.Pathspecs))
|
|
for _, pathspec := range opts.Pathspecs {
|
|
pathspec = strings.TrimSpace(pathspec)
|
|
if pathspec != "" {
|
|
pathspecs = append(pathspecs, pathspec)
|
|
}
|
|
}
|
|
if len(pathspecs) > 0 {
|
|
args = append(args, "--")
|
|
args = append(args, pathspecs...)
|
|
}
|
|
return runner.Run(workdir, args...)
|
|
}
|
|
|
|
func Commit(runner CommandRunner, workdir string, opts CommitOptions) error {
|
|
if strings.TrimSpace(workdir) == "" || strings.TrimSpace(opts.Message) == "" {
|
|
return ErrInvalidGitInput
|
|
}
|
|
if opts.All {
|
|
if _, err := runner.Run(workdir, "add", "-A"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err := runner.Run(workdir, "commit", "-m", opts.Message)
|
|
return err
|
|
}
|
|
|
|
func Push(runner CommandRunner, workdir, remote, branch string) error {
|
|
if strings.TrimSpace(workdir) == "" || strings.TrimSpace(branch) == "" {
|
|
return ErrInvalidGitInput
|
|
}
|
|
remote = strings.TrimSpace(remote)
|
|
if remote == "" {
|
|
remote = "origin"
|
|
}
|
|
_, err := runner.Run(workdir, "push", remote, branch)
|
|
return err
|
|
}
|
|
|
|
func HeadRevision(runner CommandRunner, workdir string) (string, error) {
|
|
if strings.TrimSpace(workdir) == "" {
|
|
return "", ErrInvalidGitInput
|
|
}
|
|
output, err := runner.Run(workdir, "rev-parse", "HEAD")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
revision := strings.TrimSpace(output)
|
|
if revision == "" {
|
|
return "", ErrInvalidGitInput
|
|
}
|
|
return revision, nil
|
|
}
|
|
|
|
func ChangedFiles(runner CommandRunner, workdir, before, after string) ([]string, error) {
|
|
before = strings.TrimSpace(before)
|
|
after = strings.TrimSpace(after)
|
|
if strings.TrimSpace(workdir) == "" || before == "" || after == "" {
|
|
return nil, ErrInvalidGitInput
|
|
}
|
|
output, err := runner.Run(workdir, "diff", "--name-only", before+".."+after)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
lines := strings.Split(output, "\n")
|
|
files := make([]string, 0, len(lines))
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line != "" {
|
|
files = append(files, line)
|
|
}
|
|
}
|
|
return files, nil
|
|
}
|
|
|
|
func ChangedFilesWithStatus(runner CommandRunner, workdir, before, after string) ([]ChangedFile, error) {
|
|
before = strings.TrimSpace(before)
|
|
after = strings.TrimSpace(after)
|
|
if strings.TrimSpace(workdir) == "" || before == "" || after == "" {
|
|
return nil, ErrInvalidGitInput
|
|
}
|
|
output, err := runner.Run(workdir, "diff", "--name-status", before+".."+after)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
lines := strings.Split(output, "\n")
|
|
files := make([]ChangedFile, 0, len(lines))
|
|
for _, line := range lines {
|
|
if file, ok := parseNameStatus(line); ok {
|
|
files = append(files, file)
|
|
}
|
|
}
|
|
return files, nil
|
|
}
|
|
|
|
func parseNameStatus(line string) (ChangedFile, bool) {
|
|
fields := strings.Fields(strings.TrimSpace(line))
|
|
if len(fields) < 2 {
|
|
return ChangedFile{}, false
|
|
}
|
|
changeType := changeTypeFromGitStatus(fields[0])
|
|
path := fields[len(fields)-1]
|
|
if path == "" {
|
|
return ChangedFile{}, false
|
|
}
|
|
return ChangedFile{Path: path, ChangeType: changeType}, true
|
|
}
|
|
|
|
func changeTypeFromGitStatus(status string) string {
|
|
switch {
|
|
case strings.HasPrefix(status, "A"):
|
|
return "added"
|
|
case strings.HasPrefix(status, "D"):
|
|
return "deleted"
|
|
case strings.HasPrefix(status, "R"):
|
|
return "renamed"
|
|
case strings.HasPrefix(status, "C"):
|
|
return "copied"
|
|
default:
|
|
return "modified"
|
|
}
|
|
}
|