🏡 index : quine-global-labs/nugitea.git

// Package gitcmd wraps invocation of the git subprocess, mirroring the
// pattern Gitea itself uses in modules/git/gitcmd: a small builder around
// os/exec with a fixed baseline environment and context-based cancellation.
package gitcmd

import (
	"context"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"
)

// Command builds a single git invocation.
type Command struct {
	args   []string
	dir    string
	env    []string
	stdin  io.Reader
	stdout io.Writer
	stderr io.Writer
}

// New starts a git command with the given arguments (e.g. "upload-pack",
// "--stateless-rpc", ".").
func New(args ...string) *Command {
	return &Command{args: args}
}

// WithDir sets the working directory the command runs in (the bare repo path).
func (c *Command) WithDir(dir string) *Command {
	c.dir = dir
	return c
}

// WithEnv sets the environment passed to the subprocess. The baseline env
// (see baseEnv) is always appended on top of this in Run.
func (c *Command) WithEnv(env []string) *Command {
	c.env = env
	return c
}

// WithStdio wires the subprocess's stdin/stdout/stderr.
func (c *Command) WithStdio(stdin io.Reader, stdout, stderr io.Writer) *Command {
	c.stdin = stdin
	c.stdout = stdout
	c.stderr = stderr
	return c
}

// baseEnv returns the environment variables Gitea always sets for git
// subprocesses, to keep behavior deterministic and free of surprise
// user/system config interference.
func baseEnv() []string {
	home, _ := os.UserHomeDir()
	return []string{
		"HOME=" + home,
		"GIT_CONFIG_NOSYSTEM=1",
		"GIT_TERMINAL_PROMPT=0",
		"LC_ALL=C",
	}
}

// Run executes the git command, waiting for it to complete or ctx to be
// cancelled.
func (c *Command) Run(ctx context.Context) error {
	cmd := exec.CommandContext(ctx, "git", c.args...)
	cmd.Dir = c.dir
	cmd.Stdin = c.stdin
	cmd.Stdout = c.stdout
	cmd.Stderr = c.stderr

	env := os.Environ()
	if c.env != nil {
		env = c.env
	}
	cmd.Env = append(env, baseEnv()...)

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("git %v: %w", c.args, err)
	}
	return nil
}

// RunStdString runs the command and returns its combined stdout as a string,
// with stderr included in the error on failure.
func RunStdString(ctx context.Context, dir string, args ...string) (string, error) {
	var stdout, stderr strings.Builder
	err := New(args...).WithDir(dir).WithStdio(nil, &stdout, &stderr).Run(ctx)
	if err != nil {
		return "", fmt.Errorf("%w: %s", err, stderr.String())
	}
	return stdout.String(), nil
}