mirror of
https://github.com/prdlk/gh-commit.git
synced 2026-09-16 23:16:25 +00:00
feat: rewrite gh-commit in Go with local Ollama backend
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.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
// 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[:])
|
||||
}
|
||||
Reference in New Issue
Block a user