Artifact 0451e9079c49398d3ea562f5ab27c2d49adf7effd011566837b8b6dd1b53bad2:
- File
internal/vfile/vfile_test.go
— part of check-in
[f55d0ed2de]
at
2026-10-03 16:47:05
on branch trunk
— refactor(vfile): give the checkout's vfile encoding one owner (#233) (#243)
* refactor(vfile): give the checkout's vfile encoding one owner (#233)
Checkin, stash and status each decoded fossil's vfile columns on their own: rid=0 for an add, origname==pathname on unrenamed rows, chnged merge codes. That is how #228 happened, and stash still had it wrong: it selected chnged=1 only, so files changed by a merge (chnged 2..9) were silently left out, and it stashed a rename as a plain edit.
New package internal/vfile owns the encoding. Load reads a version's rows; Row answers what callers ask (IsAdded, IsRemoved, RenamedFrom, ContentChanged, PendingMerge); Classify gives the change type in fossil's order. checkout.FileChange is now an alias of vfile.Change, so the checkout API is unchanged.
internal/checkout's status, Extract safety check and checkin read rows through it. The never-read vfileCommitEntry.changed field is gone.
internal/stash selects changes with vfile.Classify against the checked- out version and refuses a pending merge or rename by name, since its save and revert paths restore files by current name against the committed version and would drop either. Its store-and-revert loop is split into stashRecord and revertStashedFile; removed and modified files share one revert path, keyed by row id.
* fix(vfile): a pending merge is chnged 2-5 or mrid != rid (#233)
PendingMerge counted every chnged above 1 as a merge, but 6 through 9 record a change to the executable or symlink bit; fossil leaves chnged=6 after a plain chmod +x. Stash then refused an ordinary permission change as "a pending merge". It also ignored mrid: an edit made on top of a merge drops chnged back to 1 while mrid still names the merged-in content, so the refusal could be bypassed and the merge half-dropped.
Row now carries mrid; PendingMerge is chnged 2..5 or mrid != rid, and ModeChanged covers 6..9. Stash refuses a permission change with that reason, since its revert writes default permissions.
Load reads a NULL rid as 0 and a NULL mrid as rid, the tolerance the checkin code had when it scanned rid into a NullInt64.
* refactor(checkout): ask vfile.Row about merges in Extract's check (#233)
Extract's safety check relaxed only rows with chnged == 1, decoding the column itself. It now asks the row (not a pending merge, not a mode change), keeping fossil's encoding in internal/vfile. Equivalent for every real row. nextChangedState's comment no longer calls the mode changes (6..9) merge states. (user: [email protected], size: 4236) [annotate] [blame] [check-ins using]
package vfile import ( "database/sql" "testing" "github.com/danmestas/go-libfossil/db" _ "github.com/danmestas/go-libfossil/internal/testdriver" ) // TestClassify pins each row shape the fossil binary and go-libfossil write // to its change type, in fossil's priority order. func TestClassify(t *testing.T) { renamed := sql.NullString{String: "old.txt", Valid: true} same := sql.NullString{String: "f.txt", Valid: true} cases := []struct { name string row Row missing bool want Change }{ {"unchanged", Row{Pathname: "f.txt", RID: 2}, false, ChangeNone}, {"origname equals pathname", Row{Pathname: "f.txt", RID: 2, Origname: same}, false, ChangeNone}, {"fossil add, chnged 0", Row{Pathname: "f.txt"}, false, ChangeAdded}, {"edit", Row{Pathname: "f.txt", RID: 2, Chnged: 1}, false, ChangeModified}, {"merge outcome", Row{Pathname: "f.txt", RID: 2, Chnged: 2}, false, ChangeModified}, {"rename", Row{Pathname: "f.txt", RID: 2, Origname: renamed}, false, ChangeRenamed}, {"rename and edit", Row{Pathname: "f.txt", RID: 2, Origname: renamed, Chnged: 1}, false, ChangeRenamed}, {"removed beats missing", Row{Pathname: "f.txt", RID: 2, Deleted: 1}, true, ChangeRemoved}, {"missing beats added", Row{Pathname: "f.txt"}, true, ChangeMissing}, {"missing beats edit", Row{Pathname: "f.txt", RID: 2, Chnged: 1}, true, ChangeMissing}, } for _, tc := range cases { if got := Classify(tc.row, tc.missing); got != tc.want { t.Errorf("%s: Classify = %v, want %v", tc.name, got, tc.want) } } } // TestRowQuestions pins the predicates callers use instead of reading // columns. func TestRowQuestions(t *testing.T) { add := Row{Pathname: "a"} if !add.IsAdded() || !add.ContentChanged() { t.Error("an add (rid=0, chnged=0) must be added with changed content") } merged := Row{Pathname: "m", RID: 2, MRID: 2, Chnged: 4} if !merged.PendingMerge() || !merged.ContentChanged() { t.Error("chnged=4 must be a pending merge with changed content") } edit := Row{Pathname: "e", RID: 2, MRID: 2, Chnged: 1} if edit.PendingMerge() { t.Error("a plain edit is not a pending merge") } editedMerge := Row{Pathname: "e", RID: 2, MRID: 5, Chnged: 1} if !editedMerge.PendingMerge() { t.Error("an edit on top of merged-in content (mrid != rid) is a pending merge") } for chnged := int64(6); chnged <= 9; chnged++ { mode := Row{Pathname: "x", RID: 2, MRID: 2, Chnged: chnged} if mode.PendingMerge() { t.Errorf("chnged=%d is a mode change, not a merge", chnged) } if !mode.ModeChanged() { t.Errorf("chnged=%d must be a mode change", chnged) } } unrenamed := Row{Pathname: "r", Origname: sql.NullString{Valid: true, String: "r"}} if unrenamed.RenamedFrom() != "" { t.Error("origname equal to pathname is not a rename") } } // TestLoadOrdersByPathnameAndFiltersVersion pins Load against a real vfile // table: only the requested version's rows, sorted by pathname, with NULL // origname read as invalid. func TestLoadOrdersByPathnameAndFiltersVersion(t *testing.T) { d, err := db.OpenSQL(t.TempDir()+"/ck.db", db.OpenConfig{}, nil) if err != nil { t.Fatal(err) } t.Cleanup(func() { if err := d.Close(); err != nil { t.Errorf("close: %v", err) } }) for _, stmt := range []string{ `CREATE TABLE vfile(id INTEGER PRIMARY KEY, vid INTEGER, chnged INT DEFAULT 0, deleted BOOLEAN DEFAULT 0, isexe BOOLEAN, islink BOOLEAN, rid INTEGER, mrid INTEGER, mtime INTEGER, pathname TEXT, origname TEXT, mhash TEXT)`, `INSERT INTO vfile(vid, pathname, rid, isexe, islink) VALUES(7, 'b.txt', 3, 0, 0)`, `INSERT INTO vfile(vid, pathname, rid, isexe, islink) VALUES(7, 'a.txt', 2, 1, 0)`, `INSERT INTO vfile(vid, pathname, rid, isexe, islink) VALUES(8, 'c.txt', 4, 0, 0)`, } { if _, err := d.Exec(stmt); err != nil { t.Fatal(err) } } rows, err := Load(d, 7) if err != nil { t.Fatal(err) } if len(rows) != 2 { t.Fatalf("Load returned %d rows, want 2", len(rows)) } if rows[0].Pathname != "a.txt" || rows[1].Pathname != "b.txt" { t.Fatalf("Load order = %s, %s; want a.txt, b.txt", rows[0].Pathname, rows[1].Pathname) } if rows[0].IsExe != 1 || rows[0].Origname.Valid { t.Fatalf("a.txt = %+v, want isexe 1 and NULL origname", rows[0]) } }