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:
+166
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user