Skip to content

Commit 3d4de23

Browse files
Adopt kit git helpers and pin the Go toolchain (#576)
## Summary - Switch git repository discovery and author lookup onto `go.kenn.io/kit v0.1.0` - Thread caller `ctx` through the git outcome path instead of using background contexts - Pin the Go toolchain consistently in `go.mod` and the Docker build image ## Testing - `go test ./internal/db/git ./internal/db` - `docker build --target build -t agentsview-kit-check .` - `go test ./...` reported one unrelated existing failure in `internal/parser` (`TestParseWorkBuddySession`) Co-authored-by: Marius van Niekerk <mariusvniekerk@users.noreply.github.com>
1 parent 1c330e8 commit 3d4de23

14 files changed

Lines changed: 219 additions & 67 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ RUN npm ci
88
COPY frontend/ ./
99
RUN npm run build
1010

11-
FROM golang:1.26-bookworm AS build
11+
FROM golang:1.26.3-bookworm AS build
1212

1313
RUN apt-get update \
1414
&& apt-get install -y --no-install-recommends build-essential ca-certificates \

cmd/agentsview/token_use.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,8 @@ func sessionUsageData(sessionID string) (*sessionUsageOutput, int, error) {
317317
Machine: "local",
318318
BlockedResultCategories: appCfg.ResultContentBlockedCategories,
319319
})
320-
if syncErr := engine.SyncSingleSession(
321-
resolvedID,
320+
if syncErr := engine.SyncSingleSessionContext(
321+
ctx, resolvedID,
322322
); syncErr != nil {
323323
// Not fatal: session may already be in the DB
324324
// from a previous sync, or may not exist at all.

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module go.kenn.io/agentsview
22

3-
go 1.26.2
3+
go 1.26.3
44

55
require (
66
github.com/BurntSushi/toml v1.6.0
@@ -14,6 +14,7 @@ require (
1414
github.com/spf13/pflag v1.0.10
1515
github.com/stretchr/testify v1.11.1
1616
github.com/tidwall/gjson v1.19.0
17+
go.kenn.io/kit v0.1.0
1718
golang.org/x/mod v0.36.0
1819
golang.org/x/sync v0.20.0
1920
golang.org/x/sys v0.44.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
4848
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
4949
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
5050
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
51+
go.kenn.io/kit v0.1.0 h1:7l8aWAcnal/+DXT8I9t9UlvQrNCjsBoGrUiyJVS6i+c=
52+
go.kenn.io/kit v0.1.0/go.mod h1:nuRwWHhrDZP2rC8Auntl71la2Iu04B4twUkJl0SxIJ4=
5153
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
5254
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
5355
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=

internal/db/git/log.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import (
55
"bytes"
66
"context"
77
"fmt"
8-
"os/exec"
98
"regexp"
109
"strconv"
1110
"strings"
11+
12+
gitcmd "go.kenn.io/kit/git/cmd"
1213
)
1314

1415
// LogResult aggregates author-filtered counts from `git log --numstat` output.
@@ -33,20 +34,19 @@ type LogResult struct {
3334
func AggregateLog(
3435
ctx context.Context, repo, authorEmail, since, until string,
3536
) (LogResult, error) {
36-
cmd := exec.CommandContext(
37-
ctx, "git", "log",
37+
runner := gitcmd.New()
38+
runner.NullGlobalConfig = false
39+
out, stderr, err := runner.Run(
40+
ctx, repo, nil,
41+
"log",
3842
"--numstat",
3943
"--format=%H",
4044
"--since="+since,
4145
"--until="+until,
4246
"--author="+authorEmailPattern(authorEmail),
4347
)
44-
cmd.Dir = repo
45-
var stderr bytes.Buffer
46-
cmd.Stderr = &stderr
47-
out, err := cmd.Output()
4848
if err != nil {
49-
msg := strings.TrimSpace(stderr.String())
49+
msg := strings.TrimSpace(string(stderr))
5050
// An empty repo (initialized but no commits, or a worktree
5151
// pointed at an unborn branch) is a normal state, not an
5252
// error — there is simply no log to aggregate. Treat as a
@@ -169,17 +169,19 @@ func authorEmailPattern(email string) string {
169169
// AuthorEmail returns `git config user.email` run from inside the repo,
170170
// falling back to the global config. Returns "" if neither is set or git
171171
// is not available.
172-
func AuthorEmail(repo string) string {
173-
cmd := exec.Command("git", "config", "user.email")
174-
cmd.Dir = repo
175-
out, err := cmd.Output()
172+
func AuthorEmail(ctx context.Context, repo string) string {
173+
localRunner := gitcmd.New()
174+
localRunner.NullGlobalConfig = false
175+
out, err := localRunner.Output(ctx, repo, "config", "user.email")
176176
if err == nil {
177177
if v := strings.TrimSpace(string(out)); v != "" {
178178
return v
179179
}
180180
}
181-
cmd = exec.Command("git", "config", "--global", "user.email")
182-
out, err = cmd.Output()
181+
182+
globalRunner := gitcmd.New()
183+
globalRunner.NullGlobalConfig = false
184+
out, err = globalRunner.Output(ctx, "", "config", "--global", "user.email")
183185
if err != nil {
184186
return ""
185187
}

internal/db/git/log_test.go

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,49 @@ func TestAggregateLog_EmptyRepoReturnsZero(t *testing.T) {
178178
assert.Equal(t, LogResult{}, got, "AggregateLog on empty repo")
179179
}
180180

181+
func TestAggregateLog_UsesGlobalGitConfig(t *testing.T) {
182+
skipIfNoGit(t)
183+
home := t.TempDir()
184+
t.Setenv("HOME", home)
185+
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
186+
globalConfig := filepath.Join(home, ".gitconfig")
187+
t.Setenv("GIT_CONFIG_GLOBAL", globalConfig)
188+
189+
attrsPath := filepath.Join(home, "attributes")
190+
require.NoError(t, os.WriteFile(
191+
attrsPath, []byte("*.txt binary\n"), 0o644,
192+
), "write global attributes")
193+
require.NoError(t, os.WriteFile(
194+
globalConfig,
195+
[]byte("[core]\n\tattributesfile = "+filepath.ToSlash(attrsPath)+"\n"),
196+
0o644,
197+
), "write global git config")
198+
199+
repo := initRepo(t)
200+
writeFile(t, repo, "configured-binary.txt", []byte("a\nb\nc\n"))
201+
commitAs(t, repo, "test@example.com", "Test User", "c1")
202+
203+
got, err := AggregateLog(
204+
context.Background(),
205+
repo, "test@example.com",
206+
"1970-01-01T00:00:00Z", "2099-01-01T00:00:00Z",
207+
)
208+
require.NoError(t, err, "AggregateLog")
209+
assert.Equal(t, LogResult{
210+
Commits: 1,
211+
LOCAdded: 0,
212+
LOCRemoved: 0,
213+
FilesChanged: 1,
214+
}, got, "AggregateLog should respect global git config")
215+
}
216+
181217
func TestAuthorEmail_LocalConfig(t *testing.T) {
182218
skipIfNoGit(t)
183219
repo := t.TempDir()
184220
gitRun(t, repo, nil, "init", "-q", "-b", "main")
185221
gitRun(t, repo, nil, "config", "user.email", "local@example.com")
186222

187-
got := AuthorEmail(repo)
223+
got := AuthorEmail(context.Background(), repo)
188224
assert.Equal(t, "local@example.com", got, "AuthorEmail")
189225
}
190226

@@ -221,10 +257,40 @@ func TestAuthorEmail_FallsBackToGlobal(t *testing.T) {
221257
out, err = initCmd.CombinedOutput()
222258
require.NoError(t, err, "git init: %s", out)
223259

224-
got := AuthorEmail(repo)
260+
got := AuthorEmail(context.Background(), repo)
225261
assert.Equal(t, "global@example.com", got, "AuthorEmail (global fallback)")
226262
}
227263

264+
func TestAuthorEmail_UsesIncludeIfGitdir(t *testing.T) {
265+
skipIfNoGit(t)
266+
home := t.TempDir()
267+
t.Setenv("HOME", home)
268+
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
269+
globalConfig := filepath.Join(home, ".gitconfig")
270+
t.Setenv("GIT_CONFIG_GLOBAL", globalConfig)
271+
272+
repo := t.TempDir()
273+
gitRun(t, repo, nil, "init", "-q", "-b", "main")
274+
275+
includePath := filepath.Join(home, "repo.gitconfig")
276+
require.NoError(t, os.WriteFile(
277+
includePath,
278+
[]byte("[user]\n\temail = includeif@example.com\n"),
279+
0o644,
280+
), "write include config")
281+
gitdir, err := filepath.EvalSymlinks(filepath.Join(repo, ".git"))
282+
require.NoError(t, err, "resolve repo gitdir")
283+
require.NoError(t, os.WriteFile(
284+
globalConfig,
285+
[]byte(`[includeIf "gitdir:`+filepath.ToSlash(gitdir)+`"]
286+
path = `+filepath.ToSlash(includePath)+"\n"),
287+
0o644,
288+
), "write global config")
289+
290+
got := AuthorEmail(context.Background(), repo)
291+
assert.Equal(t, "includeif@example.com", got, "AuthorEmail (includeIf.gitdir)")
292+
}
293+
228294
func TestParseNumstat_SkipsBinaryLOCButCountsFile(t *testing.T) {
229295
// Unit test of the pure parser, independent of git exec.
230296
input := []byte(strings.Join([]string{

internal/db/git/repos.go

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
package git
44

55
import (
6-
"bytes"
76
"context"
87
"os"
9-
"os/exec"
108
"path/filepath"
11-
"strings"
129
"time"
10+
11+
gitrepo "go.kenn.io/kit/git/repo"
1312
)
1413

1514
// DiscoverRepos resolves each cwd to its enclosing git repository toplevel and
@@ -23,11 +22,11 @@ import (
2322
// helper falls back to walking upward from the nearest existing ancestor and
2423
// invoking `git rev-parse` from there — that mirrors how the parser package
2524
// recovers repo roots for archived sessions whose cwd has been deleted.
26-
func DiscoverRepos(cwds []string) []string {
25+
func DiscoverRepos(ctx context.Context, cwds []string) []string {
2726
seen := map[string]struct{}{}
2827
out := []string{}
2928
for _, cwd := range cwds {
30-
root := findRepoRoot(cwd)
29+
root := findRepoRoot(ctx, cwd)
3130
if root == "" {
3231
continue
3332
}
@@ -42,15 +41,15 @@ func DiscoverRepos(cwds []string) []string {
4241

4342
// findRepoRoot returns the absolute repo toplevel for start, or "" when no
4443
// enclosing repo can be resolved.
45-
func findRepoRoot(start string) string {
44+
func findRepoRoot(ctx context.Context, start string) string {
4645
if start == "" {
4746
return ""
4847
}
4948
dir := existingAncestor(start)
5049
if dir == "" {
5150
return ""
5251
}
53-
return gitToplevel(dir)
52+
return gitToplevel(ctx, dir)
5453
}
5554

5655
// existingAncestor returns the closest ancestor of path that exists on disk
@@ -79,18 +78,12 @@ func existingAncestor(path string) string {
7978
// gitToplevel runs `git rev-parse --show-toplevel` from dir and returns the
8079
// trimmed result, or "" if git fails or prints nothing. A 5s timeout guards
8180
// against hung git invocations on broken repos.
82-
func gitToplevel(dir string) string {
83-
ctx, cancel := context.WithTimeout(
84-
context.Background(), 5*time.Second,
85-
)
81+
func gitToplevel(ctx context.Context, dir string) string {
82+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
8683
defer cancel()
87-
cmd := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel")
88-
cmd.Dir = dir
89-
var stderr bytes.Buffer
90-
cmd.Stderr = &stderr
91-
out, err := cmd.Output()
84+
root, err := gitrepo.Root(ctx, dir)
9285
if err != nil {
9386
return ""
9487
}
95-
return strings.TrimSpace(string(out))
88+
return root
9689
}

internal/db/git/repos_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package git
22

33
import (
4+
"context"
45
"os"
56
"path/filepath"
67
"sort"
@@ -54,7 +55,7 @@ func TestDiscoverRepos_FindsRootAndFiltersMissing(t *testing.T) {
5455
sub := mkdirIn(t, repoA, "subdir")
5556
outside := t.TempDir()
5657

57-
got := DiscoverRepos([]string{sub, outside})
58+
got := DiscoverRepos(context.Background(), []string{sub, outside})
5859
want := []string{repoA}
5960
assert.Equal(t, canonAll(want), canonAll(got), "DiscoverRepos")
6061
}
@@ -65,17 +66,17 @@ func TestDiscoverRepos_Dedup(t *testing.T) {
6566
sub1 := mkdirIn(t, repoA, "sub1")
6667
sub2 := mkdirIn(t, repoA, "sub2/deeper")
6768

68-
got := DiscoverRepos([]string{sub1, sub2, repoA})
69+
got := DiscoverRepos(context.Background(), []string{sub1, sub2, repoA})
6970
require.Len(t, got, 1, "want exactly one entry (dedup)")
7071
assert.Equal(t, canonAll([]string{repoA}), canonAll(got),
7172
"DiscoverRepos")
7273
}
7374

7475
func TestDiscoverRepos_EmptyInputReturnsEmptySlice(t *testing.T) {
75-
got := DiscoverRepos(nil)
76+
got := DiscoverRepos(context.Background(), nil)
7677
require.NotNil(t, got, "DiscoverRepos(nil)")
7778
assert.Empty(t, got, "DiscoverRepos(nil) should be empty slice")
78-
got = DiscoverRepos([]string{})
79+
got = DiscoverRepos(context.Background(), []string{})
7980
require.NotNil(t, got, "DiscoverRepos([])")
8081
assert.Empty(t, got, "DiscoverRepos([]) should be empty slice")
8182
}
@@ -98,7 +99,7 @@ func TestDiscoverRepos_LinkedWorktreeResolves(t *testing.T) {
9899
"worktree", "add", "-b", "feature", worktreeRoot,
99100
)
100101

101-
got := DiscoverRepos([]string{worktreeRoot})
102+
got := DiscoverRepos(context.Background(), []string{worktreeRoot})
102103
require.Len(t, got, 1, "want one worktree root")
103104
assert.Equal(t,
104105
canonAll([]string{worktreeRoot}),
@@ -113,6 +114,6 @@ func TestDiscoverRepos_MissingCwdSkipped(t *testing.T) {
113114
skipIfNoGit(t)
114115
missing := filepath.Join(t.TempDir(), "no", "such", "path")
115116

116-
got := DiscoverRepos([]string{missing})
117+
got := DiscoverRepos(context.Background(), []string{missing})
117118
assert.Empty(t, got, "DiscoverRepos missing path")
118119
}

internal/db/session_stats.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,7 +1392,7 @@ func (db *DB) computeOutcomeStats(
13921392
cwds = append(cwds, r.cwd)
13931393
}
13941394
}
1395-
repos := git.DiscoverRepos(cwds)
1395+
repos := git.DiscoverRepos(ctx, cwds)
13961396
if len(repos) == 0 {
13971397
return nil
13981398
}
@@ -1402,7 +1402,7 @@ func (db *DB) computeOutcomeStats(
14021402
out := &StatsOutcomeStats{}
14031403
contributed := false
14041404
for _, repo := range repos {
1405-
email := git.AuthorEmail(repo)
1405+
email := git.AuthorEmail(ctx, repo)
14061406
if email == "" {
14071407
continue
14081408
}

0 commit comments

Comments
 (0)