mirror of
https://github.com/prdlk/gh-commit.git
synced 2026-09-17 07:26:26 +00:00
Replaces the Python/Crush/DuckDB implementation with a CGO-free Go binary: cobra CLI, modernc.org/sqlite storage, huh/lipgloss UI, and speed-tuned raw /api/generate calls (think:false, keep_alive, capped num_predict) against a local qwen3.5:2b. Ships as a gh extension via cli/gh-extension-precompile.
143 lines
3.7 KiB
Go
143 lines
3.7 KiB
Go
// Package gitx wraps the git CLI. Read helpers swallow errors and return ""
|
|
// (mirroring the Python tool); mutating helpers pass git output through and
|
|
// return errors.
|
|
package gitx
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Git runs git with args and returns trimmed stdout, or "" on any failure.
|
|
func Git(args ...string) string {
|
|
out, err := exec.Command("git", args...).Output()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
// passthrough runs git with args, streaming output to the terminal.
|
|
func passthrough(args ...string) error {
|
|
cmd := exec.Command("git", args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
// IsRepo reports whether the working directory is inside a git work tree.
|
|
func IsRepo() bool {
|
|
return exec.Command("git", "rev-parse", "--is-inside-work-tree").Run() == nil
|
|
}
|
|
|
|
// RepoRoot returns the repository top-level path, or "" outside a repo.
|
|
func RepoRoot() string { return Git("rev-parse", "--show-toplevel") }
|
|
|
|
// Lines splits git output into non-empty lines.
|
|
func Lines(out string) []string {
|
|
var lines []string
|
|
for _, l := range strings.Split(out, "\n") {
|
|
if l != "" {
|
|
lines = append(lines, l)
|
|
}
|
|
}
|
|
return lines
|
|
}
|
|
|
|
// ChangedFiles returns staged + unstaged + untracked files, deduplicated and sorted.
|
|
func ChangedFiles() []string {
|
|
seen := map[string]struct{}{}
|
|
for _, out := range []string{
|
|
Git("diff", "--cached", "--name-only"),
|
|
Git("diff", "--name-only"),
|
|
Git("ls-files", "--others", "--exclude-standard"),
|
|
} {
|
|
for _, f := range Lines(out) {
|
|
seen[f] = struct{}{}
|
|
}
|
|
}
|
|
files := make([]string, 0, len(seen))
|
|
for f := range seen {
|
|
files = append(files, f)
|
|
}
|
|
sort.Strings(files)
|
|
return files
|
|
}
|
|
|
|
// FilesInScope returns the files whose path starts with any of the scope prefixes.
|
|
func FilesInScope(files, prefixes []string) []string {
|
|
var matched []string
|
|
for _, f := range files {
|
|
for _, p := range prefixes {
|
|
if strings.HasPrefix(f, p) {
|
|
matched = append(matched, f)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return matched
|
|
}
|
|
|
|
// StageFiles stages files with git add.
|
|
func StageFiles(files []string) error {
|
|
if len(files) == 0 {
|
|
return nil
|
|
}
|
|
return passthrough(append([]string{"add"}, files...)...)
|
|
}
|
|
|
|
// ResetStaging unstages everything (git reset HEAD -- .), ignoring errors.
|
|
func ResetStaging() {
|
|
_ = exec.Command("git", "reset", "HEAD", "--", ".").Run()
|
|
}
|
|
|
|
// Commit commits the staging area with message.
|
|
func Commit(message string) error { return passthrough("commit", "-m", message) }
|
|
|
|
// UnpushedCommits returns oneline entries for commits not on any remote.
|
|
func UnpushedCommits() []string {
|
|
return Lines(Git("log", "--branches", "--not", "--remotes", "--oneline"))
|
|
}
|
|
|
|
// CurrentBranch returns the checked-out branch name.
|
|
func CurrentBranch() string { return Git("branch", "--show-current") }
|
|
|
|
// Push pushes branch to origin.
|
|
func Push(branch string) error { return passthrough("push", "origin", branch) }
|
|
|
|
// Filetree returns a sorted, deduplicated repo-relative file listing
|
|
// (tracked + non-ignored untracked), one path per line.
|
|
func Filetree() string {
|
|
seen := map[string]struct{}{}
|
|
for _, out := range []string{
|
|
Git("ls-files"),
|
|
Git("ls-files", "--others", "--exclude-standard"),
|
|
} {
|
|
for _, f := range Lines(out) {
|
|
seen[f] = struct{}{}
|
|
}
|
|
}
|
|
files := make([]string, 0, len(seen))
|
|
for f := range seen {
|
|
files = append(files, f)
|
|
}
|
|
sort.Strings(files)
|
|
return strings.Join(files, "\n")
|
|
}
|
|
|
|
// GitignoreHash returns the SHA-256 hex digest of the repo's .gitignore,
|
|
// or "" when there is none.
|
|
func GitignoreHash(repoRoot string) string {
|
|
data, err := os.ReadFile(filepath.Join(repoRoot, ".gitignore"))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
sum := sha256.Sum256(data)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|