Skip to content
go-libfossil Timeline
Login

Timeline

50 descendants of a71bf662ee3b87b7

2026-10-19
22:48
test(manifest): cover ticket schemas without fossil's optional columns

Fossil treats the ticket table, ticket.tkt_ctime, the ticketchng table, ticketchng.tkt_rid and ticketchng.tkt_user as optional. Each schema here must derive the rows fossil rebuild derives; all fail today. check-in: 40a0ba9626 user: [email protected] tags: fix/ticket-optional-columns, main, trunk

22:07
fix(manifest): propagate tags in the transaction that links the batch

Each link transaction (a sweep batch, a receive transaction, a retried waiter) now reparents and propagates from the parents it wrote plink rows for before it commits, as fossil's crosslink propagates from a check-in's parent in the transaction that links it. A link and the propagation it owes commit or roll back together, so a failed gate or an interrupted process can no longer leave a committed plink edge whose parent's tags never reached the child. The closing gate still applies the batch's leaf rechecks once, runs the reparent pass for parent tags no link transaction reached, and propagates from any parent left by a link outside a link transaction.

Tracked in #432. check-in: e9eeab7e17 user: [email protected] tags: main, perf/tag-repair-batch, trunk

21:50
fix(sync): gate sub-manifest trees on the send-submanifests pragma

draft-fossil-submanifests-00 §7.2 no longer gates S-bearing artifacts on the peer's announced version: fossil 2.29 announces 22900 and reads no tree. A peer that reads trees now says so with pragma send-submanifests, which go-libfossil sends in every request right after client-version and in every reply, clone replies included, right after server-version, whatever the sub-manifest settings. The version threshold and the 22900 reader marker are gone; client-version and server-version announce 22800, the fossil release whose protocol this library speaks.

A tree manifest or sub-manifest is refused to a peer that has not sent the pragma with "Peer does not accept sub-manifests.", never converted or omitted. A server decides from the whole request it answers, a client from the replies it has received in the exchange. avoid-submanifests moves from the ci-lock answer into the same leading run of every reply.

Enabling emission over a stored submanifest-k outside 1 to 12 without an explicit k is refused, naming the stored value (§8.1).

The conformance suite pins the draft with the 2026-10-19 errata applied: SM-7.2-1 and SM-6.3-3 are met rather than deviations, SM-7.2-2, SM-7.2-3, SM-6.3-1/2/7 and SM-8.1-3 follow the corrected text, and SM-7.2-4 covers the pragma's placement. The zit interop pushes a tree go-libfossil wrote to zit and checks both sides refuse trees when the pragma is stripped.

Tracked in #452. check-in: 05a53c393d user: [email protected] tags: feat/submanifest-reader, main, trunk

21:17
fix(verify): report stray leaf rows and read every parent tag

Review follow-ups for the leaf audit:

- leaf.Audit reports a row for a rid that no recheck reaches, that is a rid with no branch row in no plink row. A file artifact or a rid with no artifact used to pass. - Verify collects the parents named by every parent tag, as well as by P-cards, whose link plink no longer holds. A parent that an earlier, since replaced, reparent linked is then excused. repostate.7.3.reparent-twice-no-recheck covers that order. - The stale finding excuses a child carrying a parent tag row of any type, since a cancelled reparent leaves its link in place until a rebuild. - Every leaf-table case also runs Verify on the tables fossil(1) derives, per artifact and rebuilt, except where the case records fossil departing from the draft.

Tracked in #433. check-in: 41e39a41fc user: [email protected] tags: fix/verify-leaf-rule, main, trunk

20:40
perf(manifest): skip a reparent whose plink rows are already settled

Each phantom-fill cascade batch now runs the repair gate, so the reparent pass runs once per batch rather than once per sync. A check-in whose plink rows already name the parent tag's parents, each a linked check-in, is no longer re-expanded and rewritten, and its parent is not propagated from again. A rewrite against a parent that is not linked yet is still redone at every gate until it is.

Tracked in #432. check-in: 4701ed89a2 user: [email protected] tags: main, perf/tag-repair-batch, trunk

20:36
test(cli): cover log across a rename; order exit statuses in the docs

Tracked in #451 check-in: e9b2985d62 user: [email protected] tags: feat/cli-snapshot, main, trunk

20:20
perf(manifest): bound the repair gate's tag propagation by the batch

repairTagPropagation re-propagated tags from every primary parent in the repository at every gate, so the gate at the end of a sweep, clone or sync cost the repository, not the batch. The batch now records the primary parents it writes plink rows for, and the new parents of a reparent, in a set beside its leaf schedule; the set is marked, rolled back and reset with the schedule, and the gate propagates from it alone. A rebuild links every check-in, so its set is every primary parent and nothing changes there.

A phantom-fill cascade's parents are known only to the cascade, so each cascade batch runs the gate itself before it commits rather than leaving propagation to the end of the sync.

Tracked in #432. check-in: 08820292b8 user: [email protected] tags: main, perf/tag-repair-batch, trunk

20:17
feat(cli): checkout-free snapshot, per-path log, cat --at and diff --json

repo snapshot commits one file's bytes onto trunk with no checkout, a no-op when they equal the tip. repo log --path lists the check-ins that changed a path, repo cat --at reads a file as a version holds it, and repo diff --json A B [PATH] diffs two versions. Exit 2 means not found. A snapshot re-checks the check-in stamp inside its write transaction and retries, so concurrent processes build one linear trunk.

Tracked in #451 check-in: beed361c03 user: [email protected] tags: feat/cli-snapshot, main, trunk

20:12
fix(verify): check the leaf table by the same-branch rule

verify.Verify expected a leaf for every check-in with no child at all, so it reported merged branch tips and branch points whose children are on other branches, and the arrival-order states repo-state Section 7.3 allows. Verify no longer recomputes one expected table. leaf.Audit, which applies the Section 7.2 test leaf.Check uses, reports a row whose same-branch child carries a branch row written once it was linked, and a check-in carrying a branch row with no same-branch child and no row, excusing the parents checkPlink finds a reparent took a child from.

Every conformance case comparing the leaf table now also runs Verify on it, and repostate.7.2.verify-leaf-table checks that Verify reports a dropped and a planted row.

Tracked in #433. check-in: c6db63319e user: [email protected] tags: fix/verify-leaf-rule, main, trunk

20:09
fix(checkout): revert renames onto taken names, saving undo first

Checkout.Revert refused to undo a rename when another file held the old name: a swap, a chain, a new add or an untracked file there. Like fossil's revert it now saves each file it writes or deletes for undo, then reverts in one transaction, so Undo or fossil undo brings back what it overwrote, and a failed revert puts the files back.

Undoing a rename takes back the old name from whatever row holds it: a row renamed onto it is reverted to its own old name, and any other row is dropped, as fossil's UPDATE OR REPLACE drops it. Every committed file ends under its committed name; fossil's own revert leaves a swap renamed and stops tracking the second file of a chain.

Tracked in #248. check-in: 80764da743 user: [email protected] tags: feat/submanifest-reader, main, trunk

18:49
fix(checkout): Update decides the cone before renames, and never merges symlinks

A file outside a sparse checkout's cone is never on disk, so it read as missing, hence changed, and followed the target's rename; any upstream rename outside the cone then refused the update. It is now classified before renames are followed and matched by name: a rename outside the cone moves only the record, one into the cone writes the file under its new name, and an unedited file renamed out of it leaves the disk.

A symlink edited locally and changed by the target was merged through the link, writing conflict markers into the file it points at. As fossil's update does, it is now a conflict with nothing written, listed in UpdateResult.Unmerged; a rename still removes the old name. check-in: f373c9a8ae user: [email protected] tags: feat/submanifest-reader, main, trunk

18:34
fix(checkout): materialize a symlink entering the cone as a symlink

The cones merge called extractSingleFile without the islink argument the symlink fix added, so it did not build. SetCone now passes the row's islink, and a file entering the cone is written as Extract writes it: a symlink with allow-symlinks on. check-in: 36fccc1f87 user: [email protected] tags: feat/submanifest-reader, main, trunk

18:29
Merge branch 'feat/cones' into feat/submanifest-reader check-in: 1a17a3defe user: [email protected] tags: feat/submanifest-reader, main, trunk
18:29
Merge branch 'fix/scan-symlink-extract' into feat/submanifest-reader

# Conflicts: # internal/checkout/update.go check-in: 6708292b55 user: [email protected] tags: feat/submanifest-reader, main, trunk

18:27
Merge branch 'fix/receipt-followups' into feat/submanifest-reader check-in: 41ff5d315a user: [email protected] tags: feat/submanifest-reader, main, trunk
18:27
Merge branch 'fix/stash-undo-followups' into feat/submanifest-reader check-in: 22101b521f user: [email protected] tags: feat/submanifest-reader, main, trunk
18:27
fix(checkout): write symlink entries as fossil does, so a fresh checkout extracts

Since the scan began recording islink and chnged 7/9, Create followed by Extract without Force failed on any repository with an "l" file: the file was missing, the scan read it as UNLINK, and the missing-file skip only covered plain edits.

Extract and Update now write a symlink entry as fossil's vfile_to_disk does: as a symlink to the blob content while allow-symlinks is on and the storage can make links, otherwise as a plain file holding the target. A symlink is deleted before a file is written over it, and setMergedMode leaves symlinks and directories alone, as file_setexe does. Extract's missing-file skip covers mode changes too, so the first Extract after Create plays fossil's open whatever the entry.

With allow-symlinks off, fossil itself reports such a file UNLINK and its checkout refuses without --force; Extract keeps that behavior, and an interop test pins it against fossil's open in both modes.

ScanENotFile no longer refuses a directory at a pending add or removal recorded with chnged=0, matching the else-if in vfile_check_signature.

Tracked in #449 check-in: 8cad365e82 user: [email protected] tags: feat/submanifest-reader, main, trunk

16:51
Merge branch 'feat/submanifest-reader' into feat/cones

Update now follows the target's renames and saves undo state. A file outside the cone stays a record-only step: never written, chmodded, backed up or read for undo. A local edit that a rename would carry out of the cone is refused, naming the cone. check-in: 74aa290eda user: [email protected] tags: feat/submanifest-reader, main, trunk

16:29
Merge branch 'feat/submanifest-reader' into fix/stash-undo-followups

# Conflicts: # internal/undo/undo.go check-in: d6ac19e580 user: [email protected] tags: feat/submanifest-reader, main, trunk

16:26
fix(stash,undo): scan before stashing; return undo changes, print nothing

Stash save now scans the checkout first, as fossil's stash_create does (vfile_check_signature), so an edit no scan had recorded is stashed too. Save takes the scanner, a *checkout.Checkout, as its first argument.

Undo and redo no longer print to standard output: they return the files they changed, as []undo.Change, in the undo table's row order, the order fossil undoes them in.

Tracked in #448 check-in: 2dfae63b63 user: [email protected] tags: feat/submanifest-reader, main, trunk

16:25
Merge branch 'fix/update-undo' into feat/submanifest-reader check-in: da7c8a19cd user: [email protected] tags: feat/submanifest-reader, main, trunk
16:10
fix(checkout): save undo state in Update, as fossil does

Update saved no undo state, so nothing could reverse it. It now reads the file of every step that writes or removes one before writing, as fossil's update calls undo_save, and saves those files with copies of vfile and vmerge and the checked-out version in the same transaction that moves the checkout to the target. If a write fails partway the files are put back, as fossil's undo_rollback does.

Checkout.Undo and Checkout.Redo (internal and public) swap that state back and forth and keep checkout-hash naming the restored version. The undo package gains ReadFile, SaveTx and RestoreFiles, and its undo table now has fossil's schema (isExe, unique pathname, rowid order, the executable bit read from disk), so fossil undo reverses a go-libfossil update and Checkout.Undo reverses fossil update; fossil 2.28 interop tests check both. Under js/wasm, which has no undo package, Update saves nothing.

Tracked in #446 check-in: 294a537880 user: [email protected] tags: feat/submanifest-reader, main, trunk

15:59
Merge branch 'fix/update-renames' into feat/submanifest-reader

# Conflicts: # CHANGELOG.md check-in: 5d0a52d706 user: [email protected] tags: feat/submanifest-reader, main, trunk

15:55
fix(checkout): follow renames between versions in Update, as fossil does

Update matched files by name, so a local edit to a file the target renamed was left behind as an untracked file under the old name while the new name got the target's content.

path.NameChanges ports fossil's find_filename_changes with revOK: the renames along the primary path from the checkout's version up to the common ancestor and down to the target, a rename on the way up taken backwards. Update uses it as update.c does: a changed file the target renamed is matched to the target's file under its new name. A merge is written under the new name and the old name removed; when the target only renamed the file, nothing is written and the edit stays under the old name, untracked, as in fossil, which Unmerged now reports. A local rename is not followed, as in fossil.

fossil 2.28 interop tests compare both directions: a target-side rename and edit, a target-side pure rename, a local rename of a file the target edited, and a rename on the checkout's own branch followed backwards to a sibling.

Tracked in #445 check-in: c88a801cf9 user: [email protected] tags: feat/submanifest-reader, main, trunk

15:45
docs(manifest): CheckinOpts.User is the U card, fossil's --user-override

It is never checked against the user table; the check-in's rcvfrom row credits that login's uid when the table holds it and the local setup user otherwise, as fossil credits g.userUid whatever the U card says. A test pins that an unknown name commits under that name. check-in: 54c9e56786 user: [email protected] tags: feat/submanifest-reader, main, trunk

14:52
fix(db): forget a receipt's row when its transaction rolls back

Receipt.ID took (rcvid, mtime) to identify its own row and checked it with a SELECT on every use. After a rollback the rcvid can be reused by another receipt's insert, and under a frozen clock the mtime matches too, so the receipt attached to the wrong row.

Fossil holds g.rcvid for its process and never re-reads the row (content_rcvid_init); a rollback ends the process with it. A Receipt outlives transactions, so the transaction or savepoint that makes its row now records that, forgets the id if it rolls back, and hands it to the enclosing level when released. A kept id costs no query, which takes one SELECT per stored blob off the clone path.

Also checks that a tree check-in's sub-manifests share the commit's receipt. check-in: 0269bd9461 user: [email protected] tags: feat/submanifest-reader, main, trunk

14:41
Merge branch 'feat/submanifest-reader' into feat/cones

Update's rewrite meets the sparse worktree: a file outside the cone is a record-only step, counted in OutOfCone, and never planned, scanned, checked, written, backed up as NAME-original or chmodded; reloading vfile moves its record to the target. SetCone records the mtime of each file it materializes, as Extract now does. check-in: 47f89f0798 user: [email protected] tags: feat/submanifest-reader, main, trunk

14:40
docs: state where text Julian times are checked and how sweep errors read

Only macOS arm64 and linux-x64 fossil have been compared bit for bit, so the rebuild test no longer claims Windows is exact. julian.go says which long doubles fall back to 53 bits wrongly, and ExpandAll's doc says its errors match Cache.Expand's in kind, not always in wording. check-in: e4c84f20f9 user: [email protected] tags: feat/submanifest-reader, main, trunk

14:38
test(fsltype): check macOS fossil's event times at its own long double

TestTextJulianDayMatchesFossil compared macOS arm64 fossil's stored values with the text form at the running platform's long double, so it failed on linux-x64, where fossil itself stores different last digits. TestTextJulianDayPerLongDouble already checks those values at 53 bits and linux-x64 fossil's at 64, so the platform-blind test goes. check-in: e499428d3b user: [email protected] tags: feat/submanifest-reader, main, trunk

12:56
fix(verify): count a tree gap found in the one-pass check-in sweep

checkCheckins now gathers into a staging report and copied only its issues back, so the MissingRefs count treeGap adds was lost: Verify reported a missing sub-manifest as an issue but counted zero missing references. check-in: 39b524c37c user: [email protected] tags: feat/submanifest-reader, main, trunk

12:47
fix(verify): give treeGap a cache after the one-pass blob walk

The rebuild speed-up hands checkOneCheckin each check-in's expanded bytes and drops the pass-wide expansion cache. treeGap, from the sub-manifest reader, still lists a check-in's files to find a missing sub-manifest, and it needs a cache: without one every delta manifest re-expands its baseline. checkCheckins now keeps a small cache for those manifest reads only. check-in: 1a45812566 user: [email protected] tags: feat/submanifest-reader, main, trunk

12:47
Merge branch 'perf/rebuild-blob-check' into feat/submanifest-reader

# Conflicts: # CHANGELOG.md check-in: e3c81fb936 user: [email protected] tags: feat/submanifest-reader, main, trunk

12:47
Merge branch 'fix/stash-undo-storage' into feat/submanifest-reader

# Conflicts: # CHANGELOG.md # internal/checkout/vfile.go check-in: 189088f46c user: [email protected] tags: feat/submanifest-reader, main, trunk

12:46
Merge branch 'fix/merge3-markers' into feat/submanifest-reader

# Conflicts: # CHANGELOG.md check-in: 2a0701c7d3 user: [email protected] tags: feat/submanifest-reader, main, trunk

12:46
Merge branch 'fix/tag-events' into feat/submanifest-reader check-in: e9e08cbb58 user: [email protected] tags: feat/submanifest-reader, main, trunk
02:08
Merge branch 'fix/private-blob-public-checkin' into feat/submanifest-reader check-in: 52ca05c6c4 user: [email protected] tags: feat/submanifest-reader, main, trunk
02:08
fix(manifest): make every file blob a public check-in names public

A public check-in could reuse content first stored privately, say by syncing a private branch, and leave it in the private table. Sync never sends private blobs to a peer without private access, so that peer got a check-in whose file stayed a phantom and could not be checked out.

storeFileBlobs now calls content.MakePublic on each file blob inside the check-in's transaction, as fossil's commit calls content_make_public for each F card (checkin.c). The blob is already queued in unsent, so the next push carries it. The sub-manifest writer already does this for trees.

Tracked in #439. check-in: 6d239246d0 user: [email protected] tags: feat/submanifest-reader, main, trunk

02:00
Merge branch 'fix/scan-missing-perms' into feat/submanifest-reader check-in: ef787892fe user: [email protected] tags: feat/submanifest-reader, main, trunk
02:00
Merge branch 'fix/tag-events' into feat/submanifest-reader

# Conflicts: # CHANGELOG.md check-in: 2e1d1e6fb7 user: [email protected] tags: feat/submanifest-reader, main, trunk

02:00
Merge branch 'fix/rcvfrom-receipts' into feat/submanifest-reader

# Conflicts: # CHANGELOG.md # internal/manifest/manifest.go # internal/sync/handler.go check-in: 6651024b58 user: [email protected] tags: feat/submanifest-reader, main, trunk

01:54
Merge remote-tracking branch 'origin/main' into fix/stash-undo-storage

# Conflicts: # CHANGELOG.md check-in: 044f6da5e0 user: [email protected] tags: feat/submanifest-reader, main, trunk

01:53
fix(stash,undo): write working files through the checkout's storage

Stash and undo read, wrote and removed working files with the os package, so a MemStorage checkout and simulation tests never saw those writes, and no write recorded its mtime. They now take a *simio.Env and go through its Storage, recording each written file's mtime via vfile.WrittenMTime (moved with RecordableMTime from internal/checkout so these packages can share the same-second guard).

Stash save marks reverted rows unchanged with their written mtime, as fossil's revert does. Apply and pop add a stashed added file again, as fossil's stash_apply does, and give a restored edit's row its changed state and written mtime. Undo and redo record a written file's mtime only when the restored row's mtime matched the file when it was saved.

Tracked in #448 check-in: 41951ffbc5 user: [email protected] tags: feat/submanifest-reader, main, trunk

01:52
fix(merge): IsText converts as fossil's text_diff does

Read data through the ported BOM/UTF-16 conversion and line splitter, so the Update gate and the merge agree on what is text.

Tracked in #447 check-in: eb1791acd5 user: [email protected] tags: feat/submanifest-reader, main, trunk

01:51
Merge remote-tracking branch 'origin/main' into perf/rebuild-blob-check

# Conflicts: # CHANGELOG.md check-in: 2f61d4898f user: [email protected] tags: feat/submanifest-reader, main, trunk

01:50
perf(verify): check blob integrity by walking delta trees once

Verify and Rebuild expanded every blob one at a time through a 256 MiB LRU cache. A real repository's delta chains run hundreds of revisions deep, so evicted chains were walked again from their roots: the blob check on the Fossil SCM repository ran for over 45 minutes.

content.ExpandAll walks each delta tree from its root, as fossil's rebuild_step does, expanding every blob once from its parent's content and binding it to its name. It reports for each blob what a caching Expand of that blob alone reports, and expands blobs no sound root reaches (cycles, phantom-grounded or too-deep chains) on their own for that error. checkBlobs and Verify's checkCheckins both use it and sort their issues by rid, so reports are unchanged. check-in: 9a9bc65f92 user: [email protected] tags: feat/submanifest-reader, main, trunk

01:50
Merge remote-tracking branch 'origin/main' into fix/merge3-markers check-in: 4f1f98b70d user: [email protected] tags: feat/submanifest-reader, main, trunk
01:50
fix(merge): write fossil's merge3 output byte for byte

Port fossil 2.28's merge3.c and the diff it runs on (break_into_lines, break_into_tokens, longestCommonSequence, diff_step, diff_optimize, the UTF-8/UTF-16 BOM handling) so Merge and Update align, merge and mark conflicts exactly as fossil does: line-numbered markers, the token-level suggested resolution, CRLF markers, and binary input left unmerged. HasConflictMarkers follows contains_merge_marker in skipping CR runs.

A fossil-binary comparison test checks 1237 generated and hand-written cases against "fossil 3-way-merge" and UTF-16 cases against "fossil merge"; fossil commit refuses a file holding our markers.

Tracked in #447 check-in: dcec15d91f user: [email protected] tags: feat/submanifest-reader, main, trunk

00:33
Merge remote-tracking branch 'origin/main' into feat/submanifest-reader

# Conflicts: # CHANGELOG.md check-in: c851990f41 user: [email protected] tags: feat/submanifest-reader, main, trunk

2026-10-18
22:16
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 check-in: 2d2e62d420 user: [email protected] tags: feat/submanifest-reader, main, trunk

21:59
fix(checkout): keep uncommitted changes across Update, as fossil does (#450)

* fix(checkout): keep uncommitted changes across Update, as fossil does

Update compared only committed versions: it wrote the target over a locally edited file, deleted an edited file the target removed, and merged the current and target check-ins with their ancestor while ignoring the working file.

It now follows fossil's update (src/update.c): scan the checkout, then for each file take the target's content when there is no local change, merge a local edit three ways (the checkout's version as base, the working file as local, the target as other) with conflicts listed in Conflicted, keep an edited file the target deletes as an untracked file, keep pending adds and removals, restore a missing file, and copy an untracked file the target adds over to NAME-original first. A dry run writes nothing. Files written with the target's content record the mtime of the write. Files are matched by name; a target-side rename is not followed.

The two tests that relied on merging two branches' committed changes now make the clashing edit locally. A fossil 2.28 interop test runs the same edits through fossil update and compares files, fossil changes and Status.

Tracked in #443

* fix(checkout): make Update safe on binary files, failures and dry runs

A binary file (a NUL byte, or a line over 32,767 bytes, as fossil's line diff refuses) is no longer line-merged. Update saves the working copy as NAME-original and writes the target, as fossil's update does; Merge keeps the checkout's copy, as fossil's merge does. Both used to splice the versions and report a clean merge. merge.IsText is fossil's test.

Update refuses before writing anything when a file it must write or delete has a directory or a read-only file in the way, moves the checkout database to the target in one transaction, and in a dry run leaves the database untouched (the scan's findings stay in memory). Every target file gets the target's executable bit, as fossil's checkout_set_all_exe does, so a bit-only change reaches the disk.

UpdateResult.Unmerged lists files left to reconcile without markers: edited files the target deletes, files added both locally and by the target, and binary conflicts. The interop test now covers a binary conflict against fossil 2.28.

Tracked in #443

* fix(checkout): check real write access before Update writes anything

The pre-write check read the owner-write bit: it missed an unwritable directory (a remove failed after a merge had been written), files owned by others, and wrongly refused for root. It now asks the storage, via an optional CheckWritable: OSStorage uses access(2) with W_OK on unix and a read-only-attribute check elsewhere (js/wasm, Windows). Every step needs its directory writable; a write or merge over an existing file needs the file writable too.

A read-only untracked file where the target adds one is backed up to NAME-original, deleted and replaced, as fossil does, instead of refused.

Merge keeps a binary file whatever its strategy, as fossil's merge does. setVVar's precondition now catches a nil *sql.DB or *sql.Tx inside the interface.

Tracked in #443 check-in: 6e6969bf97 user: [email protected] tags: main, trunk

20:59
fix(checkout): record and trust vfile.mtime as fossil does (#444)

* fix(checkout): record and trust vfile.mtime as fossil does

go-libfossil wrote vfile.mtime = 0 everywhere and hashed every file on every scan, so it and fossil rehashed a go-libfossil checkout on every command.

The scan is now fossil's vfile_check_signature: a file whose size matches its artifact (blob.size of mrid) and whose mtime matches vfile.mtime is taken as unchanged without being read, unless ScanHash is given or the mtime-changes setting is off; the scan stores the file's mtime in whole seconds and runs its updates in one transaction. Status, commit, merge, revert and extract use the default scan, as fossil's commands do.

Mtimes are recorded where fossil records them: extract and update store the mtime of each file written with its artifact's content, an unchanged file keeps its mtime through LoadVFile (fossil's update transfer and in-place commit), committed files keep theirs, revert stores the restored file's, and SetMTime stores the time it gives. Undoing a rename drops the mtime, since the file was not checked.

One deliberate difference: an mtime not older than the current second is stored as 0, so the next scan (ours or fossil's) hashes the file and an edit that keeps the size within that second is still seen. A stored 0 is never trusted.

MemStorage now stamps each write with a new mtime from its own clock, as a filesystem does, so in-memory checkouts see same-size edits.

Status on the sqlite repository's 2,220-file checkout: about 250 ms before, about 30 ms after.

* test(checkout): pin the same-second guard against fossil

A same-size edit in the second a file was scanned is committed, and fossil 2.28 reading a row the guard stored as 0 reports the edit. Both tests fail with the guard removed. The guard's comment and the CHANGELOG name it as the one deliberate difference from vfile_check_signature.

* fix(checkout): record the mtime of each write as it happens

Extract and update statted their files only after every file was written and vfile reloaded, so an edit in between was recorded as the written content. They now stat each file right after writing it, as fossil's vfile_to_disk does, and carry that mtime to the row. Revert and the new writtenMTime read the clock before the stat, as the scan does.

A caller's Env without a Clock now gets the real clock, as Storage defaults to the OS; scans read it. The mtime-changes setting is read from the repository only: go-libfossil reads no global fossil settings, which the comment and CHANGELOG now say.

New tests: an edit after extract's or update's write is seen, a stored 0 is never trusted, and a simulated clock at a realistic time drives the mtime fast path. check-in: a71bf662ee user: [email protected] tags: main, trunk