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
+101
View File
@@ -0,0 +1,101 @@
// Package diff filters git diffs before they reach the model: lock/generated
// files lose their hunk bodies, JSON changes are capped, and every other file
// is truncated. Behavior is a verbatim port of the Python filter_diff.
package diff
import (
"fmt"
"regexp"
"strings"
)
var lockPattern = regexp.MustCompile(
`(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb|` +
`go\.sum|go\.mod|Cargo\.lock|poetry\.lock|composer\.lock|Gemfile\.lock|` +
`.*\.min\.(js|css)|.*\.bundle\.js|.*\.map|` +
`dist/.*|build/.*|\.next/.*|node_modules/.*|vendor/.*|__pycache__/.*|\.pyc$|target/.*)`,
)
var (
fileNameRe = regexp.MustCompile(`b/([^ ]+)`)
headerRe = regexp.MustCompile(`^(index|---|\+\+\+|@@)`)
)
const (
// MaxDiffLines caps the per-file line count for regular files.
MaxDiffLines = 200
// MaxJSONLines caps the +/- line count for JSON files.
MaxJSONLines = 50
)
// Filter rewrites a raw git diff for prompt consumption.
func Filter(diff string) string {
var lines []string
inFiltered, inJSON := false, false
lineCount, jsonCount := 0, 0
for _, line := range strings.Split(diff, "\n") {
if strings.HasPrefix(line, "diff --git") {
lineCount, jsonCount = 0, 0
filename := ""
if m := fileNameRe.FindStringSubmatch(line); m != nil {
filename = m[1]
}
switch {
case lockPattern.MatchString(filename):
inFiltered, inJSON = true, false
lines = append(lines, line)
continue
case strings.HasSuffix(filename, ".json"):
inFiltered, inJSON = false, true
lines = append(lines, line)
continue
default:
inFiltered, inJSON = false, false
}
}
// Lock/generated files: keep headers, replace each hunk body with a
// marker. (The Python original stopped filtering after the first @@,
// leaking hunk bodies; the spec requires bodies replaced, so filtering
// holds until the next "diff --git".)
if inFiltered {
if headerRe.MatchString(line) {
lines = append(lines, line)
if strings.HasPrefix(line, "@@") {
lines = append(lines, "[Generated/lock file - content filtered]")
}
}
continue
}
if inJSON {
if headerRe.MatchString(line) {
lines = append(lines, line)
continue
}
if strings.HasPrefix(line, "+") || strings.HasPrefix(line, "-") {
jsonCount++
if jsonCount <= MaxJSONLines {
lines = append(lines, line)
} else if jsonCount == MaxJSONLines+1 {
lines = append(lines, fmt.Sprintf("[... JSON truncated after %d lines ...]", MaxJSONLines))
}
} else {
lines = append(lines, line)
}
continue
}
if lineCount < MaxDiffLines {
lines = append(lines, line)
lineCount++
} else if lineCount == MaxDiffLines {
lines = append(lines, fmt.Sprintf("[... truncated after %d lines ...]", MaxDiffLines))
lineCount++
}
}
return strings.Join(lines, "\n")
}
+127
View File
@@ -0,0 +1,127 @@
package diff
import (
"fmt"
"strings"
"testing"
)
func diffHeader(file string) string {
return fmt.Sprintf("diff --git a/%s b/%s\nindex 111..222 100644\n--- a/%s\n+++ b/%s\n@@ -1,2 +1,2 @@",
file, file, file, file)
}
func TestFilter(t *testing.T) {
bigBody := make([]string, 0, 300)
for i := range 300 {
bigBody = append(bigBody, fmt.Sprintf("+line %d", i))
}
jsonBody := make([]string, 0, 80)
for i := range 80 {
jsonBody = append(jsonBody, fmt.Sprintf("+ \"key%d\": %d,", i, i))
}
tests := []struct {
name string
input string
wantContain []string
wantAbsent []string
}{
{
name: "small file passes through unchanged",
input: diffHeader("main.go") + "\n-old\n+new",
wantContain: []string{"-old", "+new"},
wantAbsent: []string{"truncated", "filtered"},
},
{
name: "lock file body replaced with marker",
input: diffHeader("package-lock.json") + "\n-\"old\": 1\n+\"new\": 2",
wantContain: []string{
"diff --git a/package-lock.json b/package-lock.json",
"@@ -1,2 +1,2 @@",
"[Generated/lock file - content filtered]",
},
wantAbsent: []string{`"old": 1`, `"new": 2`},
},
{
name: "go.sum matched by lock pattern",
input: diffHeader("go.sum") + "\n+github.com/x v1.0.0 h1:abc",
wantContain: []string{"[Generated/lock file - content filtered]"},
wantAbsent: []string{"h1:abc"},
},
{
name: "path prefix matched anywhere in name",
input: diffHeader("node_modules/foo.js") + "\n+secret",
wantContain: []string{"[Generated/lock file - content filtered]"},
wantAbsent: []string{"+secret"},
},
{
name: "json file capped at 50 +/- lines",
input: diffHeader("config.json") + "\n" + strings.Join(jsonBody, "\n"),
wantContain: []string{
`+ "key49": 49,`,
"[... JSON truncated after 50 lines ...]",
},
wantAbsent: []string{`"key50"`, `"key79"`},
},
{
name: "json context lines kept beyond the cap",
input: diffHeader("config.json") + "\n" + strings.Join(jsonBody, "\n") + "\n context line",
wantContain: []string{" context line"},
},
{
name: "regular file truncated at 200 lines",
input: diffHeader("big.go") + "\n" + strings.Join(bigBody, "\n"),
wantContain: []string{
"[... truncated after 200 lines ...]",
},
wantAbsent: []string{"+line 299"},
},
{
name: "counters reset per file",
input: diffHeader("big.go") + "\n" + strings.Join(bigBody, "\n") + "\n" +
diffHeader("small.go") + "\n+after",
wantContain: []string{"+after"},
},
{
name: "lock file followed by normal file",
input: diffHeader("yarn.lock") + "\n+lockline\n" +
diffHeader("app.ts") + "\n+visible",
wantContain: []string{"[Generated/lock file - content filtered]", "+visible"},
wantAbsent: []string{"+lockline"},
},
{
name: "empty diff stays empty",
input: "",
wantContain: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Filter(tt.input)
for _, want := range tt.wantContain {
if !strings.Contains(got, want) {
t.Errorf("output missing %q\noutput:\n%s", want, got)
}
}
for _, absent := range tt.wantAbsent {
if strings.Contains(got, absent) {
t.Errorf("output should not contain %q\noutput:\n%s", absent, got)
}
}
})
}
}
func TestFilterTruncationMarkerOnlyOnce(t *testing.T) {
var body []string
for i := range 400 {
body = append(body, fmt.Sprintf("+l%d", i))
}
got := Filter(diffHeader("big.go") + "\n" + strings.Join(body, "\n"))
if n := strings.Count(got, "[... truncated after 200 lines ...]"); n != 1 {
t.Errorf("want exactly 1 truncation marker, got %d", n)
}
}