1package repo_test
2
3import (
4 "os"
5 "path/filepath"
6 "testing"
7
8 "arche/internal/diff"
9 "arche/internal/object"
10 "arche/internal/repo"
11 "arche/internal/store"
12 "arche/internal/wc"
13
14 _ "github.com/mattn/go-sqlite3"
15)
16
17func initTestRepo(t *testing.T) *repo.Repo {
18 t.Helper()
19 dir := t.TempDir()
20 r, err := repo.Init(dir)
21 if err != nil {
22 t.Fatalf("repo.Init: %v", err)
23 }
24 t.Cleanup(func() { r.Close() })
25 return r
26}
27
28func writeFile(t *testing.T, r *repo.Repo, name, content string) string {
29 t.Helper()
30 abs := filepath.Join(r.Root, name)
31 if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil {
32 t.Fatalf("MkdirAll: %v", err)
33 }
34 if err := os.WriteFile(abs, []byte(content), 0o644); err != nil {
35 t.Fatalf("WriteFile %s: %v", name, err)
36 }
37 return name
38}
39
40func TestInit_HeadSet(t *testing.T) {
41 r := initTestRepo(t)
42 head, err := r.Head()
43 if err != nil {
44 t.Fatalf("Head: %v", err)
45 }
46 if len(head) == 0 {
47 t.Error("HEAD is empty after init")
48 }
49 if head[:3] != "ch:" {
50 t.Errorf("HEAD should start with ch:, got %q", head)
51 }
52}
53
54func TestInit_InitialCommitExists(t *testing.T) {
55 r := initTestRepo(t)
56 c, id, err := r.HeadCommit()
57 if err != nil {
58 t.Fatalf("HeadCommit: %v", err)
59 }
60 if id == object.ZeroID {
61 t.Error("initial commit has zero ID")
62 }
63 if c.Phase != object.PhaseDraft {
64 t.Errorf("initial commit phase: got %v, want draft", c.Phase)
65 }
66}
67
68func TestInit_NotARepo(t *testing.T) {
69 dir := t.TempDir()
70 _, err := repo.Open(filepath.Join(dir, "nonexistent"))
71 if err == nil {
72 t.Error("expected error opening non-repo dir, got nil")
73 }
74}
75
76func TestSnap_EmptyRepo(t *testing.T) {
77 r := initTestRepo(t)
78 w := wc.New(r)
79 _, id, err := w.Snap("first snap")
80 if err != nil {
81 t.Fatalf("Snap: %v", err)
82 }
83 if id == object.ZeroID {
84 t.Error("snapped commit has zero ID")
85 }
86 head, err := r.Head()
87 if err != nil {
88 t.Fatalf("Head: %v", err)
89 }
90 if head[:3] != "ch:" {
91 t.Errorf("HEAD after snap: %q", head)
92 }
93}
94
95func TestSnap_WithFiles(t *testing.T) {
96 r := initTestRepo(t)
97 writeFile(t, r, "hello.txt", "Hello, Arche!\n")
98 writeFile(t, r, "src/main.go", "package main\n")
99 w := wc.New(r)
100 snapped, snapID, err := w.Snap("add files")
101 if err != nil {
102 t.Fatalf("Snap: %v", err)
103 }
104 if snapped.Message != "add files" {
105 t.Errorf("Message: got %q, want %q", snapped.Message, "add files")
106 }
107 tree, err := r.ReadTree(snapped.TreeID)
108 if err != nil {
109 t.Fatalf("ReadTree: %v", err)
110 }
111 if len(tree.Entries) == 0 {
112 t.Error("snapped tree is empty")
113 }
114 _, snapID2, err := w.Snap("no change snap")
115 if err != nil {
116 t.Fatalf("second Snap: %v", err)
117 }
118 if snapID2 == snapID {
119 t.Error("two snaps should produce distinct draft commit IDs")
120 }
121}
122
123func TestSnap_FilesStoredInTree(t *testing.T) {
124 r := initTestRepo(t)
125 writeFile(t, r, "README.md", "# Project\n")
126 writeFile(t, r, "lib/util.go", "package lib\n")
127 w := wc.New(r)
128 snapped, _, err := w.Snap("files snap")
129 if err != nil {
130 t.Fatalf("Snap: %v", err)
131 }
132 tree, err := r.ReadTree(snapped.TreeID)
133 if err != nil {
134 t.Fatalf("ReadTree: %v", err)
135 }
136 names := map[string]bool{}
137 for _, e := range tree.Entries {
138 names[e.Name] = true
139 }
140 if !names["README.md"] && !names["lib"] {
141 t.Errorf("tree entries: %v", names)
142 }
143}
144
145func TestStatus_Added(t *testing.T) {
146 r := initTestRepo(t)
147 w := wc.New(r)
148 if _, _, err := w.Snap("base"); err != nil {
149 t.Fatalf("initial snap: %v", err)
150 }
151 writeFile(t, r, "new.txt", "new file")
152 changes, err := w.Status()
153 if err != nil {
154 t.Fatalf("Status: %v", err)
155 }
156 var found bool
157 for _, c := range changes {
158 if c.Path == "new.txt" && c.Status == 'A' {
159 found = true
160 }
161 }
162 if !found {
163 t.Errorf("expected new.txt as Added, got: %v", changes)
164 }
165}
166
167func TestStatus_Clean(t *testing.T) {
168 r := initTestRepo(t)
169 writeFile(t, r, "file.txt", "content")
170 w := wc.New(r)
171 if _, _, err := w.Snap("snap"); err != nil {
172 t.Fatalf("Snap: %v", err)
173 }
174 _, err := w.Status()
175 if err != nil {
176 t.Fatalf("Status: %v", err)
177 }
178}
179
180func TestBookmarks(t *testing.T) {
181 r := initTestRepo(t)
182 _, commitID, err := r.HeadCommit()
183 if err != nil {
184 t.Fatalf("HeadCommit: %v", err)
185 }
186 tx, err := r.Store.Begin()
187 if err != nil {
188 t.Fatalf("Begin: %v", err)
189 }
190 err = r.Store.SetBookmark(tx, store.Bookmark{Name: "main", CommitID: commitID})
191 if err != nil {
192 r.Store.Rollback(tx)
193 t.Fatalf("SetBookmark: %v", err)
194 }
195 if err := r.Store.Commit(tx); err != nil {
196 t.Fatalf("Commit: %v", err)
197 }
198 bm, err := r.Store.GetBookmark("main")
199 if err != nil {
200 t.Fatalf("GetBookmark: %v", err)
201 }
202 if bm == nil {
203 t.Fatal("bookmark not found")
204 }
205 if bm.CommitID != commitID {
206 t.Errorf("CommitID: got %x, want %x", bm.CommitID[:6], commitID[:6])
207 }
208 bms, err := r.Store.ListBookmarks()
209 if err != nil {
210 t.Fatalf("ListBookmarks: %v", err)
211 }
212 if len(bms) != 1 || bms[0].Name != "main" {
213 t.Errorf("unexpected bookmarks: %v", bms)
214 }
215 tx2, _ := r.Store.Begin()
216 r.Store.DeleteBookmark(tx2, "main")
217 r.Store.Commit(tx2)
218 bm2, _ := r.Store.GetBookmark("main")
219 if bm2 != nil {
220 t.Error("bookmark should be deleted")
221 }
222}
223
224func TestPhaseDefaultDraft(t *testing.T) {
225 r := initTestRepo(t)
226 _, commitID, err := r.HeadCommit()
227 if err != nil {
228 t.Fatalf("HeadCommit: %v", err)
229 }
230 phase, err := r.Store.GetPhase(commitID)
231 if err != nil {
232 t.Fatalf("GetPhase: %v", err)
233 }
234 if phase != object.PhaseDraft {
235 t.Errorf("default phase: got %v, want draft", phase)
236 }
237}
238
239func TestSetPhase(t *testing.T) {
240 r := initTestRepo(t)
241 _, commitID, err := r.HeadCommit()
242 if err != nil {
243 t.Fatalf("HeadCommit: %v", err)
244 }
245 tx, _ := r.Store.Begin()
246 if err := r.Store.SetPhase(tx, commitID, object.PhasePublic); err != nil {
247 r.Store.Rollback(tx)
248 t.Fatalf("SetPhase: %v", err)
249 }
250 r.Store.Commit(tx)
251 phase, err := r.Store.GetPhase(commitID)
252 if err != nil {
253 t.Fatalf("GetPhase: %v", err)
254 }
255 if phase != object.PhasePublic {
256 t.Errorf("phase: got %v, want public", phase)
257 }
258}
259
260func TestOperationLog(t *testing.T) {
261 r := initTestRepo(t)
262 ops, err := r.Store.ListOperations(10)
263 if err != nil {
264 t.Fatalf("ListOperations: %v", err)
265 }
266 if len(ops) == 0 {
267 t.Error("no operations after init")
268 }
269 writeFile(t, r, "a.txt", "a")
270 w := wc.New(r)
271 if _, _, err := w.Snap("snap"); err != nil {
272 t.Fatalf("Snap: %v", err)
273 }
274 ops2, err := r.Store.ListOperations(10)
275 if err != nil {
276 t.Fatalf("ListOperations: %v", err)
277 }
278 if len(ops2) <= len(ops) {
279 t.Errorf("snap should add operation; before=%d after=%d", len(ops), len(ops2))
280 }
281}
282
283func TestTreeDiff_NoChanges(t *testing.T) {
284 r := initTestRepo(t)
285 writeFile(t, r, "file.txt", "content\n")
286 w := wc.New(r)
287 snapped, _, err := w.Snap("snap")
288 if err != nil {
289 t.Fatalf("Snap: %v", err)
290 }
291 diffs, err := diff.TreeDiff(r, snapped.TreeID, snapped.TreeID)
292 if err != nil {
293 t.Fatalf("TreeDiff: %v", err)
294 }
295 if len(diffs) != 0 {
296 t.Errorf("same tree should produce no diffs, got %v", diffs)
297 }
298}
299
300func TestTreeDiff_AddedFile(t *testing.T) {
301 r := initTestRepo(t)
302 w := wc.New(r)
303 snap1, _, err := w.Snap("empty")
304 if err != nil {
305 t.Fatalf("first Snap: %v", err)
306 }
307 writeFile(t, r, "new.go", "package main\n")
308 snap2, _, err := w.Snap("add file")
309 if err != nil {
310 t.Fatalf("second Snap: %v", err)
311 }
312 diffs, err := diff.TreeDiff(r, snap1.TreeID, snap2.TreeID)
313 if err != nil {
314 t.Fatalf("TreeDiff: %v", err)
315 }
316 var found bool
317 for _, d := range diffs {
318 if d.Path == "new.go" && d.Status == 'A' {
319 found = true
320 }
321 }
322 if !found {
323 t.Errorf("expected new.go as Added, got: %v", diffs)
324 }
325}
326
327func TestCommitDiff_RootCommit(t *testing.T) {
328 r := initTestRepo(t)
329 writeFile(t, r, "main.go", "package main\n")
330 w := wc.New(r)
331 _, snapID, err := w.Snap("first commit")
332 if err != nil {
333 t.Fatalf("Snap: %v", err)
334 }
335 diffs, err := diff.CommitDiff(r, snapID)
336 if err != nil {
337 t.Fatalf("CommitDiff: %v", err)
338 }
339 for _, d := range diffs {
340 if d.Status != 'A' {
341 t.Errorf("root commit diff should only have Added files, got status=%c path=%s", d.Status, d.Path)
342 }
343 }
344}