返回 DeepSeek-Reasonix
merge_autocommit_test.go
根目录 / internal / worktree / merge_autocommit_test.go
1 package worktree
2
3 import (
4 "bytes"
5 "context"
6 "os"
7 "path/filepath"
8 "runtime"
9 "strings"
10 "testing"
11 )
12
13 func TestMergeBackRejectsSplitIndexWithoutLosingEitherVersion(t *testing.T) {
14 requireGit(t)
15 repo := initRepo(t)
16 managed := t.TempDir()
17 created, err := Create(context.Background(), repo, managed)
18 if err != nil {
19 t.Fatal(err)
20 }
21 path := filepath.Join(created.WorktreeRoot, "README.md")
22 if err := os.WriteFile(path, []byte("staged version\n"), 0o644); err != nil {
23 t.Fatal(err)
24 }
25 gitTest(t, created.WorktreeRoot, "add", "README.md")
26 if err := os.WriteFile(path, []byte("working version\n"), 0o644); err != nil {
27 t.Fatal(err)
28 }
29 inspection := inspectMergeTest(t, created.WorktreeRoot, managed)
30 _, indexBefore, err := snapshotRealIndex(context.Background(), created.WorktreeRoot)
31 if err != nil {
32 t.Fatal(err)
33 }
34 request := requestFromInspection(inspection)
35 request.AutoCommitDirty = true
36
37 result, err := MergeBack(context.Background(), managed, request)
38 if err == nil || result.Merged || result.RecoveryRequired || !strings.Contains(result.Error, "worktree_index_split") {
39 t.Fatalf("split index merge = %+v, %v", result, err)
40 }
41 if got := gitTest(t, created.WorktreeRoot, "show", ":README.md"); got != "staged version" {
42 t.Fatalf("staged version changed: %q", got)
43 }
44 body, readErr := os.ReadFile(path)
45 if readErr != nil || string(body) != "working version\n" {
46 t.Fatalf("working version changed: %q, %v", body, readErr)
47 }
48 if got := gitTest(t, created.WorktreeRoot, "rev-parse", "HEAD"); got != inspection.WorktreeHead {
49 t.Fatalf("split index advanced HEAD to %s", got)
50 }
51 _, indexAfter, err := snapshotRealIndex(context.Background(), created.WorktreeRoot)
52 if err != nil {
53 t.Fatal(err)
54 }
55 if !bytes.Equal(indexBefore, indexAfter) {
56 t.Fatal("split-index rejection changed the real index bytes")
57 }
58 }
59
60 func TestMergeBackAutoCommitUsesExactTreeWithoutHooks(t *testing.T) {
61 requireGit(t)
62 if runtime.GOOS == "windows" {
63 t.Skip("executable Git hook permissions are not portable to Windows")
64 }
65 repo := initRepo(t)
66 managed := t.TempDir()
67 created, err := Create(context.Background(), repo, managed)
68 if err != nil {
69 t.Fatal(err)
70 }
71 featurePath := filepath.Join(created.WorktreeRoot, "feature.txt")
72 if err := os.WriteFile(featurePath, []byte("feature\n"), 0o644); err != nil {
73 t.Fatal(err)
74 }
75 hookPath := gitTest(t, created.WorktreeRoot, "rev-parse", "--git-path", "hooks/pre-commit")
76 if !filepath.IsAbs(hookPath) {
77 hookPath = filepath.Join(created.WorktreeRoot, hookPath)
78 }
79 marker := filepath.Join(t.TempDir(), "hook-ran")
80 if err := os.WriteFile(hookPath, []byte("#!/bin/sh\nprintf ran > '"+marker+"'\nexit 1\n"), 0o755); err != nil {
81 t.Fatal(err)
82 }
83 inspection := inspectMergeTest(t, created.WorktreeRoot, managed)
84 request := requestFromInspection(inspection)
85 request.AutoCommitDirty = true
86 tempIndexSecured := false
87 mergeStepHook = func(step string) {
88 if step != "after_worktree_add" {
89 return
90 }
91 matches, globErr := filepath.Glob(filepath.Join(filepath.Dir(created.WorktreeRoot), ".reasonix-merge-index-*"))
92 if globErr != nil || len(matches) != 1 {
93 t.Fatalf("temporary index matches = %v, %v", matches, globErr)
94 }
95 info, statErr := os.Stat(matches[0])
96 if statErr != nil || info.Mode().Perm() != 0o600 {
97 t.Fatalf("temporary index mode = %v, %v", info.Mode().Perm(), statErr)
98 }
99 tempIndexSecured = true
100 }
101 t.Cleanup(func() { mergeStepHook = nil })
102 result, err := MergeBack(context.Background(), managed, request)
103 if err != nil || !result.Merged {
104 t.Fatalf("hook-free auto-commit = %+v, %v", result, err)
105 }
106 if _, err := os.Stat(marker); !os.IsNotExist(err) {
107 t.Fatalf("pre-commit hook ran: %v", err)
108 }
109 if !tempIndexSecured {
110 t.Fatal("temporary index permissions were not verified")
111 }
112 parents := strings.Fields(gitTest(t, created.WorktreeRoot, "rev-list", "--parents", "-n", "1", result.WorktreeHead))
113 if len(parents) != 2 || parents[1] != inspection.WorktreeHead {
114 t.Fatalf("auto-commit parents = %v", parents)
115 }
116 if got := gitTest(t, created.WorktreeRoot, "show", result.WorktreeHead+":feature.txt"); got != "feature" {
117 t.Fatalf("auto-commit tree content = %q", got)
118 }
119 }
120
121 func TestMergeBackAutoCommitRejectsSameHeadBranchSwitchBeforeCAS(t *testing.T) {
122 requireGit(t)
123 repo := initRepo(t)
124 managed := t.TempDir()
125 created, err := Create(context.Background(), repo, managed)
126 if err != nil {
127 t.Fatal(err)
128 }
129 if err := os.WriteFile(filepath.Join(created.WorktreeRoot, "feature.txt"), []byte("feature\n"), 0o644); err != nil {
130 t.Fatal(err)
131 }
132 inspection := inspectMergeTest(t, created.WorktreeRoot, managed)
133 request := requestFromInspection(inspection)
134 request.AutoCommitDirty = true
135 mergeStepHook = func(step string) {
136 if step == "before_worktree_ref_transaction" {
137 gitTest(t, created.WorktreeRoot, "switch", "-c", "external-same-head")
138 }
139 }
140 t.Cleanup(func() { mergeStepHook = nil })
141
142 result, err := MergeBack(context.Background(), managed, request)
143 if err == nil || result.Merged || result.RecoveryRequired || !strings.Contains(result.Error, "worktree branch") {
144 t.Fatalf("same-head branch switch = %+v, %v", result, err)
145 }
146 if got := gitTest(t, repo, "rev-parse", "refs/heads/"+inspection.WorktreeBranch); got != inspection.WorktreeHead {
147 t.Fatalf("delivery branch advanced to %s", got)
148 }
149 if got := gitTest(t, created.WorktreeRoot, "symbolic-ref", "--short", "HEAD"); got != "external-same-head" {
150 t.Fatalf("external branch switch was overwritten: %s", got)
151 }
152 }
153
154 func TestMergeBackAutoCommitPreservesIndexMutationAfterRefCAS(t *testing.T) {
155 requireGit(t)
156 repo := initRepo(t)
157 managed := t.TempDir()
158 created, err := Create(context.Background(), repo, managed)
159 if err != nil {
160 t.Fatal(err)
161 }
162 if err := os.WriteFile(filepath.Join(created.WorktreeRoot, "feature.txt"), []byte("feature\n"), 0o644); err != nil {
163 t.Fatal(err)
164 }
165 inspection := inspectMergeTest(t, created.WorktreeRoot, managed)
166 request := requestFromInspection(inspection)
167 request.AutoCommitDirty = true
168 latePath := filepath.Join(created.WorktreeRoot, "late-index.txt")
169 mergeStepHook = func(step string) {
170 if step == "after_worktree_ref_update" {
171 if err := os.WriteFile(latePath, []byte("preserve\n"), 0o644); err != nil {
172 t.Fatal(err)
173 }
174 gitTest(t, created.WorktreeRoot, "add", "late-index.txt")
175 }
176 }
177 t.Cleanup(func() { mergeStepHook = nil })
178
179 result, err := MergeBack(context.Background(), managed, request)
180 if err == nil || result.Merged || !result.RecoveryRequired {
181 t.Fatalf("post-CAS index mutation = %+v, %v", result, err)
182 }
183 if got := gitTest(t, created.WorktreeRoot, "show", ":late-index.txt"); got != "preserve" {
184 t.Fatalf("late staged content was overwritten: %q", got)
185 }
186 if got := gitTest(t, repo, "rev-parse", "HEAD"); got != inspection.TargetHead {
187 t.Fatalf("source target advanced after auto-commit recovery: %s", got)
188 }
189 }
190
190 lines GO