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