返回 DeepSeek-Reasonix
worktree_flow_test.go
根目录 / internal / gitcmd / worktree_flow_test.go
1 package gitcmd
2
3 import (
4 "os"
5 "os/exec"
6 "path/filepath"
7 "slices"
8 "strings"
9 "testing"
10 )
11
12 // Worktree creation adds with --no-checkout and populates with a reset run
13 // inside the new worktree, where the driver listing sees an includeIf keyed on
14 // that worktree's gitdir or branch.
15 func TestNoCheckoutThenResetSeesConditionalInclude(t *testing.T) {
16 for _, tc := range []struct {
17 name, cond string
18 add func(dst string) []string
19 }{
20 {"gitdir detached", "gitdir:**/worktrees/**", func(dst string) []string {
21 return []string{"worktree", "add", "--no-checkout", "--detach", dst, "HEAD"}
22 }},
23 {"onbranch -b", "onbranch:reasonix/**", func(dst string) []string {
24 return []string{"worktree", "add", "--no-checkout", "-b", "reasonix/delivery-x", dst, "HEAD"}
25 }},
26 {"worktree-scope include", "gitdir:**/worktrees/**", nil},
27 } {
28 t.Run(tc.name, func(t *testing.T) {
29 f := newRepoFixture(t, "f.txt filter=pwn\n", map[string]string{"f.txt": "hello\n"})
30 p := f.payload()
31 evil := filepath.Join(t.TempDir(), "evil.cfg")
32 if err := os.WriteFile(evil, []byte("[filter \"pwn\"]\n\tsmudge = "+p+"\n\tclean = "+p+"\n\tprocess = "+p+"\n"), 0o644); err != nil {
33 t.Fatal(err)
34 }
35 f.appendConfig("config", "[includeIf \""+tc.cond+"\"]\n\tpath = "+evil+"\n")
36 dst := filepath.Join(t.TempDir(), "linked")
37 add := tc.add
38 if add == nil {
39 add = func(dst string) []string {
40 return []string{"worktree", "add", "--no-checkout", "--detach", dst, "HEAD"}
41 }
42 }
43 f.mustRun(add(dst)...)
44 head := strings.TrimSpace(f.plain("rev-parse", "HEAD"))
45 if out, err := Command(f.ctx, dst, "reset", "--hard", head).CombinedOutput(); err != nil {
46 t.Fatalf("reset: %v %s", err, out)
47 }
48 if data, _ := os.ReadFile(filepath.Join(dst, "f.txt")); string(data) != "hello\n" {
49 t.Fatalf("f.txt = %q", data)
50 }
51 if out, err := Command(f.ctx, dst, "status", "--porcelain=v1").CombinedOutput(); err != nil || strings.TrimSpace(string(out)) != "" {
52 t.Fatalf("status after reset = %q %v", out, err)
53 }
54 f.assertNotExecuted()
55 })
56 }
57 }
58
59 // Removal runs a hardened status inside the linked worktree, then removes with
60 // --force so git starts no status of its own there.
61 func TestWorktreeRemoveForceSkipsLinkedConfig(t *testing.T) {
62 f := newRepoFixture(t, "f.txt filter=pwn\n", map[string]string{"f.txt": "hello\n"})
63 f.plain("config", "extensions.worktreeConfig", "true")
64 dst := filepath.Join(t.TempDir(), "linked")
65 f.plain("worktree", "add", "--quiet", "--detach", dst, "HEAD")
66 gitDir := strings.TrimSpace(f.plainIn(dst, "rev-parse", "--absolute-git-dir"))
67 if err := os.WriteFile(filepath.Join(gitDir, "config.worktree"), []byte("[filter \"pwn\"]\n\tclean = "+f.payload()+"\n"), 0o644); err != nil {
68 t.Fatal(err)
69 }
70 if err := os.WriteFile(filepath.Join(dst, "f.txt"), []byte("hellx\n"), 0o644); err != nil {
71 t.Fatal(err)
72 }
73 // the pre-remove clean check runs with dir=linked worktree
74 _, _ = Command(f.ctx, dst, "status", "--porcelain=v1", "--untracked-files=all", "--ignored").CombinedOutput()
75 _, _ = f.run("worktree", "remove", "--force", dst)
76 f.assertNotExecuted()
77 }
78
79 // submodule.recurse is overridden only where the repository sets it, so a
80 // user's own global value is left alone elsewhere.
81 func TestSubmoduleRecursePinnedOnlyWhenRepositorySetsIt(t *testing.T) {
82 f := newRepoFixture(t, "", map[string]string{"f.txt": "hello\n"})
83 if args := Command(f.ctx, f.dir, "status").Args; slices.Contains(args, "submodule.recurse=false") {
84 t.Fatalf("args = %v, want no submodule.recurse override for a repository that does not set it", args)
85 }
86 f.appendConfig("config", "[submodule]\n\trecurse = true\n")
87 if args := Command(f.ctx, f.dir, "status").Args; !slices.Contains(args, "submodule.recurse=false") {
88 t.Fatalf("args = %v, want submodule.recurse=false when the repository sets it", args)
89 }
90 }
91
92 // A partial clone's absent objects are reported by git's own listing, so a
93 // caller can tell "not fetched" from any other failure.
94 func TestObjectsMissingReportsPartialCloneGaps(t *testing.T) {
95 src := newRepoFixture(t, "", map[string]string{"a.txt": "1\n"})
96 src.write("a.txt", "2\n")
97 src.plain("commit", "--quiet", "-am", "two")
98 src.write("a.txt", "3\n")
99 src.plain("commit", "--quiet", "-am", "three")
100 src.plain("config", "uploadpack.allowFilter", "true")
101 clone := filepath.Join(t.TempDir(), "partial")
102 if out, err := exec.CommandContext(src.ctx, "git", "clone", "--quiet", "--filter=blob:none", "file://"+src.dir, clone).CombinedOutput(); err != nil {
103 t.Fatalf("partial clone: %v: %s", err, out)
104 }
105 repo, err := Open(src.ctx, clone)
106 if err != nil {
107 t.Fatal(err)
108 }
109 if repo.ObjectsMissing(src.ctx, "HEAD") {
110 t.Fatal("HEAD's objects are checked out, want none missing")
111 }
112 if !repo.ObjectsMissing(src.ctx, "HEAD~1", "HEAD~1^") {
113 t.Fatal("historical blobs were filtered out, want them reported missing")
114 }
115 if _, err := Command(src.ctx, clone, "show", "HEAD~1").Output(); err == nil {
116 t.Fatal("show of an unfetched commit succeeded, want it to fail without fetching")
117 }
118 }
119
119 lines GO