Artifact a4417f65a7b49b9c972ba680208df60c7e600330b27d1b219a7b7700c177807e:
- File
internal/checkout/perm_fossilbin_test.go
— part of check-in
[2d2e62d420]
at
2026-10-18 22:16:06
on branch feat/submanifest-reader
— fix(checkout): scan missing files and mode changes as fossil does
Port the remaining branches of fossil's vfile_check_signature. A missing file is marked changed with mtime -1, a removed file is marked changed, and the permission half sets chnged 6-9 for exec-bit and symlink changes (returning to 0 when undone), honouring the allow-symlinks setting: off, a symlink is seen as what it points to; on, as itself, its content its target's name.
Status reports the mode changes as fossil's changes command labels them (executable, symlink, unexec, unlink). Commit records the permission it finds on disk, l included, with a symlink's target as its content. Where the storage has no file modes (MemStorage) or on Windows, the recorded permissions are kept, as fossil on Windows keeps them. ScanENotFile now refuses a non-file where a tracked file belongs, and merge scans with it as fossil's does. Extract still writes a missing file without Force.
Interop tests run the same edits through fossil changes and go-libfossil status on one checkout, in both orders and with allow-symlinks on and off, and compare a commit's F cards with fossil's.
Tracked in #449 (user: [email protected], size: 6628) [annotate] [blame] [check-ins using] [more...]
//go:build unix package checkout import ( "os" "path/filepath" "strings" "testing" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/testutil" ) // fossilLabel is the label fossil's changes command prints for each change. var fossilLabel = map[FileChange]string{ ChangeAdded: "ADDED", ChangeRemoved: "DELETED", ChangeMissing: "MISSING", ChangeRenamed: "RENAMED", ChangeModified: "EDITED", ChangeExecutable: "EXECUTABLE", ChangeSymlink: "SYMLINK", ChangeUnexec: "UNEXEC", ChangeUnlink: "UNLINK", } // permHistory commits, with fossil and allow-symlinks set to allow, a plain // file, an executable, a symlink and two more plain files, and returns the // repository. func permHistory(t *testing.T, bin, allow string) string { t.Helper() dir := t.TempDir() repoPath := filepath.Join(dir, "r.fossil") ck := filepath.Join(dir, "ck") if err := os.Mkdir(ck, 0o755); err != nil { t.Fatal(err) } runFossilIn(t, bin, dir, "init", repoPath) runFossilIn(t, bin, ck, "open", repoPath) runFossilIn(t, bin, ck, "settings", "allow-symlinks", "on") for name, body := range map[string]string{ "plain.txt": "plain\n", "run.sh": "#!/bin/sh\n", "gone.txt": "gone\n", "tolink.txt": "t\n", } { if err := os.WriteFile(filepath.Join(ck, name), []byte(body), 0o644); err != nil { t.Fatal(err) } } if err := os.Chmod(filepath.Join(ck, "run.sh"), 0o755); err != nil { t.Fatal(err) } if err := os.Symlink("plain.txt", filepath.Join(ck, "link")); err != nil { t.Fatal(err) } runFossilIn(t, bin, ck, "add", ".") runFossilIn(t, bin, ck, "commit", "-m", "v1") runFossilIn(t, bin, ck, "settings", "allow-symlinks", allow) runFossilIn(t, bin, ck, "close", "--force") return repoPath } // permEdits makes every kind of change the scan's permission half sees, and // a missing file, in the checkout at ck. func permEdits(t *testing.T, ck string) { t.Helper() must := func(err error) { t.Helper() if err != nil { t.Fatal(err) } } must(os.Chmod(filepath.Join(ck, "plain.txt"), 0o755)) must(os.Chmod(filepath.Join(ck, "run.sh"), 0o644)) must(os.Remove(filepath.Join(ck, "gone.txt"))) must(os.Remove(filepath.Join(ck, "tolink.txt"))) must(os.Symlink("run.sh", filepath.Join(ck, "tolink.txt"))) must(os.Remove(filepath.Join(ck, "link"))) must(os.WriteFile(filepath.Join(ck, "link"), []byte("plain.txt"), 0o644)) } // fossilChanges parses `fossil changes` into name → label. func fossilChanges(t *testing.T, bin, ck string) map[string]string { t.Helper() out := map[string]string{} for _, line := range strings.Split(runFossilIn(t, bin, ck, "changes"), "\n") { f := strings.Fields(line) if len(f) == 2 { out[f[1]] = f[0] } } return out } // goChanges opens the fossil checkout at ck and reports its status as // fossil's labels. func goChanges(t *testing.T, repoPath, ck string) map[string]string { t.Helper() r, err := repo.Open(repoPath) if err != nil { t.Fatal(err) } defer r.Close() co, err := Open(r, ck, OpenOpts{}) if err != nil { t.Fatal(err) } defer co.Close() out := map[string]string{} for name, change := range statusOf(t, co) { out[name] = fossilLabel[change] } return out } func sameLabels(t *testing.T, what string, got, want map[string]string) { t.Helper() if len(got) != len(want) { t.Errorf("%s: %v, fossil %v", what, got, want) return } for name, label := range want { if got[name] != label { t.Errorf("%s: %s %q, fossil %q", what, name, got[name], label) } } } // The same edits on the same checkout are reported identically by fossil's // changes command and go-libfossil's status, whichever scans first, with // allow-symlinks on and off. func TestModeChangesMatchFossil(t *testing.T) { bin := testutil.RequireFossilBin(t) for _, allow := range []string{"on", "off"} { repoPath := permHistory(t, bin, allow) fossilFirst := t.TempDir() runFossilIn(t, bin, fossilFirst, "open", repoPath) permEdits(t, fossilFirst) want := fossilChanges(t, bin, fossilFirst) sameLabels(t, "allow-symlinks "+allow+", go after fossil", goChanges(t, repoPath, fossilFirst), want) goFirst := t.TempDir() runFossilIn(t, bin, goFirst, "open", repoPath) permEdits(t, goFirst) got := goChanges(t, repoPath, goFirst) sameLabels(t, "allow-symlinks "+allow+", go before fossil", got, want) sameLabels(t, "allow-symlinks "+allow+", fossil after go", fossilChanges(t, bin, goFirst), want) if len(want) < 5 { t.Errorf("allow-symlinks %s: fossil reports %v; the test's premise is wrong", allow, want) } } } // fCards returns the F cards of the check-in uuid, by file name. func fCards(t *testing.T, bin, repoPath, uuid string) map[string]string { t.Helper() out := map[string]string{} art := runFossilIn(t, bin, filepath.Dir(repoPath), "artifact", "-R", repoPath, uuid) for _, line := range strings.Split(art, "\n") { if f := strings.Fields(line); len(f) >= 3 && f[0] == "F" { out[f[1]] = line } } return out } // A commit carrying exec and symlink changes records the same F cards // whether fossil or go-libfossil makes it. func TestModeCommitMatchesFossil(t *testing.T) { bin := testutil.RequireFossilBin(t) repoPath := permHistory(t, bin, "on") goRepo := filepath.Join(t.TempDir(), "g.fossil") data, err := os.ReadFile(repoPath) if err != nil { t.Fatal(err) } if err := os.WriteFile(goRepo, data, 0o644); err != nil { t.Fatal(err) } edit := func(ck string) { permEdits(t, ck) if err := os.WriteFile(filepath.Join(ck, "gone.txt"), []byte("gone\n"), 0o644); err != nil { t.Fatal(err) } } fossilCk := t.TempDir() runFossilIn(t, bin, fossilCk, "open", repoPath) edit(fossilCk) runFossilIn(t, bin, fossilCk, "commit", "-m", "modes") tip := strings.Fields(runFossilIn(t, bin, fossilCk, "sql", "SELECT uuid FROM blob WHERE rid=(SELECT value FROM vvar WHERE name='checkout')")) want := fCards(t, bin, repoPath, tip[0]) goCk := t.TempDir() runFossilIn(t, bin, goCk, "open", goRepo) edit(goCk) r, err := repo.Open(goRepo) if err != nil { t.Fatal(err) } co, err := Open(r, goCk, OpenOpts{}) if err != nil { r.Close() t.Fatal(err) } _, uuid, err := co.Commit(CommitOpts{Message: "modes", User: "test"}) co.Close() r.Close() if err != nil { t.Fatal(err) } got := fCards(t, bin, goRepo, uuid) if len(got) != len(want) { t.Fatalf("F cards %v, fossil %v", got, want) } for name, card := range want { if got[name] != card { t.Errorf("%s: %q, fossil %q", name, got[name], card) } } if out := fossilChanges(t, bin, goCk); len(out) != 0 { t.Errorf("fossil changes after go commit: %v, want nothing", out) } }