返回 DeepSeek-Reasonix
repo_test.go
根目录 / internal / gitcmd / repo_test.go
1 package gitcmd
2
3 import (
4 "errors"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9 )
10
11 // A Repo is resolved when the session opens; what the workspace gains
12 // afterwards must not change which repository, or whose config, host git reads.
13
14 func (f *repoFixture) open(dir string) Repo {
15 f.t.Helper()
16 repo, err := Open(f.ctx, dir)
17 if err != nil {
18 f.t.Fatalf("Open(%s): %v", dir, err)
19 }
20 return repo
21 }
22
23 func (f *repoFixture) repoOut(repo Repo, args ...string) (string, error) {
24 f.t.Helper()
25 out, err := repo.Command(f.ctx, args...).Output()
26 return strings.TrimSpace(string(out)), err
27 }
28
29 // substituteConfig is a local config a later write points git at: a probe key
30 // to read the config's origin by, and a filter that records any run.
31 func (f *repoFixture) substituteConfig() string {
32 return "[probe]\n\tx = substitute\n[filter \"evil\"]\n\tclean = " + f.payload() + "\n\tsmudge = " + f.payload() + "\n"
33 }
34
35 func (f *repoFixture) assertReadsOpenedRepository(repo Repo, wantGitDir string) {
36 f.t.Helper()
37 if got, err := f.repoOut(repo, "config", "--get", "probe.x"); err == nil || got != "" {
38 f.t.Fatalf("config probe.x = %q (err %v), want the substitute config unread", got, err)
39 }
40 if got, err := f.repoOut(repo, "rev-parse", "--absolute-git-dir"); err != nil || filepath.Clean(got) != wantGitDir {
41 f.t.Fatalf("git dir = %q (err %v), want %s", got, err, wantGitDir)
42 }
43 f.makeStatDirty("f.txt", "two\n")
44 if _, err := f.repoOut(repo, "status", "--porcelain"); err != nil {
45 f.t.Fatalf("status: %v", err)
46 }
47 if _, err := f.repoOut(repo, "diff", "HEAD"); err != nil {
48 f.t.Fatalf("diff: %v", err)
49 }
50 f.assertNotExecuted()
51 }
52
53 func TestOpenResolvesLinkedWorktreeIdentity(t *testing.T) {
54 f := newRepoFixture(t, "", map[string]string{"f.txt": "one\n"})
55 linked := filepath.Join(t.TempDir(), "linked")
56 f.plain("worktree", "add", "--quiet", linked)
57 repo := f.open(linked)
58 common := f.open(f.dir).GitDir
59 real, err := filepath.EvalSymlinks(linked)
60 if err != nil {
61 t.Fatal(err)
62 }
63 if repo.CommonDir != common || repo.GitDir == common || repo.WorkTree != real {
64 t.Fatalf("linked worktree resolved as %+v, want common dir %s", repo, common)
65 }
66 if _, err := Open(f.ctx, t.TempDir()); !errors.Is(err, ErrNotRepository) {
67 t.Fatalf("Open(non-repo) = %v, want ErrNotRepository", err)
68 }
69 if err := (Repo{}).Command(f.ctx, "status").Run(); !errors.Is(err, ErrNotRepository) {
70 t.Fatalf("unresolved Repo ran: %v, want ErrNotRepository", err)
71 }
72 }
73
74 // A workspace that is a subdirectory of a repository gains its own .git.
75 func TestOpenedRepoIgnoresNestedRepositoryCreatedLater(t *testing.T) {
76 f := newRepoFixture(t, "", map[string]string{"pkg/f.txt": "one\n", "f.txt": "one\n"})
77 pkg := filepath.Join(f.dir, "pkg")
78 repo := f.open(pkg)
79 if top := f.open(f.dir); repo.GitDir != top.GitDir || repo.WorkTree != top.WorkTree {
80 t.Fatalf("subdirectory resolved as %+v, want the enclosing repository %+v", repo, top)
81 }
82 f.plainIn(pkg, "init", "--quiet")
83 if err := os.WriteFile(filepath.Join(pkg, ".gitattributes"), []byte("*.txt filter=evil\n"), 0o644); err != nil {
84 t.Fatal(err)
85 }
86 if err := os.WriteFile(filepath.Join(pkg, ".git", "config"), []byte(f.substituteConfig()), 0o644); err != nil {
87 t.Fatal(err)
88 }
89 if out, _ := Command(f.ctx, pkg, "config", "--get", "probe.x").Output(); strings.TrimSpace(string(out)) != "substitute" {
90 t.Fatalf("discovery from the workspace read %q, want the nested config (the case being guarded)", out)
91 }
92 f.assertReadsOpenedRepository(repo, repo.GitDir)
93 }
94
95 // A commondir file written into the git dir redirects discovery to another
96 // repository's config.
97 func TestOpenedRepoIgnoresCommondirWrittenLater(t *testing.T) {
98 f := newRepoFixture(t, "", map[string]string{"f.txt": "one\n"})
99 repo := f.open(f.dir)
100 evil := filepath.Join(t.TempDir(), "evil")
101 f.plain("clone", "--quiet", "--bare", f.dir, evil)
102 if err := os.WriteFile(filepath.Join(evil, "config"), []byte("[core]\n\trepositoryformatversion = 0\n"+f.substituteConfig()), 0o644); err != nil {
103 t.Fatal(err)
104 }
105 f.write(".gitattributes", "*.txt filter=evil\n")
106 if err := os.WriteFile(filepath.Join(f.dir, ".git", "commondir"), []byte(evil+"\n"), 0o644); err != nil {
107 t.Fatal(err)
108 }
109 if out, _ := Command(f.ctx, f.dir, "config", "--get", "probe.x").Output(); strings.TrimSpace(string(out)) != "substitute" {
110 t.Fatalf("discovery read %q, want the commondir's config (the case being guarded)", out)
111 }
112 f.assertReadsOpenedRepository(repo, repo.GitDir)
113 }
114
115 // A damaged HEAD makes the git dir invalid; the workspace root dressed as an
116 // implicit bare repository must not stand in for it.
117 func TestOpenedRepoFailsClosedOnDamagedHead(t *testing.T) {
118 f := newRepoFixture(t, "", map[string]string{"f.txt": "one\n"})
119 repo := f.open(f.dir)
120 for _, d := range []string{"objects", "refs"} {
121 if err := os.MkdirAll(filepath.Join(f.dir, d), 0o755); err != nil {
122 t.Fatal(err)
123 }
124 }
125 f.write("HEAD", "ref: refs/heads/main\n")
126 f.write("config", "[core]\n\tbare = false\n\tworktree = "+f.dir+"\n"+f.substituteConfig())
127 f.write(".gitattributes", "*.txt filter=evil\n")
128 if err := os.WriteFile(filepath.Join(f.dir, ".git", "HEAD"), []byte("garbage\n"), 0o644); err != nil {
129 t.Fatal(err)
130 }
131 f.makeStatDirty("f.txt", "two\n")
132 for _, args := range [][]string{{"config", "--get", "probe.x"}, {"status", "--porcelain"}, {"diff", "HEAD"}} {
133 if out, err := f.repoOut(repo, args...); err == nil || strings.Contains(out, "substitute") {
134 t.Fatalf("%v = %q (err %v), want a failure naming no substitute", args, out, err)
135 }
136 if out, _ := f.run(args...); strings.Contains(out, "substitute") {
137 t.Fatalf("discovery %v read the implicit bare repository: %q", args, out)
138 }
139 }
140 f.assertNotExecuted()
141 }
142
143 // A git dir that belongs to another checkout makes host diffs read, and host
144 // writes land in, that checkout: Open refuses it as not a repository.
145 func TestOpenRefusesGitDirOfAnotherCheckout(t *testing.T) {
146 other := newRepoFixture(t, "", map[string]string{"other.txt": "other\n"})
147 t.Run("gitdir file", func(t *testing.T) {
148 ws := t.TempDir()
149 if err := os.WriteFile(filepath.Join(ws, ".git"), []byte("gitdir: "+filepath.Join(other.dir, ".git")+"\n"), 0o644); err != nil {
150 t.Fatal(err)
151 }
152 if out, _ := Command(other.ctx, ws, "ls-files").Output(); !strings.Contains(string(out), "other.txt") {
153 t.Fatalf("discovery listed %q, want the other checkout (the case being guarded)", out)
154 }
155 if _, err := Open(other.ctx, ws); !errors.Is(err, ErrNotRepository) {
156 t.Fatalf("Open = %v, want ErrNotRepository", err)
157 }
158 })
159 t.Run("core.worktree", func(t *testing.T) {
160 f := newRepoFixture(t, "", map[string]string{"f.txt": "one\n"})
161 f.plain("config", "core.worktree", other.dir)
162 if _, err := Open(f.ctx, f.dir); !errors.Is(err, ErrNotRepository) {
163 t.Fatalf("Open = %v, want ErrNotRepository", err)
164 }
165 })
166 t.Run("linked admin dir of another worktree", func(t *testing.T) {
167 linked := filepath.Join(t.TempDir(), "linked")
168 other.plain("worktree", "add", "--quiet", linked)
169 ws := t.TempDir()
170 admin := strings.TrimSpace(other.plainIn(linked, "rev-parse", "--absolute-git-dir"))
171 if err := os.WriteFile(filepath.Join(ws, ".git"), []byte("gitdir: "+admin+"\n"), 0o644); err != nil {
172 t.Fatal(err)
173 }
174 if _, err := Open(other.ctx, ws); !errors.Is(err, ErrNotRepository) {
175 t.Fatalf("Open = %v, want ErrNotRepository", err)
176 }
177 })
178 }
179
180 // A stored identity is held to Open's test when it is used again.
181 func TestVerifyRefusesStoredIdentityOfAnotherCheckout(t *testing.T) {
182 f := newRepoFixture(t, "", map[string]string{"f.txt": "one\n"})
183 other := newRepoFixture(t, "", map[string]string{"other.txt": "other\n"})
184 repo := f.open(f.dir)
185 if err := repo.Verify(); err != nil {
186 t.Fatalf("Verify(opened) = %v", err)
187 }
188 forged := repo
189 forged.GitDir = other.open(other.dir).GitDir
190 forged.CommonDir = forged.GitDir
191 if err := forged.Verify(); !errors.Is(err, ErrNotRepository) {
192 t.Fatalf("Verify(forged) = %v, want ErrNotRepository", err)
193 }
194 if err := (Repo{}).Verify(); !errors.Is(err, ErrNotRepository) {
195 t.Fatalf("Verify(zero) = %v, want ErrNotRepository", err)
196 }
197 }
198
198 lines GO