| 1 | package worktree |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "runtime" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | ) |
| 11 | |
| 12 | // What a worktree gains after the host created it — a nested repository with |
| 13 | // its own local filter, a .git file naming another git dir — never runs, and |
| 14 | // the merge still reads the identities pinned at creation. |
| 15 | |
| 16 | func markerPayload(t *testing.T) (marker, payload string) { |
| 17 | t.Helper() |
| 18 | marker = filepath.Join(t.TempDir(), "executed") |
| 19 | payload = filepath.Join(t.TempDir(), "payload.sh") |
| 20 | if err := os.WriteFile(payload, []byte("#!/bin/sh\necho ran >> '"+marker+"'\ncat\n"), 0o755); err != nil { |
| 21 | t.Fatal(err) |
| 22 | } |
| 23 | return marker, payload |
| 24 | } |
| 25 | |
| 26 | func requireMarkerAbsent(t *testing.T, marker, when string) { |
| 27 | t.Helper() |
| 28 | if _, err := os.Stat(marker); err == nil { |
| 29 | t.Fatalf("%s ran a repository-configured filter", when) |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | func requirePOSIXPayload(t *testing.T) { |
| 34 | t.Helper() |
| 35 | requireGit(t) |
| 36 | if runtime.GOOS == "windows" { |
| 37 | t.Skip("payload script is POSIX shell") |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | func TestMergeBackAutoCommitDoesNotEnterGitlinkRepository(t *testing.T) { |
| 42 | requirePOSIXPayload(t) |
| 43 | repo := initRepo(t) |
| 44 | managed := t.TempDir() |
| 45 | created, err := Create(context.Background(), opened(t, repo), managed) |
| 46 | if err != nil { |
| 47 | t.Fatal(err) |
| 48 | } |
| 49 | wt := created.WorktreeRoot |
| 50 | sub := filepath.Join(wt, "sub") |
| 51 | if err := os.MkdirAll(sub, 0o755); err != nil { |
| 52 | t.Fatal(err) |
| 53 | } |
| 54 | marker, payload := markerPayload(t) |
| 55 | gitTest(t, sub, "init", "-q") |
| 56 | gitCommitFile(t, sub, ".gitattributes", "f.txt filter=pwn\n", "attributes") |
| 57 | gitCommitFile(t, sub, "f.txt", "a\n", "nested") |
| 58 | gitTest(t, sub, "config", "filter.pwn.clean", payload) |
| 59 | gitTest(t, sub, "config", "filter.pwn.smudge", payload) |
| 60 | gitlink := gitTest(t, sub, "rev-parse", "HEAD") |
| 61 | gitTest(t, wt, "update-index", "--add", "--cacheinfo", "160000,"+gitlink+",sub") |
| 62 | gitTest(t, wt, "commit", "-q", "-m", "gitlink") |
| 63 | if err := os.WriteFile(filepath.Join(sub, "f.txt"), []byte("b\n"), 0o644); err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | if err := os.WriteFile(filepath.Join(wt, "feature.go"), []byte("package feature\n"), 0o644); err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | |
| 70 | inspection := inspectMergeTest(t, created.WorkspaceRoot, managed) |
| 71 | requireMarkerAbsent(t, marker, "InspectMerge") |
| 72 | request := requestFromInspection(inspection) |
| 73 | request.AutoCommitDirty = true |
| 74 | result, err := MergeBack(context.Background(), managed, request) |
| 75 | if err != nil || !result.Merged { |
| 76 | t.Fatalf("MergeBack = %+v, %v", result, err) |
| 77 | } |
| 78 | requireMarkerAbsent(t, marker, "MergeBack") |
| 79 | tree := gitTest(t, repo, "ls-tree", "-r", result.MergedCommit) |
| 80 | for _, want := range []string{"160000 commit " + gitlink + "\tsub", "\tfeature.go", "\tREADME.md"} { |
| 81 | if !strings.Contains(tree, want) { |
| 82 | t.Fatalf("merged tree:\n%s\nwant %q", tree, want) |
| 83 | } |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | // The worktree's own .git file is the agent's to write; the merge reads the |
| 88 | // git dir recorded when the host added the worktree. |
| 89 | func TestMergeBackKeepsWorktreeIdentityPinnedAtCreation(t *testing.T) { |
| 90 | requirePOSIXPayload(t) |
| 91 | repo := initRepo(t) |
| 92 | managed := t.TempDir() |
| 93 | created, err := Create(context.Background(), opened(t, repo), managed) |
| 94 | if err != nil { |
| 95 | t.Fatal(err) |
| 96 | } |
| 97 | wt := created.WorktreeRoot |
| 98 | marker, payload := markerPayload(t) |
| 99 | evil := filepath.Join(t.TempDir(), "evil") |
| 100 | gitTest(t, repo, "clone", "-q", "--bare", repo, evil) |
| 101 | cfg, err := os.OpenFile(filepath.Join(evil, "config"), os.O_APPEND|os.O_WRONLY, 0) |
| 102 | if err != nil { |
| 103 | t.Fatal(err) |
| 104 | } |
| 105 | _, _ = cfg.WriteString("[probe]\n\tx = substitute\n[filter \"pwn\"]\n\tclean = " + payload + "\n\tsmudge = " + payload + "\n") |
| 106 | _ = cfg.Close() |
| 107 | if err := os.WriteFile(filepath.Join(wt, ".git"), []byte("gitdir: "+evil+"\n"), 0o644); err != nil { |
| 108 | t.Fatal(err) |
| 109 | } |
| 110 | if got := gitTest(t, wt, "config", "--get", "probe.x"); got != "substitute" { |
| 111 | t.Fatalf("discovery in the worktree read %q, want the substitute (the case being guarded)", got) |
| 112 | } |
| 113 | if err := os.WriteFile(filepath.Join(wt, ".gitattributes"), []byte("*.go filter=pwn\n"), 0o644); err != nil { |
| 114 | t.Fatal(err) |
| 115 | } |
| 116 | if err := os.WriteFile(filepath.Join(wt, "feature.go"), []byte("package feature\n"), 0o644); err != nil { |
| 117 | t.Fatal(err) |
| 118 | } |
| 119 | |
| 120 | inspection := inspectMergeTest(t, created.WorkspaceRoot, managed) |
| 121 | if inspection.WorktreeBranch != created.Branch { |
| 122 | t.Fatalf("inspection branch = %q, want %q", inspection.WorktreeBranch, created.Branch) |
| 123 | } |
| 124 | request := requestFromInspection(inspection) |
| 125 | request.AutoCommitDirty = true |
| 126 | result, err := MergeBack(context.Background(), managed, request) |
| 127 | if err != nil || !result.Merged { |
| 128 | t.Fatalf("MergeBack = %+v, %v", result, err) |
| 129 | } |
| 130 | requireMarkerAbsent(t, marker, "MergeBack") |
| 131 | if _, err := os.Stat(filepath.Join(repo, "feature.go")); err != nil { |
| 132 | t.Fatalf("merged file missing from source: %v", err) |
| 133 | } |
| 134 | } |
| 135 |