| 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 |