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:
t
2026-08-15 19:10:16 -04:00
parent 735229b150
commit 9647c854c9
31 changed files with 2731 additions and 1085 deletions
+166
View File
@@ -0,0 +1,166 @@
// Package cmd wires the cobra CLI: config resolution, database and Ollama
// client lifecycles, and one file per subcommand.
package cmd
import (
"errors"
"fmt"
"os"
"strconv"
"time"
"github.com/spf13/cobra"
"github.com/prdlk/gh-commit/internal/ai"
"github.com/prdlk/gh-commit/internal/db"
"github.com/prdlk/gh-commit/internal/gitx"
"github.com/prdlk/gh-commit/internal/ui"
)
// errAborted signals exit code 1 after the failure has already been printed.
var errAborted = errors.New("aborted")
type config struct {
host string
model string
timeout time.Duration
auto bool
push bool
noAutoRefresh bool
debug bool
}
var (
version = "dev"
cfg config
store *db.Store
client *ai.Client
flagHost string
flagModel string
flagAuto bool
flagPush bool
)
func envOr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func envBool(key string) bool { return os.Getenv(key) == "1" }
func longHelp() string {
return fmt.Sprintf(`AI-powered scoped git commits driven by a local Ollama model.
Scopes map Conventional Commit scope names to path prefixes. They are
generated from the repository file tree by the model, stored in a local
SQLite database, and auto-regenerate whenever .gitignore changes.
Legacy .github/Repo.toml or scopes.json files are auto-migrated on first run.
Database:
%s
Environment:
GH_COMMIT_OLLAMA_HOST Ollama server URL (default http://localhost:11434)
GH_COMMIT_MODEL Model tag (default qwen3.5:2b)
GH_COMMIT_TIMEOUT Per-request timeout in seconds (default 120)
GH_COMMIT_AUTO=1 Skip all confirmation prompts
GH_COMMIT_PUSH=1 Auto-push after commits
GH_COMMIT_NO_AUTO_REFRESH=1 Don't auto-regenerate scopes on .gitignore change
GH_COMMIT_DEBUG=1 Print raw model output on parse failure`, db.Path())
}
var rootCmd = &cobra.Command{
Use: "gh-commit",
Short: "AI-powered scoped git commits (Ollama)",
Long: longHelp(),
Args: cobra.NoArgs,
SilenceUsage: true,
SilenceErrors: true,
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
},
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
cfg.host = envOr("GH_COMMIT_OLLAMA_HOST", "http://localhost:11434")
cfg.model = envOr("GH_COMMIT_MODEL", "qwen3.5:2b")
if flagHost != "" {
cfg.host = flagHost
}
if flagModel != "" {
cfg.model = flagModel
}
seconds := 120
if v, err := strconv.Atoi(envOr("GH_COMMIT_TIMEOUT", "120")); err == nil && v > 0 {
seconds = v
}
cfg.timeout = time.Duration(seconds) * time.Second
cfg.auto = envBool("GH_COMMIT_AUTO") || flagAuto
cfg.push = envBool("GH_COMMIT_PUSH") || flagPush
cfg.noAutoRefresh = envBool("GH_COMMIT_NO_AUTO_REFRESH")
cfg.debug = envBool("GH_COMMIT_DEBUG")
ui.AutoConfirm = cfg.auto
switch cmd.Name() {
case "version", "help":
return nil
}
s, err := db.Open()
if err != nil {
return err
}
store = s
if s.HadLegacyDuckDB {
ui.Warnf("Found a legacy DuckDB database (gh-commit.db) — it cannot be migrated; scopes will regenerate on 'gh commit init'")
}
return nil
},
RunE: runCommit,
}
// Execute runs the CLI, printing errors in red and exiting 1 on failure.
func Execute(v string) {
version = v
rootCmd.Version = v
rootCmd.SetVersionTemplate("gh-commit {{.Version}}\n")
err := rootCmd.Execute()
if store != nil {
_ = store.Close()
}
if err != nil {
if !errors.Is(err, errAborted) {
ui.Errorf("%s", err)
}
os.Exit(1)
}
}
// requireGit asserts we're inside a git repo and returns its root path.
func requireGit() (string, error) {
if !gitx.IsRepo() {
ui.Errorf("Not in a git repository")
return "", errAborted
}
return gitx.RepoRoot(), nil
}
// ensureAI builds the Ollama client and verifies server + model availability.
func ensureAI() error {
if client != nil {
return nil
}
c := ai.New(cfg.host, cfg.model, cfg.timeout)
if err := c.EnsureReady(); err != nil {
return err
}
client = c
return nil
}
func init() {
rootCmd.PersistentFlags().BoolVar(&flagAuto, "auto", false, "skip all confirmation prompts")
rootCmd.PersistentFlags().BoolVar(&flagPush, "push", false, "auto-push after committing")
rootCmd.PersistentFlags().StringVar(&flagModel, "model", "", "override the Ollama model tag")
rootCmd.PersistentFlags().StringVar(&flagHost, "host", "", "override the Ollama server URL")
}