119 lines
3.3 KiB
Go
119 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
// gitRun executes a git command in repoPath, returning a descriptive error
|
|
// that includes the combined stdout+stderr on failure.
|
|
func gitRun(cfg *Config, args ...string) error {
|
|
env, err := gitCommandEnv(cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.Command("git", append([]string{"-C", cfg.RepoPath}, args...)...)
|
|
cmd.Env = env
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("git %s: %w\n%s",
|
|
strings.Join(args, " "), err, bytes.TrimSpace(out))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// gitOutput executes a git command and returns its stdout as a string.
|
|
func gitOutput(cfg *Config, args ...string) (string, error) {
|
|
env, err := gitCommandEnv(cfg)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
cmd := exec.Command("git", append([]string{"-C", cfg.RepoPath}, args...)...)
|
|
cmd.Env = env
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
var stderr []byte
|
|
if ee, ok := err.(*exec.ExitError); ok {
|
|
stderr = ee.Stderr
|
|
}
|
|
return "", fmt.Errorf("git %s: %w\n%s",
|
|
strings.Join(args, " "), err, bytes.TrimSpace(stderr))
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
// gitCommandEnv builds the environment for git subprocesses.
|
|
// If GIT_AUTH_TOKEN is provided, non-interactive HTTPS auth is enabled via askpass.
|
|
func gitCommandEnv(cfg *Config) ([]string, error) {
|
|
env := os.Environ()
|
|
if cfg.GitAuthToken == "" {
|
|
return env, nil
|
|
}
|
|
|
|
exePath, err := os.Executable()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve executable path for GIT_ASKPASS: %w", err)
|
|
}
|
|
|
|
env = append(env,
|
|
"GIT_TERMINAL_PROMPT=0",
|
|
"GIT_ASKPASS_REQUIRE=force",
|
|
"GIT_ASKPASS="+exePath,
|
|
"TDW_GIT_ASKPASS=1",
|
|
"GIT_AUTH_USERNAME="+cfg.GitAuthUsername,
|
|
"GIT_AUTH_TOKEN="+cfg.GitAuthToken,
|
|
)
|
|
return env, nil
|
|
}
|
|
|
|
// GitPull fetches from the remote and rebases the local branch.
|
|
// Using --autostash ensures any uncommitted changes are preserved across the rebase
|
|
// (should not normally happen but guards against manual edits).
|
|
func GitPull(cfg *Config) error {
|
|
if err := gitRun(cfg, "fetch", "--prune", cfg.RepoRemote); err != nil {
|
|
return fmt.Errorf("git fetch: %w", err)
|
|
}
|
|
if err := gitRun(cfg,
|
|
"pull", "--rebase", "--autostash",
|
|
cfg.RepoRemote, cfg.RepoBranch,
|
|
); err != nil {
|
|
return fmt.Errorf("git pull --rebase: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GitStatusChanged reports true when there are changes in the machine-managed
|
|
// dynamic directory. Unrelated repository changes are intentionally ignored.
|
|
func GitStatusChanged(cfg *Config) (bool, error) {
|
|
out, err := gitOutput(cfg, "status", "--porcelain", "--", cfg.DynamicDir)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return strings.TrimSpace(out) != "", nil
|
|
}
|
|
|
|
// GitCommitAndPush stages the dynamic directory, commits with the given message,
|
|
// and pushes to the configured remote branch.
|
|
// Author identity is passed via git -c flags to avoid requiring global git config.
|
|
func GitCommitAndPush(cfg *Config, message string) error {
|
|
if err := gitRun(cfg, "add", cfg.DynamicDir); err != nil {
|
|
return fmt.Errorf("git add: %w", err)
|
|
}
|
|
if err := gitRun(cfg,
|
|
"-c", "user.name="+cfg.AuthorName,
|
|
"-c", "user.email="+cfg.AuthorEmail,
|
|
"commit", "-m", message,
|
|
); err != nil {
|
|
return fmt.Errorf("git commit: %w", err)
|
|
}
|
|
if err := gitRun(cfg, "push", cfg.RepoRemote, cfg.RepoBranch); err != nil {
|
|
return fmt.Errorf("git push: %w", err)
|
|
}
|
|
return nil
|
|
}
|