Skip to content

Commit f38ae87

Browse files
takecyclaude
andcommitted
refactor(syncer): detect repos with os.Stat to support git worktrees
IsRepo previously called os.ReadDir(dirName) and walked every entry looking for a name match against ".git". That has two problems: it pays full directory-listing cost just to answer a yes/no question about a single fixed name, and the name-only match doesn't make the worktree case explicit even though git worktrees place ".git" as a *file* (a "gitdir: ..." pointer) rather than a directory. Replace the loop with a single os.Stat on the joined path. One syscall instead of N, and stat doesn't care whether the entry is a file or a directory, so a worktree's .git file is correctly accepted as a repository indicator. Add syncer/dir_test.go covering both layouts plus the negative cases (non-git directory, non-existent path), pinning the worktree-support contract so a future "must be a directory" change would surface as a test failure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 77e85c4 commit f38ae87

2 files changed

Lines changed: 71 additions & 13 deletions

File tree

syncer/dir.go

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package syncer
22

33
import (
44
"os"
5+
"path/filepath"
56
"strings"
67
)
78

@@ -22,18 +23,11 @@ func ListDirs() (dirs []string, err error) {
2223
return
2324
}
2425

25-
// IsRepo returns check result, the directory whether git repository
26+
// IsRepo reports whether dirName contains a `.git` entry. It accepts both a
27+
// regular repository (where `.git` is a directory) and a git worktree
28+
// (where `.git` is a file pointing at the main repo's git directory),
29+
// because exec'd git commands work in either layout.
2630
func IsRepo(dirName string) bool {
27-
files, err := os.ReadDir(dirName)
28-
if err != nil {
29-
return false
30-
}
31-
32-
for _, f := range files {
33-
if f.Name() == ".git" {
34-
return true
35-
}
36-
}
37-
38-
return false
31+
_, err := os.Stat(filepath.Join(dirName, ".git"))
32+
return err == nil
3933
}

syncer/dir_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package syncer
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/matryer/is"
9+
)
10+
11+
func TestIsRepo(t *testing.T) {
12+
t.Run("regular repo with .git directory", func(t *testing.T) {
13+
t.Parallel()
14+
is := is.New(t)
15+
16+
dir := t.TempDir()
17+
repo := filepath.Join(dir, "myrepo")
18+
if err := os.MkdirAll(filepath.Join(repo, ".git"), 0o755); err != nil {
19+
t.Fatalf("setup: %v", err)
20+
}
21+
22+
is.True(IsRepo(repo))
23+
})
24+
25+
t.Run("worktree with .git file", func(t *testing.T) {
26+
t.Parallel()
27+
is := is.New(t)
28+
29+
dir := t.TempDir()
30+
worktree := filepath.Join(dir, "myworktree")
31+
if err := os.MkdirAll(worktree, 0o755); err != nil {
32+
t.Fatalf("setup: %v", err)
33+
}
34+
// git worktrees place a .git file (not directory) containing
35+
// a `gitdir: <path>` pointer. The exact contents don't matter for
36+
// IsRepo — only that the entry exists.
37+
gitFile := filepath.Join(worktree, ".git")
38+
if err := os.WriteFile(gitFile, []byte("gitdir: /tmp/main/.git/worktrees/x\n"), 0o644); err != nil {
39+
t.Fatalf("setup: %v", err)
40+
}
41+
42+
is.True(IsRepo(worktree))
43+
})
44+
45+
t.Run("non-git directory returns false", func(t *testing.T) {
46+
t.Parallel()
47+
is := is.New(t)
48+
49+
dir := t.TempDir()
50+
plain := filepath.Join(dir, "plain")
51+
if err := os.MkdirAll(plain, 0o755); err != nil {
52+
t.Fatalf("setup: %v", err)
53+
}
54+
55+
is.True(!IsRepo(plain))
56+
})
57+
58+
t.Run("non-existent directory returns false", func(t *testing.T) {
59+
t.Parallel()
60+
is := is.New(t)
61+
62+
is.True(!IsRepo(filepath.Join(t.TempDir(), "does-not-exist")))
63+
})
64+
}

0 commit comments

Comments
 (0)