30 check-ins that include changes to files matching 'internal/checkout/*'
|
2026-10-13
| ||
| 11:14 |
feat(repo): name the current trunk and a branch's open leaf (#411)
* feat(repo): name the current trunk and a branch's open leaf Repo.CurrentTrunk resolves the main-branch setting's name the way ResolveVersion does, as fossil does for update outside a checkout and open with no version (repo-state-01 Section 9). Repo.BranchOpenLeaf names the open leaf of a branch with the greatest mtime, or reports the branch not open; fossil's branch info takes whichever open leaf its query meets first, and this follows the draft. checkout.BranchLeaves and the new checkout.BranchOpenLeaf share one open-leaf query. RS-9-1 and RS-9-4 lose their scope notes and gain cases. * fix(repo): resolve the current trunk as a check-in, reject an empty branch name Tracked in the stacked work on repo-state-01 Section 9. check-in: c2795c4f48 user: [email protected] tags: main, trunk | |
| 11:05 |
feat(resolve): resolve checkout-relative names and branch-type requests (#409)
* feat(resolve): resolve checkout-relative names and branch-type requests Checkout.ResolveVersion resolves current, prev/previous and next relative to the open checkout, after tip and before the other forms, as fossil's symbolic_name_to_rid does; a keyword that names nothing falls through. Repo.ResolveBranchStart is the branch-type request: it resolves a name and walks back to the first check-in of its branch. Section 8.3 walks back from every result; fossil only from tag:NAME and plain names, and this follows the draft. The repostate.8.3 checkout and branch-type cases run against the new entry points, with new cases for a keyword shadowing a tag, prev at a root, an untyped ckout, root:current and a branch-type hash. Tracked in #386. * fix(resolve): match check-ins only for branch-type requests, merge via checkout A branch-type request is rewritten to a check-in request (repo-state-01 Section 8.3), so a technote tagged with the branch name cannot answer it. A checkout with no recorded version resolves the ordinary forms, and merge resolves its version through the open checkout. Tracked in #386. * fix(resolve): wrap a long test line check-in: cba2d0c823 user: [email protected] tags: main, trunk | |
| 06:14 | fix(checkout): pick the no-target update target as fossil does; read untagged check-ins as the default branch (#399) check-in: 425dd36e13 user: [email protected] tags: main, trunk | |
| 05:59 |
fix(manifest,hash): verify the R card at commit; reject an unrecognized hash-policy (#396)
* fix(manifest,hash): verify the R card at commit; reject an unrecognized hash-policy A check-in reads its manifest back and requires the R card to equal the digest recomputed over the resolved tree, byte for byte (artifact-format-01 6.3.4, AF-6.3-5). A hash-policy setting outside the ordinals 0 through 4 is now an error instead of a fall back to the default (7.1, AF-7.1-2). Tracked in #387. * fix(manifest,repo): drive the commit R check from conformance; refuse a bad hash-policy on write check-in: c42a2b90c6 user: [email protected] tags: main, trunk | |
| 05:32 | test(conformance): cover the repo-state GAP and PARTIAL rows (#395) check-in: 3c3b12fdf2 user: [email protected] tags: main, trunk | |
|
2026-10-12
| ||
| 20:17 |
fix(checkout): make WouldFork fossil's commit-time fork check (#347) (#348)
Since #291 WouldFork was fossil's leaf_ambiguity, which answers no at once for a version that is not a leaf. That is the case a commit forks in: when a pull brings in a child of the checked-out version, the version stops being a leaf, WouldFork said false, and the commit became a second child. EdgeSync's autosync relies on WouldFork to refuse that commit. WouldFork is now fossil's commit refusal (src/checkin.c "Would fork", taken when !is_a_leaf(vid)): true when the version has a child on its own branch, read from plink as is_a_leaf does, merge children included. Another open leaf elsewhere on the branch no longer counts; fossil commits there, extending the checkout's leaf, and only warns afterwards. The repo-state draft's Section 7.5 ambiguity test moves to checkout.LeafAmbiguous, and the repostate.7.5.ambiguity-* conformance cases call it. The superseded #291 changelog text and the WouldFork API doc are corrected. check-in: cd4a11828f user: [email protected] tags: main, trunk | |
|
2026-10-11
| ||
| 22:59 |
fix(checkout): count only open leaves for forks and fork warnings (#291) (#315)
BranchLeaves joined the leaf table without excluding leaves that carry a closed tag, so a closed leaf still counted toward a branch's leaves, and WouldFork, built on it, reported a fork for a version that was not itself a leaf. This made a closed leaf look like an open one and a non-leaf commit look like it would fork. BranchLeaves now excludes any leaf with a closed tag (tagtype>0), matching fossil's leaf_is_closed_sql, so it returns a branch's open leaves. WouldFork is now fossil's leaf_ambiguity: the current version must itself be an open leaf (is_a_leaf AND not closed) before another open leaf on the same branch can make a commit fork; a non-leaf or closed version returns false. currentBranch now surfaces a real query error instead of masking it as "trunk". Unit tests cover the open-sibling, closed-sibling and non-leaf-parent cases. check-in: f8496d12cc user: [email protected] tags: main, trunk | |
|
2026-10-06
| ||
| 15:57 |
fix(checkout): merge the way fossil does, and commit merges with their parent (#244) (#252)
The CLI's merge marked a conflict with vfile.chnged=5, which fossil reads as "added by integrate", and recorded no merge-in version or vmerge row, so a merge went unseen and the next commit dropped the merge parent. Merging now lives in the library as Checkout.Merge and records what fossil records: chnged=2 and mrid for a file taken from the merged-in version, chnged=3 for an added file, a per-file vmerge row for a merged file, and vmerge id 0 for the merge. Conflicts use fossil's markers and are found by looking for them (Checkout.Conflicts), so fossil and go-libfossil read each other's merges. Where fossil relies on its undo copy, Merge keeps the file or refuses instead. Commit reads vmerge for merge parents, and, as fossil's commit does, refuses unresolved conflict markers, empty commits, unknown names and partial commits of a merge. The CLI's merge, conflicts, mark-resolved and ci commands call the library; ci now commits the checkout. check-in: 7ea6fba63f user: [email protected] tags: trunk | |
|
2026-10-03
| ||
| 17:38 |
fix(checkout): revert undoes a pending rename (#242) (#249)
* fix(checkout): revert undoes a pending rename (#242) Revert-all selected rows by content and removal, so a rename recorded by fossil mv (origname set, chnged 0) was skipped; a renamed and edited file had its committed content written under the new name, leaving the rename pending. A named revert only matched the current name. Like fossil's revert, a pending rename is now undone: the file under the new name is removed, the row takes its old name back (UPDATE OR REPLACE, as fossil does), and the committed content is written there. Revert-all includes renamed rows, and a named revert answers to either the new or the old name. A renamed file a merge added is still removed outright, as before and as in fossil. removeRegularFile is shared by the rename undo and removeMergeAdded, so neither deletes a directory the user made at that path. * fix(checkout): plan a revert before changing anything (#242) Undoing a rename with UPDATE OR REPLACE let a swap destroy files: reverting a.txt and b.txt exchanged through fossil mv deleted the other row, the loop then removed a file it had just restored from its stale row list, and the next restore failed half way, leaving both files gone from disk. A new file added under the old name was overwritten the same way. A named revert of the old name also stopped at the first match. Revert now plans, checks, then acts. planRevert decides every target row's action (restore, un-manage, remove a merge-added file, undo a rename) before anything changes; checkRenameUndoFree refuses the whole revert when undoing a rename would land on a name another tracked row holds, since go-libfossil has no undo copy to recover an overwritten file (fossil saves one); revertFile carries out the planned action. The rename undo is a plain UPDATE, which fails rather than replace a row. A name selects every row it matches, new name or old. The callback reports a rename under the name the file gets back. Swaps and chains, which fossil completes via its undo store, are #248. * fix(checkout): revert named paths as one; refuse untracked file at old name (#242) Review of the planned revert found two remaining ways to lose work: - Several named paths were reverted one at a time, so in `revert b.txt z.txt` the edit to b.txt was discarded before z.txt's rename undo was refused. revertPaths now gathers every named row (new or old name) and plans and checks them as one revert: a refusal reverts none of them. - Undoing a rename restored the committed content over an untracked file at the old name. checkRenameUndoFree now also refuses when a file on disk there differs from the committed content (wouldClobber), for the same reason it refuses a tracked one: there is no undo copy. * docs(changelog): note revert's untracked-file and multi-path rules (#242) check-in: 50d1a7f72b user: [email protected] tags: trunk | |
| 17:27 |
fix(content): report database faults from the availability check (#239) (#247)
* fix(content): report database faults from the availability check (#239) IsAvailable and AvailableByUUID return a bool, so a busy, closed or I/O-failed database read as "content missing". Where missing content is tolerated on purpose, that swallowed the fault: since #231, CreateCheckout leaves vfile empty for a partly synced tip, and a fault during resolveFiles' availability check took the same path, so Create succeeded with an empty checkout. The chain walk moves to checkAvailable, which returns an error for a fault and a plain false for an unknown rid, a phantom, a cycle or an over-long chain. New CheckAvailableByUUID exposes that distinction, and resolveFiles uses it, so a fault fails LoadVFile with its own error instead of errMissingContent. IsAvailable and AvailableByUUID keep their signatures and their fail-closed reading of a fault; their ~30 callers treat "not available" as a failure already. * docs(changelog): note the availability fault fix (#239) * fix(content): NULL size is missing content; don't cache faults (#239) checkAvailable scanned blob.size into an int64, so a NULL size (legal in the schema) became a hard error in resolveFiles where it used to read as missing content. Both walks now read a NULL size as a phantom. AvailabilityCache memoized a walk's verdict for every node it visited, including a false that came from a database fault, so one transient fault stayed wrong for the rest of the crosslink sweep. A faulted walk now returns false without being remembered; an unknown rid is still a verdict and is cached as before. check-in: 4b14509374 user: [email protected] tags: trunk | |
| 17:22 |
fix(checkout): dry runs change nothing; switching ends a merge (#236) (#246)
* fix(checkout): dry runs change nothing; switching ends a merge (#236) A dry-run Extract still called LoadVFile(rid, clear=true) and finalizeExtract, replacing the current version's vfile rows (pending adds, renames, merge state) and moving vvar to the target. A dry-run Update still ran finalizeUpdate, with the same effect. Extract now lists the target's files from its manifest (resolveFiles) instead of reading them back from vfile, so a dry run never loads them; only a real extract replaces vfile and records the version. Update skips finalizeUpdate on a dry run. finalizeExtract also clears vmerge, as fossil's checkout does: a forced switch used to leave a merge record that would give the next commit a bogus merge parent. * fix(checkout): forced extract starts fresh; re-extract keeps a merge (#236) Two gaps against fossil's checkout found in review: - A forced extract dropped only other versions' vfile rows (LoadVFile keeps the target's), so re-forcing the current version left a pending rename's two names on disk and in vfile, and a removed file restored but still marked deleted. fossil checkout --force deletes every row first; replaceFileList now does the same when Force is set. - Extract cleared vmerge on every completed run. fossil's checkout returns early when the version does not change and Force is off, keeping a pending merge; finalizeExtract now ends the merge only for a switch or a forced extract. * refactor(checkout): finalizeExtract reads the prior version itself (#236) Only the final step needs the version being replaced, and nothing changes vvar before it, so finalizeExtract reads it there. A dry run no longer calls Version at all, and Extract drops to 64 lines. check-in: 139d426384 user: [email protected] tags: trunk | |
| 17:09 |
fix(cli): add, rm, rename and revert call the library (#234) (#245)
* fix(cli): add, rm, rename and revert call the library (#234) These commands wrote the checkout's vfile with raw SQL, each with its own copy of rules the library already gets right, and ignored most Exec errors. rename recorded the new name but left the file under the old one, so the next commit failed on a "missing" tracked file. revert only cleared change flags over an edited file and restored nothing. add wrote a SHA1 mhash whatever the repo's hash policy. They now open the checkout through the library (openWorkingCheckout, which finds the repository from the checkout when -R is not given) and call Add, Remove, Rename and Revert, returning every error, including from closing. rename moves the file, like fossil mv --hard; revert restores committed content, like fossil revert. Two library gaps the CLI exposed are fixed where they live, matching fossil's revert: - Revert refreshes change flags from disk before choosing files, so an edit no scan has recorded yet is reverted instead of skipped. - Reverting a never-committed add un-manages it and leaves the file on disk. It used to delete the file, destroying the only copy; fossil prints UNMANAGE and keeps it. revertFile's restore path moves to restoreCommitted, bringing it under 70 lines. The merge, merge-resolve and conflicts commands are #244. * docs(changelog): note the CLI and Revert changes (#234) * fix(checkout,cli): revert and rename match fossil; find repo without -R (#234) Review of the CLI switch found the library and the new helper falling short of fossil: - Without -R, every command failed on a fresh fossil checkout: the repository lookup opened the checkout database read-only, which clashes with the WAL pragma on a database fossil left in delete journal mode, and hid the real error. It now opens it as the library does, reports the error, and resolves a relative repository path, as fossil open stores it, against the checkout's directory. - Rename moved the file onto an existing untracked file, destroying it. It now refuses, as fossil mv --hard does, before changing anything. - Revert skipped a file missing from disk (no change flag records it) while the CLI reported it reverted. A named file is now reverted whatever its flags say, and revert-all includes missing files. - Revert left merge state behind: restored rows kept mrid on the merged-in version and vmerge kept its row, so the next scan saw the file changed and the next commit recorded a merge parent. Restored rows now reset mrid and mhash, and revert-all clears vmerge, as fossil's revert does. Rename's vfile lookups move to renameRowID and revert-all's selection to needsRevert, keeping both functions under 70 lines. The re-add test now checks the vfile row itself, since fossil's own scan would hide a stray chnged=1. * fix(checkout): revert removes a file a merge added, as fossil does (#234) Revert decided between restore and un-manage by rid alone. A file a merge added has a non-zero rid (its merged-in artifact) but is not in the checked-out version, so revert restored it as if committed and left it tracked; the next commit then included it. Fossil looks the file up in the version's manifest and, when it is absent and rid is not 0, deletes the file and its row (its content lives on in the merged-in version). revertFile now takes the vfile row and the version's path set and does the same: a pending add is un-managed and kept on disk; a file the version lacks, under its name or its name before a pending rename, is removed; anything else gets its committed content back. Checking the pre-rename name keeps a renamed, edited file from being deleted. Rename's refusal message now fits a directory at the destination too. * fix(checkout): never remove a non-file when reverting a merge add (#234) removeMergeAdded deletes only a regular file at the merge-added path; anything else there, such as a directory the user made, is left alone. Revert's doc now says edits to reverted files are discarded: fossil saves an undo copy first, go-libfossil has no undo yet. check-in: de8f3ed302 user: [email protected] tags: trunk | |
| 16:47 |
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. check-in: f55d0ed2de user: [email protected] tags: trunk | |
| 16:28 |
fix(checkout): classify changes in one place; report missing files (#232) (#241)
* fix(checkout): classify changes in one place; report missing files (#232) HasChanges, VisitChanges and Extract's safety check each decided what counts as a change: two SQL WHERE clauses and classifyChange, with an error path whose only job was catching their disagreement. They had already drifted once (HasChanges missed renames until #229). Now one reader, loadVFileRows, returns every row of a version and one classifier, classifyChange, judges it, in the order fossil's changes command uses: removed > missing > added > renamed > modified. A renamed file that is also edited stays "renamed", so callers that see only the change type do not lose the rename. The WHERE clauses and the malformed-row error are gone. VisitChanges with a scan now reports a tracked file that is gone from disk as ChangeMissing, which the public API already exposed as "missing" but nothing produced; such files were left out, or shown as added or modified. HasChanges sees a fossil add (rid=0, chnged=0) without a scan, since rid=0 now always means added (#231). * fix(checkout): treat a non-file at a tracked path as missing (#232) ScanChanges read each tracked path and failed the whole scan when a directory stood where a file belongs. Fossil's scan reports that path as NOT_A_FILE under its missing filter. The scan now stats first and treats anything that is not a regular file as missing, so Status reports it instead of erroring. loadVFileRows orders rows by pathname, as fossil's changes lists them. The changelog notes that checkouts written before #231 may hold rid=0 placeholder rows, which HasChanges now counts. check-in: ff9c1af49d user: [email protected] tags: trunk | |
| 16:16 |
fix(checkout): refuse to load a version with missing content (#231) (#240)
* fix(checkout): refuse to load a version with missing content (#231) LoadVFile wrote a vfile row with rid=0 for each file whose content the repo does not hold (a phantom, or a delta on one), as a partial sync leaves it. In fossil's vfile rid=0 means "added", so those rows were indistinguishable from pending adds, and Extract then panicked in content.Expand(0) trying to write them. LoadVFile now does what fossil's checkout does: it resolves every file first and refuses the version, naming up to ten missing files and wrapping errMissingContent, before changing vfile at all. It no longer returns a missing count, which no caller used. Create is the one caller that tolerates the refusal. A tip that a partial sync has not finished is no reason to fail CreateCheckout: the caller may be about to extract an older, complete version (EdgeSync's ExtractTo). vfile then stays empty instead of claiming files the repo cannot produce. Create's environment defaulting, shared with Open, moves to resolveEnv, and the initial load to loadInitialVFile. Tests cover the refusal (vfile untouched, files named), Extract leaving the checkout as it was, Create on a partial tip followed by Extract of a complete version, and parity with the fossil binary, which refuses the same repository with "missing content". * fix(checkout): refuse missing content in Update before writing (#231) Update wrote changed files to disk and only then reloaded vfile, which now refuses a version with missing content. A target sharing a missing file with the current version (its content is never read, because it does not change) could therefore leave disk on the target while vvar and vfile stayed on the old version. Like fossil's update, Update now resolves the target's content before writing anything, through resolveFiles, the resolution step split out of LoadVFile. Update moves its target selection into updateTarget, reuses Extract's checkinTime, and builds its result through sortedUpdateResult, bringing it under 70 lines. Create now reports a tip it could not load to the observer instead of tolerating it silently. * fix(checkout): report Update refusals to observers; tighten checks (#231) The missing-content check in Update moves after ExtractStarted, so a refused update still reaches observers as an ExtractCompleted carrying the error, as other Update failures do. A negative target from the public UpdateOpts is an error, not a panic. resolveFiles errors carry their own prefix instead of LoadVFile's. Adds the entry and exit assertions the new helpers were missing, a compile-time check on missingNamesMax, and wraps lines over 100 columns; tests check Version's error instead of discarding it. check-in: a317896683 user: [email protected] tags: trunk | |
| 15:52 |
fix(checkout): check Extract's safety before replacing vfile (#230) (#237)
* fix(checkout): check Extract's safety before replacing vfile (#230) Extract called LoadVFile(rid, clear=true) first and only then checked for local work. A refusal therefore returned with the current version's rows already gone: pending adds, renames and merge state were lost, and vvar still named the old version while vfile held only the new one, so Status saw an empty checkout. The check also compared disk against the target version, so switching a clean checkout to any version that changed a file was refused. Extract now follows fossil's checkout command, before touching anything: the current version must have no unsaved changes, and no file the current version does not track may sit where the target would write different content (fossil prompts there; a library refuses). Force skips both, as before. The safety scan runs through a new refreshChanged, the observer-free core of ScanChanges, so an Extract does not report a scan of its own. Extract is split into extractRows, checkinTime and extractRow to bring it under 70 lines; behavior of the write loop is unchanged. * fix(checkout): let Extract proceed when no change would be lost (#230) Create records the tip as checked out before any file is written, so Create followed by Extract of an older version, into a directory that already holds it, saw every file differing from the tip as an unsaved edit and refused. That is EdgeSync's ExtractTo pattern. The safety check now refuses only changes the extract would lose: a plain edit whose content already equals what the target writes is not lost, so it no longer blocks. Adds, removals, renames, merge states and edits to files the target does not write still refuse. The edit check and the untracked-file check share wouldClobber, and the target manifest is read once and walked in manifest order. * fix(checkout): never relax Extract's check while a merge is pending (#230) A vmerge row is unsaved work on its own. With every merged file already matching the target, the previous commit let Extract switch versions, dropping the merge and leaving the stale vmerge row to give a later commit a bogus merge parent. The content-equality relaxation now applies only when no merge is pending. check-in: c9d97efb90 user: [email protected] tags: trunk | |
| 15:20 |
fix(checkout): read checkouts made by the fossil binary (#228) (#229)
Status on a checkout created by `fossil open` failed with "converting NULL to string" on vfile.mhash. go-libfossil treated mhash as every file's baseline hash; fossil only fills it while a merge is pending (the hash of mrid when mrid != rid) and judges a file against the blob that vfile.mrid names. A NULL-tolerant scan alone would not have fixed it: with no baseline the comparison panics, and skipping it would hide real edits. ScanChanges now compares each file with blob.uuid for vfile.mrid and moves chnged through fossil's own transitions (vfile_check_signature): a pending add (mrid=0) becomes 1, unchanged and clean-merge states (0, 2, 4) become 1 on a difference, a reverted edit returns to 0, and other merge states are kept. Comparing against the artifact's own uuid also picks the hash algorithm per file, independent of hash-policy. The non-forced Extract guard read mhash with its error discarded, so on a fossil checkout it skipped the check and overwrote local edits. It now uses the same baseline, propagates read errors, and refuses while a merge is pending (mrid != rid or chnged > 1). Status and HasChanges treat a rename as origname not in ('', pathname), since fossil stores origname == pathname on unrenamed rows, and HasChanges now sees `fossil mv`. A malformed row returns an error instead of being reported as ChangeNone. LoadVFile and Manage stop writing mhash, and Manage writes islink, which fossil's vfile schema declares without a default. Tests drive real fossil-made checkouts: clean status, edits and adds, a pending merge (status, preserved merge state, extract refusal), local edits under extract, fossil mv, and a go-libfossil Add read back by both tools. check-in: 0069100efe user: [email protected] tags: trunk | |
|
2026-08-07
| ||
| 17:33 |
fix(db): seed hash-policy so new repos name artifacts like fossil init (#225) (#226)
SeedConfig never wrote hash-policy, so naming fell back to deriving from the repo's own artifacts — and an empty repo has none, settling on SHA1 permanently. Seed sha3, which is what fossil init writes. Clone drops the row the same way it drops project-code: a clone adopts its source's policy, and an absent row derives naming from the artifacts that arrive. Moves the policy enum to db so the seed and hash.NamingFor read one definition, and updates fixtures that hardcoded SHA1 names to ask the repo how it names artifacts. check-in: 1c02c944ec user: [email protected] tags: trunk | |
| 15:54 |
fix(blob): name new artifacts by the repo's hash-policy (#223) (#224)
Store hardcoded SHA1, so committing into a sha3 repo mixed hash families and made every tracked file read as CHANGED. Read hash-policy once per store and name new artifacts with the algorithm it selects. Content already stored under the other algorithm's name keeps that name unless the policy forbids it (sha3-only, shun-sha1), so unchanged files don't churn their F-cards. Also routes checkout.Manage and uv.Write through the same lookup, replacing their width-sniffing guesses — uv's read project-code, which is 40 hex chars in every repo, so it could only ever pick SHA1. check-in: 847c195286 user: [email protected] tags: trunk | |
|
2026-07-28
| ||
| 22:08 |
fix(manifest): unify mlink derivation (#216)
Route direct check-ins and internal rebuilds through canonical mlink derivation. Update sparse-history consumers and preserve legacy conflict-fork rows with an explicit RID discriminator migration.\n\nCloses #213 check-in: 04f41a4c9c user: [email protected] tags: trunk | |
|
2026-07-24
| ||
| 12:19 |
checkin: emit renames as a single F-card with the w placeholder (#147)
* fix: emit renames as a single F-card with the "w" placeholder (#51) buildCommitFiles seeded its working set from the parent manifest and layered vfile changes on top, but never consulted vfile.origname. A pure rename has identical content, so ScanChanges clears its chnged flag; the new name then never entered the working set while the old name carried forward from the parent, so the manifest emitted the file's content under its old pathname and dropped the rename entirely. Drive renames off vfile.origname, mirroring canonical Fossil's manifest query (src/checkin.c:1939): applyRenames retires the old pathname and records it on the surviving new-name entry, so a rename is emitted exactly once. The unchanged-content pass then loads the blob and preserves OldName. Serialize the rename card to match stock Fossil: force a "w" permission placeholder when a renamed file's perm would otherwise be empty, keeping the prior-name field in its 4th positional slot (src/checkin.c:1999). Thread OldName through manifest.File and storeFileBlobs so the commit path can carry it. Verified the exact card "F greet.txt <uuid> w hello.txt" against a real fossil binary for the same rename scenario. * fix: never emit a renamed-then-deleted file in the commit manifest applyRenames enumerated every vfile row with origname != pathname, including rows marked deleted, and unconditionally seeded a new-name entry for them. A file that was renamed and then unmanaged therefore committed as a rename F-card under its new name. The deleted-files pass could not catch it: it only knows the file's current pathname, so deleting the new name from a map keyed on parent-manifest pathnames was a no-op and nothing retired the old name. Canonical Fossil excludes deleted rows from its manifest query outright (WHERE NOT deleted OR NOT is_selected). Match that: a deleted renamed row now retires both names and emits nothing, keeping the existing guard that leaves the old name alone when a live vfile row has reclaimed it. Also move the deck marshaller's prior-name field inside the UUID guard. The field is positional and only meaningful after a hash; a deletion card carrying one would serialize as "F name oldname", with the old name misread as the UUID. Tests: rename-then-delete emits no card under either name; a rename chain that recycles a freed pathname emits both cards with the right prior names and content. check-in: 29c2c5c6f5 user: [email protected] tags: trunk | |
|
2026-07-23
| ||
| 10:41 |
fix: zero-length file commit panic and stale missing-file carry-forward (#128)
* fix: zero-length file commit panic and stale missing-file carry-forward Two related bugs in the checkin path: #68 - Committing a zero-length file panicked in blob.Store, which treated an empty artifact (a normal, well-known Fossil blob) as invalid input. Worse, manifest.Checkin's postcondition defer fired unconditionally during the panic's stack unwind and re-panicked with its own unrelated "manifestRid must be positive" message, masking the real cause. Fixed both: blob.Store now accepts zero-length (and nil) content, and Checkin's defer re-panics with the original panic value instead of asserting a postcondition that was never going to hold mid-unwind. Fixing the panic surfaced a second bug in the same family: buildCommitFiles used len(Content) > 0 to detect "already loaded from disk," which can't distinguish a genuinely empty file from one not yet loaded, so a newly added empty file failed with "no RID for unchanged file." Replaced the length sentinel with an explicit loaded-set. #79 - A tracked file deleted from disk without Unmanage/Remove was silently carried forward into the new manifest with its last-committed content, rather than being detected. Verified against fossil 2.28: canonical fails a commit when a missing tracked file is in scope (either the implicit "commit everything" case, or explicitly named), but leaves an out-of-scope missing file alone. buildCommitFiles now stats every in-scope tracked file before treating it as unchanged and errors clearly, naming the file, when it's gone; a file outside the commit's scope (relevant only to an explicit Enqueue) is left untouched exactly as before. Updated the two existing tests that had documented the old silent-carry-forward behavior as deliberately deferred to this issue. * fix: add CHANGELOG entry and unwrap-safe stat checks for #68/#79 Adds the CHANGELOG entries the #79 acceptance criteria required for a user-visible behavior change (missing tracked file now hard-fails a commit instead of silently carrying stale content forward) and for the #68 zero-length-file panic fix. Also switches checkin.go's two os.IsNotExist(err) checks to errors.Is(err, os.ErrNotExist), which correctly unwraps the %w-wrapped os.ErrNotExist that MemStorage.Stat returns. Production was already correct (OSStorage.Stat returns a raw *PathError), but the tests were only exercising a generic fallback error branch instead of the intended "tracked file missing from disk" message. check-in: 0351167ae5 user: [email protected] tags: trunk | |
|
2026-07-22
| ||
| 23:03 |
fix: correct go.mod module path to go-libfossil (#76) (#117)
* fix: correct go.mod module path to go-libfossil (#76) The module directive still declared github.com/danmestas/libfossil after the repository was renamed to go-libfossil. pkg.go.dev/godoc scraping matches the declared module path against its hosting path, so the stale path broke SDK doc generation. Rename the module path across all four in-repo modules (root, the two db/driver submodules, observer/otel, and docs/site) and every internal import, plus the release workflow, the doc-generation script, and the docs. No tag has been published under the go-libfossil path yet, so the cross-module requires cannot resolve from the proxy. Add replace directives pointing the in-repo requires at their local directories so GOWORK=off builds and the CI matrix resolve without a published version. These replaces are ignored by downstream consumers and can be dropped once new-path tags are cut. * Guard releases against replace directives; regenerate SDK docs The go-libfossil rename bridges the in-repo modules with `replace` directives. Go refuses a main module that carries them, so a tag cut while they are present breaks `go install .../cmd/libfossil@<tag>` and every `@version` fetch of the driver submodules. Add a Release workflow step that fails the job before anything is published if any tracked go.mod still contains a replace directive, so the bridge is a CI-enforced TODO rather than a silent landmine. The step comment records what unblocks a release: re-tag the submodules with the corrected module paths, bump the root requires, then drop the replace blocks. Regenerate the SDK reference docs. The previous pass only substituted URLs over the pre-existing generated files, leaving stale signatures (Repo.Timeline was still documented as taking LogOpts rather than TimelineOpts). Note that gomarkdoc must be run outside a git worktree — inside one it silently strips every source link. Also reword the README rename note, which read as though the import path had not changed, and re-sort the ncruces require block. check-in: 6273a0750d user: [email protected] tags: trunk | |
|
2026-07-19
| ||
| 18:27 |
checkout: re-stat executable bit at commit time (#80)
* checkout: re-stat executable bit at commit time, not just on changed files Change scanning compares content hashes, so a file whose only edit is a chmod (owner-execute bit gained or lost, no content change) is never flagged as changed. vfile.isexe then carries forward whatever was captured at the last Add/Commit, and the resulting F-card silently keeps the stale permission. Mirror canonical Fossil's belt-and-braces approach (src/checkin.c): manifest generation re-stats each committed file's live on-disk mode independently of whatever change scanning recorded, using the existing modeIsExecutable predicate (added for the prior executable-bit fix) rather than trusting a cached flag a second time. This runs for every file with a vfile row that is part of the commit, not only ones flagged as content-changed, so a chmod-only edit is now captured. Applies only to files actually present in vfile, leaves symlink markers alone, and is a no-op on Windows where os.FileMode carries no owner-execute bit. * checkout: skip missing files during commit-time perm re-stat restatCommitPerms Stats every tracked file that is part of the commit, and a Stat failure on any of them aborted the whole commit. That regressed a case the mode-change fix never meant to touch: a tracked file deleted from disk without Unmanage, with an unrelated file modified, used to commit successfully and carry the missing file's last-committed content forward — after the fix it failed the commit outright, on the implicit "commit everything" path in particular. Skip only os.IsNotExist and carry the existing Perm forward unchanged, matching pre-fix behavior exactly. This fix is about mode changes being missed, not about detecting stale tracking — that is a legitimate feature canonical Fossil also checks for at commit time, but it deserves its own issue rather than arriving as a side effect here. Any other Stat error (permission denied, I/O fault, ...) still fails the commit. check-in: 094abd4b4e user: [email protected] tags: trunk | |
| 16:31 |
checkout: Update returns UpdateResult so conflicts are visible (#61)
* checkout: Update returns UpdateResult so conflicts are visible The internal 3-way merge already tracked which files were written, removed, and left with conflict markers, but Checkout.Update discarded all of it and returned a bare error. That collapsed a clean update and one that silently wrote <<<<<<< markers into working-tree files into the same outcome, with no way for a caller to tell them apart. Update now returns (UpdateResult, error). UpdateResult.Conflicted lists paths that were merged but not cleanly -- this is still a successful update (err == nil), not an error, since the working tree is usable and the caller just needs to know which files to show a user. A genuine failure still returns a non-nil error paired with a zero-value UpdateResult. FilesWritten/FilesRemoved are populated the same way, as paths rather than counts. Threading the paths up required changing the internal update's processFileUpdates from returning bare int counts to returning the actual []string paths, since the count was all that existed before. Checkout.Extract is untouched -- it has no conflict concept. Breaking change, clean break at v0.7.0 (pre-1.0). * checkout: sort Update result paths, document FilesWritten/Conflicted overlap Update's result slices were built by ranging over maps.allNames, a map[string]bool, so their order was whatever Go's randomized map iteration happened to produce that run. A caller diffing result.FilesWritten against a golden file, or asserting on it with reflect.DeepEqual, would see it fail intermittently for no reason related to their code. Sort all three slices (sort.Strings) right before Update returns. Added a test that touches four files -- two conflicting, two clean additions, named so map iteration order is very unlikely to hand them back sorted by luck -- and asserts the exact sorted slice on both FilesWritten and Conflicted. Verified it fails reliably (5/5 runs) with the sort calls temporarily removed, then restored the fix. Also stated outright, in both the internal and public UpdateResult doc comments, what "clean or not" was only implying: every path in Conflicted also appears in FilesWritten, since the conflict markers are what got written there. check-in: 75f3521e54 user: [email protected] tags: trunk | |
| 16:00 |
fix: separate blob existence from blob availability (#43) (#52)
* fix: separate blob existence from blob availability (#43) A phantom is a real blob row with size = -1 and NULL content, so blob.Exists — "SELECT rid FROM blob WHERE uuid=?" — answers true for it. Every guard built on Exists therefore admitted artifacts whose content cannot be read, and content.Expand aborted with "rid N is a phantom". Canonical Fossil keeps two predicates and never substitutes one for the other: rid_from_uuid (existence, xfer.c:70) and content_is_available (availability, content.c:163). The latter is transitive — it follows delta.srcid up the chain, so a delta whose base is a phantom is unavailable even though every row in the chain exists. Add content.IsAvailable / content.AvailableByUUID, ported from content_is_available including the chain walk, with an explicit depth bound so a cyclic delta table terminates instead of hanging. The walk lives in exactly one place; no caller touches the delta table. blob.Exists keeps its behavior and gains a doc comment stating that it answers existence only, returns true for phantoms, and naming the availability predicate as the alternative. manifest.blobPresent is deleted. It was a local patch at one call site that tested size >= 0 without the chain walk, so it still reported a delta-on-phantom as present — the same class of bug it was written to work around. Every blob.Exists call site was classified as existence or availability and converted where it guards a content read. Sites that want rid identity — plink/mlink/tagxref population, cluster phantom creation, private-status bookkeeping, the post-store sanity check in handleFile, and phantom filling in storeReceivedFile — keep existence, since delta-before-base is a normal steady state during clone. Verified against a loopback fossil 2.28 server: the 23-artifact repro that previously failed roughly half the time now clones 6 of 6. * fix: IsAvailable must fail closed when the delta lookup errors Only sql.ErrNoRows means the chain is grounded. Any other failure — cancelled context, closed connection, NULL srcid — says nothing about groundedness, so returning true there claims readable content that was never verified. That is the same class of bug this predicate exists to prevent, and it runs on data arriving over the wire during clone. The blob-size lookup above already fails closed on the same pattern; this restores the symmetry. check-in: 670ca0e75f user: [email protected] tags: trunk | |
| 15:31 |
fix: capture executable bit at Add/Manage time and propagate to commit (#48)
Manage() inserted new vfile rows with isexe always defaulting to 0, regardless of the file's on-disk permissions. buildCommitFiles() also never consulted vfile.isexe when constructing the manifest F-card for changed/added files — it only carried forward the parent manifest's Perm string, which is empty for a brand new file. Together this meant an executable file added via Manage always committed without an "x" marker, and a later Extract wrote it back as 0644. Fix: - Manage() now stats the file and records isexe using the same predicate as Fossil's file_perm() (src/file.c:316): owner-execute on a regular file (S_ISREG && S_IXUSR). Group/other-only execute (e.g. 0645) does not count, matching canonical behavior. The detection lives in one place (modeIsExecutable, next to the existing isexe-to-mode helper vfilePerm) rather than being duplicated at call sites. - buildCommitFiles() now derives the F-card Perm for changed/added files from vfile.isexe instead of only inheriting the parent's Perm, without disturbing an existing symlink marker ("l"). Extract already read isexe correctly when writing files back to disk, so no change was needed there. Out of scope, left untouched: symlink (islink) handling, and executable-bit CHANGE detection on an already-tracked file whose mode changes without going through Manage again (Fossil's belt-and-braces re-stat at commit time / VFileIsExec path) — that path appears to still be missing here and should be tracked separately if it matters. check-in: 76dd5241c2 user: [email protected] tags: trunk | |
|
2026-06-09
| ||
| 06:11 | fix checkout portability under ncruces (#39) check-in: 6abfdc7c25 user: [email protected] tags: trunk | |
|
2026-05-12
| ||
| 20:01 |
feat: CreateOpts.ProjectCode — accept explicit project-code on Create (#31)
* feat: CreateOpts.ProjectCode — accept explicit project-code on Create Downstream EdgeSync coordinates a shared project-code across sibling Fossil repos created on different machines so its NATS sync subject ("<prefix>.<project-code>.sync") matches across leaves. Today every Create generates a fresh project-code, so commits never propagate. Add CreateOpts.ProjectCode: empty preserves current behavior; non-empty must be 40-char lowercase hex (^[0-9a-f]{40}$) — the same format generated project-codes use. Validation runs at the top of libfossil.Create before any disk I/O, so invalid input cannot leave a half-written file behind. Server-code stays auto-generated. Threaded through repo.Create / repo.CreateWithEnv / db.SeedConfig. SeedConfig validates as defense-in-depth (public package, multiple callers possible). Clone accepting ProjectCode is out of scope; will follow separately if EdgeSync needs it. * test: round-trip explicit ProjectCode through fossil rebuild Asserts that vanilla `fossil` (2.28) accepts a Go-side repo created with an explicit ProjectCode, and that `fossil sql` reads back the same value we set. Closes the gap where the existing TestCreate_FossilValidation only covered the generated-project-code path. check-in: 6b66ea6b2d user: [email protected] tags: trunk, v0.6.1 | |
|
2026-04-19
| ||
| 16:30 |
Initial open source release
libfossil is a pure-Go implementation of the Fossil SCM, reading and writing the same .fossil SQLite repository format as upstream Fossil. This is the first public release of code that previously lived in a private repository. The module path is github.com/danmestas/libfossil and the binary is named libfossil to avoid $PATH collisions with upstream fossil. Included: - Repo lifecycle (create, clone), checkout/checkin, timeline, merge, diff, annotate, shun, tag, branch, bisect, stash, undo/redo - Sync-over-HTTP protocol (client and handler) - Two SQLite drivers: modernc (pure Go, default) and ncruces (wasm-capable) - Observer interfaces (SyncObserver, CheckoutObserver) with an optional OpenTelemetry implementation in observer/otel (separate submodule) - Deterministic simulation testing harness with BUGGIFY fault injection - Kong-based embeddable CLI under cli/ and a standalone binary under cmd/libfossil/ check-in: f5744359d3 user: [email protected] tags: trunk | |