123 lines
4.0 KiB
Go
123 lines
4.0 KiB
Go
// Package gitutil wraps the git operations the tool relies on: enumerating tracked
|
|
// files, checking working-tree cleanliness before a write, deriving the
|
|
// first-commit year for the git year policy, and making the optional atomic commit.
|
|
//
|
|
// WHY shell out to git rather than use a pure-Go git library: git ls-files is the
|
|
// authoritative honorer of .gitignore (including nested and global ignores), and
|
|
// matching its semantics in Go would be a reimplementation we would have to keep
|
|
// in sync. The non-git fallback path lives in internal/enumerate.
|
|
package gitutil
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// run executes git in path's working tree and returns trimmed stdout. WHY -C: it
|
|
// runs git as if started in path without changing the caller's working directory,
|
|
// so concurrent callers do not race on a shared cwd.
|
|
//
|
|
// run is a package var so tests can substitute git's output to exercise the
|
|
// defensive parse branches in FirstCommitYear that real git output cannot trigger.
|
|
// The default implementation's behavior is unchanged.
|
|
var run = func(path string, args ...string) (string, error) {
|
|
full := append([]string{"-C", path}, args...)
|
|
cmd := exec.Command("git", full...)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return "", fmt.Errorf("git %s: %w", strings.Join(args, " "), err)
|
|
}
|
|
return strings.TrimSpace(string(out)), nil
|
|
}
|
|
|
|
// IsRepo reports whether path is inside a git working tree.
|
|
func IsRepo(path string) bool {
|
|
out, err := run(path, "rev-parse", "--is-inside-work-tree")
|
|
return err == nil && out == "true"
|
|
}
|
|
|
|
// ListFiles returns repo-relative paths of all git-tracked files under path, as
|
|
// produced by `git ls-files` (which inherits .gitignore correctly).
|
|
func ListFiles(path string) ([]string, error) {
|
|
out, err := run(path, "ls-files")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if out == "" {
|
|
return nil, nil
|
|
}
|
|
return strings.Split(out, "\n"), nil
|
|
}
|
|
|
|
// IsClean reports whether the working tree at path has no uncommitted changes.
|
|
// Apply refuses to write to a dirty tree unless --allow-dirty is set.
|
|
func IsClean(path string) (bool, error) {
|
|
out, err := run(path, "status", "--porcelain")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return out == "", nil
|
|
}
|
|
|
|
// FirstCommitYear returns the four-digit year of the repository's first (root)
|
|
// commit at path, for the git year policy (first-commit-year-to-current range).
|
|
func FirstCommitYear(path string) (int, error) {
|
|
// --max-parents=0 selects root commit(s); %ad with a year-only date format
|
|
// yields just the year. A repo can have multiple roots; take the earliest.
|
|
out, err := run(path, "log", "--max-parents=0", "--date=format:%Y", "--format=%ad")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if out == "" {
|
|
return 0, fmt.Errorf("gitutil: no commits in %q", path)
|
|
}
|
|
earliest := 0
|
|
for _, line := range strings.Split(out, "\n") {
|
|
y, perr := strconv.Atoi(strings.TrimSpace(line))
|
|
if perr != nil {
|
|
continue
|
|
}
|
|
if earliest == 0 || y < earliest {
|
|
earliest = y
|
|
}
|
|
}
|
|
if earliest == 0 {
|
|
return 0, fmt.Errorf("gitutil: could not parse first-commit year in %q", path)
|
|
}
|
|
return earliest, nil
|
|
}
|
|
|
|
// Commit stages all changes under path and creates a single commit with message.
|
|
// Used by apply --commit to make one atomic conventional commit per repo. A commit
|
|
// with nothing staged is reported as an error by git and surfaced here.
|
|
func Commit(path, message string) error {
|
|
if _, err := run(path, "add", "-A"); err != nil {
|
|
return err
|
|
}
|
|
if _, err := run(path, "commit", "-m", message); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CommitPaths stages and commits only the named repo-relative paths. Pre-existing
|
|
// staged or unstaged changes outside this pathset are left alone.
|
|
func CommitPaths(path, message string, paths []string) error {
|
|
if len(paths) == 0 {
|
|
return fmt.Errorf("gitutil: no paths to commit")
|
|
}
|
|
|
|
addArgs := append([]string{"add", "--"}, paths...)
|
|
if _, err := run(path, addArgs...); err != nil {
|
|
return err
|
|
}
|
|
|
|
commitArgs := append([]string{"commit", "-m", message, "--only", "--"}, paths...)
|
|
if _, err := run(path, commitArgs...); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|