Repository navigation
Expand file tree
/
Copy pathinclude_test.go
More file actions
202 lines (184 loc) · 6.9 KB
/
Copy pathinclude_test.go
File metadata and controls
202 lines (184 loc) · 6.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
package main
import (
"os"
"path/filepath"
"testing"
)
// setupIncludeRepo builds a repo with a committed .gitignore, ignored files,
// and a .worktreeinclude covering some of them.
func setupIncludeRepo(t *testing.T) string {
t.Helper()
repo := newTestRepo(t)
writeFile := func(rel, content string) {
t.Helper()
path := filepath.Join(repo, rel)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
writeFile(".gitignore", ".env\nsecrets/\n")
gitT(t, repo, "add", ".gitignore")
gitT(t, repo, "commit", "-q", "-m", "add gitignore")
writeFile(".env", "SECRET=1\n")
writeFile("secrets/key.txt", "key\n")
writeFile("untracked.txt", "not ignored\n")
writeFile(".worktreeinclude", ".env\nsecrets/\nuntracked.txt\n")
return repo
}
// TestResolveWorktreeCopiesIncludesFromPathEndingInSpace guards the source
// path against whitespace trimming: `git rev-parse --show-toplevel` ends in
// a newline, and trimming more than that silently loses a trailing space of
// the repository path, so the include file is looked up in the wrong place.
func TestResolveWorktreeCopiesIncludesFromPathEndingInSpace(t *testing.T) {
dir := filepath.Join(t.TempDir(), "repo ")
if err := os.Mkdir(dir, 0o755); err != nil {
t.Fatal(err)
}
repo := newTestRepoAt(t, dir)
writeFile := func(rel, content string) {
t.Helper()
if err := os.WriteFile(filepath.Join(repo, rel), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
writeFile(".gitignore", ".env\n")
gitT(t, repo, "add", ".gitignore")
gitT(t, repo, "commit", "-q", "-m", "add gitignore")
writeFile(".env", "SECRET=1\n")
writeFile(".worktreeinclude", ".env\n")
ctx, _ := loadRepoWithRoot(t, repo)
wt := mustResolve(t, ctx, repo, "feature-x")
if _, err := os.Stat(filepath.Join(wt, ".env")); err != nil {
t.Errorf(".env must be copied from a repository path ending in a space: %v", err)
}
}
func TestCopyWorktreeInclude(t *testing.T) {
repo := setupIncludeRepo(t)
dst := filepath.Join(t.TempDir(), "wt")
gitT(t, repo, "worktree", "add", "-q", "-b", "topic", dst)
if err := copyWorktreeInclude(repo, dst); err != nil {
t.Fatalf("copyWorktreeInclude: %v", err)
}
for _, rel := range []string{".env", "secrets/key.txt"} {
body, err := os.ReadFile(filepath.Join(dst, rel))
if err != nil {
t.Errorf("%s must be copied: %v", rel, err)
continue
}
orig, _ := os.ReadFile(filepath.Join(repo, rel))
if string(body) != string(orig) {
t.Errorf("%s content = %q, want %q", rel, body, orig)
}
}
// untracked.txt matches a pattern but is not gitignored: never copied.
if _, err := os.Stat(filepath.Join(dst, "untracked.txt")); !os.IsNotExist(err) {
t.Error("untracked but not ignored file must not be copied")
}
}
func TestCopyWorktreeIncludeWithoutFile(t *testing.T) {
repo := newTestRepo(t)
dst := filepath.Join(t.TempDir(), "wt")
gitT(t, repo, "worktree", "add", "-q", "-b", "topic", dst)
if err := copyWorktreeInclude(repo, dst); err != nil {
t.Fatalf("missing .worktreeinclude must be a no-op: %v", err)
}
}
// makeDivergentBranch creates a branch whose tree is edited by mutate,
// using a throwaway worktree that is removed afterwards.
func makeDivergentBranch(t *testing.T, repo, branch string, mutate func(dir string)) {
t.Helper()
tmp := filepath.Join(t.TempDir(), "edit")
gitT(t, repo, "worktree", "add", "-q", "-b", branch, tmp)
mutate(tmp)
gitT(t, tmp, "add", "-A", "-f")
gitT(t, tmp, "commit", "-q", "-m", "diverge")
gitT(t, repo, "worktree", "remove", "--force", tmp)
}
func TestCopyWorktreeIncludeNeverOverwritesTrackedFile(t *testing.T) {
repo := setupIncludeRepo(t)
// The destination branch tracks .env (and stops ignoring it): the
// checkout puts the tracked content in place, and the include copy at
// creation time must not clobber it.
makeDivergentBranch(t, repo, "tracked-env", func(dir string) {
if err := os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("secrets/\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, ".env"), []byte("TRACKED=1\n"), 0o644); err != nil {
t.Fatal(err)
}
})
ctx, _ := loadRepoWithRoot(t, repo)
wt := mustResolve(t, ctx, repo, "tracked-env")
body, err := os.ReadFile(filepath.Join(wt, ".env"))
if err != nil {
t.Fatal(err)
}
if string(body) != "TRACKED=1\n" {
t.Errorf("tracked .env was overwritten: %q", body)
}
if clean, _ := worktreeClean(wt); !clean {
t.Error("destination worktree must stay clean after the copy")
}
}
func TestCopyWorktreeIncludeSkipsWhenDestinationDoesNotIgnore(t *testing.T) {
repo := setupIncludeRepo(t)
// The destination branch stops ignoring .env: copying it would create
// an immediately dirty worktree.
makeDivergentBranch(t, repo, "no-ignore", func(dir string) {
if err := os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("secrets/\n"), 0o644); err != nil {
t.Fatal(err)
}
})
ctx, _ := loadRepoWithRoot(t, repo)
wt := mustResolve(t, ctx, repo, "no-ignore")
if _, err := os.Stat(filepath.Join(wt, ".env")); !os.IsNotExist(err) {
t.Error(".env must not be copied when the destination does not ignore it")
}
if _, err := os.Stat(filepath.Join(wt, "secrets", "key.txt")); err != nil {
t.Errorf("still-ignored file must be copied: %v", err)
}
if clean, _ := worktreeClean(wt); !clean {
t.Error("destination worktree must stay clean after the copy")
}
}
func TestResolveWorktreeCopiesIncludes(t *testing.T) {
repo := setupIncludeRepo(t)
ctx, _ := loadRepoWithRoot(t, repo)
wt := mustResolve(t, ctx, repo, "feature-x")
if _, err := os.Stat(filepath.Join(wt, ".env")); err != nil {
t.Errorf(".env must be copied into the new worktree: %v", err)
}
}
func TestResolveWorktreeSkipsDanglingSymlinkInclude(t *testing.T) {
repo := setupIncludeRepo(t)
// A stale link matching an include pattern must not block creation.
if err := os.Symlink("/nonexistent/.env", filepath.Join(repo, "secrets", "dangling")); err != nil {
t.Fatal(err)
}
ctx, _ := loadRepoWithRoot(t, repo)
wt := mustResolve(t, ctx, repo, "feature-x")
if _, err := os.Lstat(filepath.Join(wt, "secrets", "dangling")); !os.IsNotExist(err) {
t.Errorf("dangling symlink must not be copied: %v", err)
}
if _, err := os.Stat(filepath.Join(wt, ".env")); err != nil {
t.Errorf(".env must still be copied: %v", err)
}
}
func TestResolveWorktreeSkipsSymlinkInclude(t *testing.T) {
repo := setupIncludeRepo(t)
// A link to a real file must not be dereferenced into a regular copy.
if err := os.Symlink("key.txt", filepath.Join(repo, "secrets", "link")); err != nil {
t.Fatal(err)
}
ctx, _ := loadRepoWithRoot(t, repo)
wt := mustResolve(t, ctx, repo, "feature-x")
if _, err := os.Lstat(filepath.Join(wt, "secrets", "link")); !os.IsNotExist(err) {
t.Errorf("symlink must not be copied: %v", err)
}
if _, err := os.Stat(filepath.Join(wt, "secrets", "key.txt")); err != nil {
t.Errorf("regular file next to the link must still be copied: %v", err)
}
}