diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index a9b402f3a..f581ff1d0 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -216,9 +216,9 @@ there is no promised all-phase 30-second hard deadline. See | notes | partial | `add` / `append` / `copy` / `edit` / `show` / `list` / `remove` / `merge` supported; `--ref` supported; `notes merge` is a 2-way merge of the flat note rows (Libra notes are SQLite-backed, not commit-backed trees) with `--strategy=manual` (default; aborts on a conflicting note — no NOTES_MERGE worktree)/`ours`/`theirs`/`union`/`cat_sort_uniq`; `prune` (remove notes whose annotated object no longer exists in the object store — `-n`/`--dry-run` and `-v`) and `get-ref` (print the active notes ref) supported; the interactive editor fallback for `add`/`edit`/`append` when no `-m`/`-F` is given is supported (`edit` pre-fills the existing note; notes preserve `#` lines — not stripped as comments) | | cherry-pick | partial | one-or-more commit replay, preserving the picked commit's author metadata while using current committer metadata (including `GIT_COMMITTER_*` date/identity overrides), de-signing source messages before deriving the replayed body/subject, `-n/--no-commit` (now also for multi-commit), `-x`, `-s/--signoff`, `-e/--edit`, `-m/--mainline`, `--ff`, `-S/--gpg-sign`, `--allow-empty`, `--allow-empty-message`, `--keep-redundant-commits`, `--empty=` (`stop` default / `drop` skips a redundant pick / `keep` records the empty commit — == `--keep-redundant-commits`), `--cleanup=` (`strip`/`whitespace`/`verbatim`/`scissors`/`default` message cleanup — cleans the body/edited buffer first, then appends `-x`/`Signed-off-by` trailers; `default`/`scissors` fall back to `whitespace` without an editor), repeatable last-wins `-X/--strategy-option ours|theirs` (favor only conflicting hunks while retaining clean changes; whole-path add/delete conflicts choose the requested side), and the SQLite conflict sequencer (`--continue`/`--skip`/`--abort`/`--quit` with line-level three-way conflict markers — diverging hunks only, like Git; delete/modify and binary fall back to whole-file; the shared `merge.conflictStyle=merge|diff3|zdiff3` renderer is honored (including postimage refinement and uniformly CRLF marker lines; unknown values fail before index/worktree writes) — and a merge/rebase mutex) supported; `--rerere-autoupdate` is honoured (when `rerere.enabled` it makes the rerere hook stage a replayed resolution; a no-op with rerere off); custom `--strategy` values remain explicitly rejected and unimplemented; cherry-pick's conflict state now lives in the unified `sequence_state` table (lore.md 2.6 — the lazy `cherry_pick_state` DDL and the `revert_sequence` orphan are retired by migration 2026070401; storage change is transparent). lore.md 2.6 (unified sequencer): any in-progress multi-step sequence (merge / revert / cherry-pick / rebase) now REJECTS starting a DIFFERENT one with `LBR-CONFLICT-002` naming the blocking op — a symmetric cross-op mutex (previously only merge/rebase blocked an in-progress cherry-pick); the in-progress op's own `--continue`/`--abort`/`--skip` stay available, and `libra status` surfaces a non-merge sequence in progress. Submodules are a permanent non-goal (ADR-MG-01 / declined `D24`): a pick whose three-way inputs (parent tree / index / picked tree) record different `160000` gitlink ids is refused before any write with `LBR-UNSUPPORTED-001` naming the path, through the same guard `merge` and `rebase` use. A new pick refuses to start on an unmerged index (exit 128, `LBR-CONFLICT-001`, as in Git) before any write. A pick that would overwrite an untracked file is refused before that commit writes anything, and `--continue` re-attempts it. A multi-commit sequence is recorded before its first commit lands and survives interruptions and non-conflict stops, `--ff` carries over to resumed picks, and an interrupted `--skip`/`--abort` must be re-run (`--continue` refuses with `LBR-REPO-003`). As in Git, a later reset that clears unresolved index stages ends a stopped single-commit pick; in a multi-commit sequence it marks the stopped commit concluded and keeps the rest, and `--continue` refuses until the sequence is skipped or quit. | A later commit ends a stopped single-commit pick the same way. `--continue` skips a stop concluded outside the sequence. Conflict markers use Git-form labels: `<<<<<<< HEAD` / `>>>>>>> ()`; under `diff3` the ancestor is `parent of ()`. a later reset ends a stopped single-commit pick a later commit ends a stopped single-commit pick --continue skips a stop concluded outside the sequence conflict markers label the picked side as () worktree materialization is mode-aware — files are created with the entry mode's permission bits (`100755` executable, `100644` plain) under the process `umask`, atomically replaced, and the index/tree entries keep the mode (plan issues/470 FM-02). | | push | partial | branch/tag update, multi-refspec, delete (`-d`/`--delete` or a `:` refspec), `--tags`, and `--mirror` supported; `--force-with-lease[=[:]]` (validates the remote still matches the tracking-ref/expected OID before sending; conflicts with `--force`) and `--porcelain` (machine-readable per-ref lines; conflicts with `--json`/`--machine`) supported; `--atomic` supported (advertises the `atomic` capability so the remote applies all ref updates together; refused up-front if the remote does not advertise `atomic`); `--push-option`/`-o ` supported (sends a push-options section gated on the remote's `push-options` capability); `--follow-tags` supported (also pushes annotated tags reachable from a pushed ref and missing on the remote); `--signed` supported (builds a GPG-signed push certificate via the vault signer, gated on the remote's `push-cert` capability/nonce); `--no-progress` supported (suppresses the "Compressing objects" / "Writing objects" progress meter on stderr, like `git push --no-progress`); `--no-verify` accepted as a **no-op** (Libra runs no client-side `pre-push` hook; declined-decision D3); `--no-thin` spells the self-contained default explicitly. local file remote rejected — intentional (see [docs/development/commands/_compatibility.md#d2-本地-file-remote-的-push](docs/development/commands/_compatibility.md#d2-本地-file-remote-的-push)); lore.md 2.10 refinements: `--force-if-includes` is REAL (with the All/Ref lease forms it additionally requires the remote-tracking tip to be integrated locally — pushing the tip, a descendant, or a tip REACHABLE from the pushed branch's reflog; conservative rejections, whole-push error rather than a per-ref porcelain row — documented divergence; silent no-op with the exact lease form or no lease, Git parity); `--thin` is REAL (REF_DELTA entries against server-known bases — the advertised old tips; self-contained Libra delta encoder with git-convention 64KiB copy ops since git-internal's delta module is private; net-win + 8MiB caps, full fallback on any miss; verified round-trip against real `git receive-pack` on BOTH unpack paths); self-contained remains the DEFAULT (`push.thin` config unsupported — intentionally different from git's thin-by-default, revisit recorded); lease tracking lookup consults both tracking-ref naming conventions (fully-qualified `refs/remotes//` as written by clone/fetch/push, plus a legacy short-name probe for repositories written by older binaries); `--delete`/`:ref` deletion resolves short names against the remote's advertised refs (`refs/heads/` then `refs/tags/`, ambiguity refused like Git's `dst refspec matches more than one`) and FAILS a deletion naming a ref the remote does not advertise (`unable to delete '': remote ref does not exist`) instead of reporting `Everything up-to-date` (#465); real-git-server interop matrix in L1 (capability degrades refuse cleanly with nothing sent; push-options round-trip via pre-receive hook); a configured local upstream (`branch..remote=.`) is refused before any network write with `LBR-CLI-003` / exit 129 (Git `push` is 128 — intentional, ADR-HF-08 / issues/480 HP-16); refuses a local upstream (`remote=.`) | -| fetch | partial | repository/refspec supports short sources, exact `:`, one wildcard per side, case-insensitive `remote..fetch`, and exact destinations limited to `refs/heads/*` / `refs/remotes//*` (reserved `HEAD` is refused). Destination/reflog/remote-HEAD writes are transactional; a checked-out destination in any worktree and non-fast-forward mappings without `+`/`--force` fail closed. `--all`, `--depth`, `--dry-run` (no download/writes; unknown pre-download ancestry is not reported forced), `-v`/`--verbose`, `--porcelain` (rejects `--json`), tags, `-f`/`--force`, `--no-auto-gc`, `--no-progress`, `--notes`, and `FETCH_HEAD`/`--append` are supported. `--prune`/`-p` deletes tracking refs not live under the effective configured destination mapping; one-off explicit refspecs preserve configured mappings plus ordinary advertised tracking destinations; empty advertisements skip prune. Deletes/reflogs are transactional and `--dry-run` only reports. `--no-prune` overrides strict `remote..prune` then `fetch.prune` defaults. `--refmap`, `--atomic`, and shallow-expansion flags remain deferred. Network fetches have bounded connect, idle/read, and first-byte timeouts; local remotes are exempt; a configured local upstream (`branch..remote=.`) is refused before any network or `FETCH_HEAD` write with `LBR-CLI-003` / exit 129 (Git 2.54 `fetch` can operate on it — intentional, ADR-HF-08 / issues/480 HP-16); refuses a local upstream (`remote=.`) Local Git `--depth` uses that same shortest-distance union boundary (issues/474 CL-04). A bare `fetch ` whose configured refspec does not map that ref still updates objects and `FETCH_HEAD` but does not create a tracking ref (issues/474 CL-05). | +| fetch | partial | repository/refspec supports short sources, exact `:`, one wildcard per side, case-insensitive `remote..fetch`, and exact destinations limited to `refs/heads/*` / `refs/remotes//*` (reserved `HEAD` is refused). Destination/reflog/remote-HEAD writes are transactional; a checked-out destination in any worktree and non-fast-forward mappings without `+`/`--force` fail closed. `--all`, `--depth`, `--dry-run` (no download/writes; unknown pre-download ancestry is not reported forced), `-v`/`--verbose`, `--porcelain` (rejects `--json`), tags, `-f`/`--force`, `--no-auto-gc`, `--no-progress`, `--notes`, and `FETCH_HEAD`/`--append` are supported. `--prune`/`-p` deletes tracking refs not live under the effective configured destination mapping; one-off explicit refspecs preserve configured mappings plus ordinary advertised tracking destinations; empty advertisements skip prune. Deletes/reflogs are transactional and `--dry-run` only reports. `--no-prune` overrides strict `remote..prune` then `fetch.prune` defaults. `--refmap`, `--atomic`, and shallow-expansion flags remain deferred. Network fetches have bounded connect, idle/read, and first-byte timeouts; local remotes are exempt; a configured local upstream (`branch..remote=.`) is refused before any network or `FETCH_HEAD` write with `LBR-CLI-003` / exit 129 (Git 2.54 `fetch` can operate on it — intentional, ADR-HF-08 / issues/480 HP-16); refuses a local upstream (`remote=.`) Local Git `--depth` uses that same shortest-distance union boundary (issues/474 CL-04). A bare `fetch ` whose configured refspec does not map that ref still updates objects and `FETCH_HEAD` but does not create a tracking ref (issues/474 CL-05); a remote URL that points at a Git v2 bundle is re-read on each fetch (issues/474 CL-10), including `--prune` and `--dry-run` | | format-patch | partial | `A..B`/single-revision ranges, exact `-1 [rev]`, `--root [rev]`, and `--ignore-if-in-upstream` stable patch-id suppression are supported; merge commits are skipped. Output controls include `-o`/`--output-directory`, `--stdout`, `-n`/`--numbered`, `--start-number`, `--numbered-files`, `--suffix`, `--subject-prefix`, `--cover-letter`, `--thread`/`--no-thread`, `--in-reply-to`, `-v`/`--reroll-count`, `-s`/`--signoff` and `--no-signoff`, `--full-index`, `--minimal` (equivalent to Libra's already-shortest default Myers), `--histogram`, `--src-prefix`/`--dst-prefix`, `--no-stat`, `--keep-subject`, `--zero-commit`, signature controls, and RFC 2047 header encoding. `format.subjectPrefix`, `format.signOff`, `format.outputDirectory`, and `format.suffix` use the strict local→global→system cascade with explicit CLI precedence (`--stdout` bypasses outputDirectory). Mail controls include repeatable `--to`/`--cc`, `--no-to`/`--no-cc`, `--from`, `--notes[=]`, and mutually exclusive `--attach`/`--inline`; generated plain and MIME output is consumed by real Git `am`, and Git plain format-patch output is consumed by Libra `am`. `--base ` emits a `base-commit:` plus oldest-first stable `prerequisite-patch-id:` lines on the last patch/cover letter; the base must be an ancestor, `--base=auto` is rejected, and binary prerequisite ids are not guaranteed to match Git. `--interdiff` and `--range-diff` remain unexposed (`--force` is not a Git format-patch flag). | -| pull | partial | fetch + fast-forward/three-way merge supported; the merge phase inherits the unresolved-index guard even for an already-up-to-date target: without merge state, `LBR-CONFLICT-002` carries `phase: merge` and resolution + `add` + ordinary `commit` guidance, preserving HEAD/index/worktree in that phase; fetch may already have downloaded objects and updated remote-tracking refs (the merge phase inherits `merge`'s recursive virtual ancestor for criss-cross histories and its `LBR-UNSUPPORTED-001` depth/width refusals — deeper than 20 levels or more than 32 bases at one level, MG-02); `--ff-only`, `--rebase`, `--no-rebase` (countermands `--rebase`, last wins), `--ff`, `--no-ff` (forces a merge commit), and `pull.rebase` / `branch..rebase` / `pull.ff=true|false|only` defaults when CLI flags are absent (config cascade local→global→system, case-insensitive variable matching, local/global encrypted values decrypted, legacy config rows honored; empty/invalid values fail before fetch with `LBR-CLI-002`, local/global config reads with `LBR-IO-001`, unsupported `merges|interactive` modes produce an explicit `LBR-CLI-002`, system-scope failures skipped), fetch `--depth` (shallow pull), `--squash` (even on conflict: no merge state or HEAD movement; resolve + stage + plain single-parent `commit`, with merge control actions unavailable), `--no-commit`, `--commit` (commit the merge result; may be combined with `--ff`/`--no-ff`/`--ff-only`, does not override fast-forward policy, and is last-one-wins with `--no-commit`), `--autostash` (stash tracked changes before integrating; restoration follows the selected operation's lifecycle), `--no-progress` (forward `--no-progress` to the fetch, suppressing its progress meter), and `--notes` (forward to the fetch — import the `refs/notes/deps` dependency graph from a local Libra upstream; default OFF, D17) exposed; `pull --autostash` on the MERGE path now rides the merge-owned autostash (held on non-squash conflict, applied by merge --continue/--abort; conflicted squash saves directly to `stash list` without applying, preserving unresolved stages and working-tree files for resolution + commit before `stash pop`; save failure warns and retains the held sidecar; `merge.autostash` config honored) — the rebase path keeps the legacy push/pop wrap; no-upstream pull renders Git-style tracking advice (`libra pull ` / `libra branch --set-upstream-to=...`); runs in a linked worktree in ALL modes since W2 (merge/ff v0.19.35, rebase v0.19.42, and `--rebase --autostash` v0.19.54 — the autostash wrap uses the stash stack-lock + by-id CAS protocol and pops exactly its own entry) Submodules are a permanent non-goal (ADR-MG-01 / declined `D24`): both integration paths inherit the guard — a merge or a `--rebase` replay that would have to arbitrate a `160000` gitlink is refused with `LBR-UNSUPPORTED-001` before the autostash and before any index/worktree write.; the merge phase also inherits merge's directory/file collision handling (MG-04: `~HEAD` / `~` move + `CONFLICT (file/directory)` line in human output; JSON/machine stdout stays clean), its per-side rename detection (MG-05), and `merge.renormalize` on real three-way merge paths (no pull `-X` override; fast-forward/up-to-date do not consume it); a configured local upstream (`branch..remote=.`) is refused before any network or `FETCH_HEAD` write with `LBR-CLI-003` / exit 129 (Git 2.54 `pull` can operate on it — intentional, ADR-HF-08 / issues/480 HP-16); refuses a local upstream (`remote=.`) | +| pull | partial | fetch + fast-forward/three-way merge supported; the merge phase inherits the unresolved-index guard even for an already-up-to-date target: without merge state, `LBR-CONFLICT-002` carries `phase: merge` and resolution + `add` + ordinary `commit` guidance, preserving HEAD/index/worktree in that phase; fetch may already have downloaded objects and updated remote-tracking refs (the merge phase inherits `merge`'s recursive virtual ancestor for criss-cross histories and its `LBR-UNSUPPORTED-001` depth/width refusals — deeper than 20 levels or more than 32 bases at one level, MG-02); `--ff-only`, `--rebase`, `--no-rebase` (countermands `--rebase`, last wins), `--ff`, `--no-ff` (forces a merge commit), and `pull.rebase` / `branch..rebase` / `pull.ff=true|false|only` defaults when CLI flags are absent (config cascade local→global→system, case-insensitive variable matching, local/global encrypted values decrypted, legacy config rows honored; empty/invalid values fail before fetch with `LBR-CLI-002`, local/global config reads with `LBR-IO-001`, unsupported `merges|interactive` modes produce an explicit `LBR-CLI-002`, system-scope failures skipped), fetch `--depth` (shallow pull), `--squash` (even on conflict: no merge state or HEAD movement; resolve + stage + plain single-parent `commit`, with merge control actions unavailable), `--no-commit`, `--commit` (commit the merge result; may be combined with `--ff`/`--no-ff`/`--ff-only`, does not override fast-forward policy, and is last-one-wins with `--no-commit`), `--autostash` (stash tracked changes before integrating; restoration follows the selected operation's lifecycle), `--no-progress` (forward `--no-progress` to the fetch, suppressing its progress meter), and `--notes` (forward to the fetch — import the `refs/notes/deps` dependency graph from a local Libra upstream; default OFF, D17) exposed; `pull --autostash` on the MERGE path now rides the merge-owned autostash (held on non-squash conflict, applied by merge --continue/--abort; conflicted squash saves directly to `stash list` without applying, preserving unresolved stages and working-tree files for resolution + commit before `stash pop`; save failure warns and retains the held sidecar; `merge.autostash` config honored) — the rebase path keeps the legacy push/pop wrap; no-upstream pull renders Git-style tracking advice (`libra pull ` / `libra branch --set-upstream-to=...`); runs in a linked worktree in ALL modes since W2 (merge/ff v0.19.35, rebase v0.19.42, and `--rebase --autostash` v0.19.54 — the autostash wrap uses the stash stack-lock + by-id CAS protocol and pops exactly its own entry) Submodules are a permanent non-goal (ADR-MG-01 / declined `D24`): both integration paths inherit the guard — a merge or a `--rebase` replay that would have to arbitrate a `160000` gitlink is refused with `LBR-UNSUPPORTED-001` before the autostash and before any index/worktree write.; the merge phase also inherits merge's directory/file collision handling (MG-04: `~HEAD` / `~` move + `CONFLICT (file/directory)` line in human output; JSON/machine stdout stays clean), its per-side rename detection (MG-05), and `merge.renormalize` on real three-way merge paths (no pull `-X` override; fast-forward/up-to-date do not consume it); a configured local upstream (`branch..remote=.`) is refused before any network or `FETCH_HEAD` write with `LBR-CLI-003` / exit 129 (Git 2.54 `pull` can operate on it — intentional, ADR-HF-08 / issues/480 HP-16); refuses a local upstream (`remote=.`); a remote URL that points at a Git v2 bundle is re-read on each pull so a replaced bundle can fast-forward (issues/474 CL-10) | | dirty | intentionally-different | Advisory dirty-set marks (lore.md 1.1): `libra dirty ` upserts manual marks into the `working_dirty` SQLite cache (no file reads, no index writes; over-report-only, repo-escaping paths refused atomically), `--list` shows the cache + freshness. The cache is rebuilt only by `status --scan`, consumed by `status --cached`/`--check-dirty`, and NEVER read or written by default `status`. Git has no equivalent surface. Exit 0 / 128 / 129 | | diff | partial | staged/old-new/pathspec/name/stat/numstat/shortstat/summary/output/algorithm, default working-tree diff excludes untracked paths (including untracked `.libraignore`) from machine output and `--quiet`/`--exit-code` decisions, working-tree symlink target-byte diffs (dangling symlinks are treated as existing symlinks, not deleted paths), and `--exit-code`/`-s`/`--no-patch`/`-z`/`-U` (`--unified=`; lines of context in the patch, default 3)/`-w` (`--ignore-all-space`; re-diff ignoring whitespace — whitespace-only changes drop out and counts/name/JSON reflect the re-diff)/`-b` (`--ignore-space-change`; ignore changes in whitespace amount)/`--ignore-space-at-eol` (ignore trailing-whitespace changes only)/`--ignore-cr-at-eol` (ignore a carriage return at end of line — a CRLF↔LF-only change drops out; the weakest whitespace flag, subsumed by `-w`/`-b`/`--ignore-space-at-eol`; approximation vs Git: compares with ALL trailing CRs stripped rather than Git's non-transitive allow-one-remaining-CR rule, so only pathological multi-CR endings differ)/`--ignore-blank-lines` (ignore changes whose lines are all empty — a blank-only change drops out; a blank within ` -> bytes`, and `--numstat` shows `-`/`-`)/`--no-ext-diff` (disable the external diff driver for this run, forcing the built-in engine)/`--color-moved[=]` (color moved lines — deleted in one place, added in another — distinctly in colored output: removed → bold magenta, added → bold cyan; bare `--color-moved` and the block modes `default`/`zebra`/`blocks`/`dimmed-zebra` are accepted but approximated by `plain`, coloring every moved line, since Git's conservative moved-block significance/zebra striping is not byte-reproducible; only affects colored output, and `--color=always` now forces diff color even when piped)/`--no-color-moved` (turn it off — the default, countermands an earlier `--color-moved`)/`--relative[=]` (restrict the diff to a directory and strip that prefix from displayed paths; bare `--relative` uses the cwd)/`--no-relative` (a no-op on its own, but takes precedence over `--relative` when both are given)/`--no-indent-heuristic` (accepted no-op — Libra's diff applies no indent heuristic)/`--textconv` (run textconv filters, on by default like Git: a file whose `diff=` attribute from Git/Libra attributes sources (`core.attributesFile`, per-directory `.gitattributes`, `.libra_attributes`, and the worktree-local `info/attributes` — `.libra/info/attributes`, plus `.git/info/attributes` in Git- or dual-layout trees) names a driver with a configured `diff..textconv` command has each side converted by that command before diffing — stat/numstat/name/JSON all reflect the converted content; matched via the shared attributes matcher, last-match-wins, with `-diff`/`!diff`/bare `diff` clearing an earlier driver, and a rename resolving each side's driver independently; a failing textconv command is a fatal error (`LBR-IO-001`, like Git's "unable to read files to diff") rather than a silent raw fall-back; skipped under `--check` and when `diff.external` is active)/`--no-textconv` (diff raw content; countermands `--textconv`)/`-M[]` / `--find-renames[=]` (rename detection — a deleted + added pair similar enough is folded into one rename with `similarity index N%` / `rename from`/`to`, and `R` / brace-compacted `old => new` paths across name-status/numstat/stat/summary; the similarity index matches Git for real content (chunked like Git's rename spanhash but hashed with FNV-1a rather than Git's `HASHBASE`, so only contrived hash-collision inputs can differ); bare `-M` is 50%, a bare integer is read as `0.` like Git so `-M5`=50%/`-M100`=10%, `-M%` is a literal percent and `-M100%` is exact-only (integer threshold math, no float rounding), invalid scores are a usage error; enabled at 50% by default (matching Git), honoring `diff.renames` through the strict cascade — truthy or unset enables 50%, false disables detection, and `copies` degrades to renames; `diff.context` accepts Git integers and sets the default `-U` width; the rename `index` line uses mode `100644`, rename pairing is score-ranked greedy with a same-basename tie-break (Git's diffcore-rename runs a same-basename pre-pass that can prefer a lower-scored same-basename pair, so the chosen old/new pairs for a multi-rename set can differ), and a pathspec cannot directly follow a bare `-M`/`--find-renames` — put it before the flag or after `--`)/`--no-renames` (turn rename detection off, overriding the default, `diff.renames`, and an earlier `-M`) and `--ext-diff` / `diff.external` (route each file's patch through an external diff driver via Git's GIT_EXTERNAL_DIFF protocol — `cmd path old-file old-hex old-mode new-file new-hex new-mode`, run through the shell; a working-tree new side reports an all-zero hash; `--no-ext-diff` disables it and `--stat`/name/numstat/`-s`/`--check` bypass it) and `--word-diff[=plain|color|porcelain|none]` (re-render the patch at word granularity — `plain` brackets `[-removed-]`/`{+added+}`, `color` highlights in a terminal, `porcelain` is the machine format, `none` disables; whitespace-delimited words; matches Git's structure but, like all Libra diffs, the exact token grouping can differ on ambiguous cases and the `@@` headers use Libra's unified-diff format) supported; the working-tree scan liveness hint (`Scanning working tree ...`, stderr) is deferred until the scan has run longer than 2s (fast local scans print nothing, unlike Git which shows no diff progress at all), only guards the unstaged worktree comparison (never `--staged`/rev-vs-rev/`--quiet`/`--json`/`--machine`), TTY-gated under `--progress=auto` so redirected stderr/CI logs never receive it, erased on completion on a TTY (an explicit `--progress=text` with redirected stderr keeps the bare line text but receives no ANSI escapes), and replaced by immediate `diff_scan.start` NDJSON events under `--progress=json` (#466); positional revisions follow Git's `diff [...] [--] [...]` grammar — `diff A` (A vs worktree), `diff A B` (≡ `A..B`), two-dot `A..B` and three-dot `A...B` glued ranges (`A...B` diffs from the merge base of `A` and `B` to `B`; unrelated histories error), `diff --staged ` (commit vs index; a range or second revision with `--staged` is rejected), a `--` separator forcing the path reading, and Git's two disambiguation errors (`ambiguous argument '': both a revision and a filename`; `unknown revision or path not in the working tree`, glob pathspecs exempt) — these exit 129 (`LBR-CLI-002`/`LBR-CLI-003`, Libra's CLI-error convention) where Git exits 128; more than two revisions (Git ≥2.38's merge combined-diff form) is declined; when `--old`/`--new` is given positionals stay pathspecs (documented Libra-only leniency); `--color-words[=]` is exposed as the redirected-output-aware `--word-diff=color` shorthand with an optional Rust-regex tokenizer (explicit global `--color=never` wins); `--word-diff-regex=` defines non-overlapping comparison words and implies plain mode when standalone, while the indent heuristic (`--indent-heuristic`) is not exposed; `diff.renameLimit` (per-side cap on the quadratic inexact pass, strict cascade, `0` = uncapped, default 1000 like Git — the cheap stages run BEFORE the gate, so exceeding it keeps exact AND scored unique-basename renames, skips only the exhaustive stage, and warns) and the Libra-only `diff.renameComparisonBudget` (total similarity-comparison budget charged per comparison across the unique-basename and exhaustive stages, `0` = unlimited default — exhaustion stops all further comparisons and discards the exhaustive pass; pairs already scored (exact + already-paired unique-basename) survive, with a warning) bound rename detection with fail-closed validation (`LBR-CLI-002`) with `core.filemode=true` (Unix default) a mode-only worktree change renders the mode header and a `--summary` `mode change` line, while `false` does not diff mode-only differences (plan-20260918 FM-04) | | diff-tree | partial | `diff-tree [-- ...]` diffs two trees by delegating to the shared engine (`diff --old a --new b --no-renames`); as Git plumbing it does not inherit porcelain `diff.renames` or default rename detection. Follows Git plumbing exit semantics — exit 1 when there are differences, 0 when clean, 128 on error. Path limiters require a `--` separator (stricter than Git's bare paths). Single-commit `diff-tree ` (vs parent), `-r`/`-t`/`--stdin`, raw output, and the porcelain flag surface are not exposed | diff --git a/Cargo.lock b/Cargo.lock index 5fde36a54..974a405c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3783,7 +3783,7 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libra" -version = "0.23.57" +version = "0.23.58" dependencies = [ "anyhow", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index 950c1be96..201a49d9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libra" -version = "0.23.57" +version = "0.23.58" edition = "2024" license = "MIT" description = "AI agent-native version control system with Git on-disk compatibility, SQLite-backed metadata, and tiered cloud storage" diff --git a/docs/commands/fetch.md b/docs/commands/fetch.md index 8e30d4556..13bec0845 100644 --- a/docs/commands/fetch.md +++ b/docs/commands/fetch.md @@ -22,8 +22,10 @@ one source ref and may map it to an exact local destination (`:`). Whe explicit refspec is given, `remote..fetch` entries are honored; if none exist, all advertised branches use the default `refs/remotes//*` mapping. -Fetch supports SSH, HTTPS, local file, and `git://` transports. Vault-backed SSH keys -are loaded automatically when configured via `vault.ssh..privkey`. +Fetch supports SSH, HTTPS, local file, Git v2 bundle files, and `git://` +transports. A remote URL that points at a bundle is re-read on every fetch +(including `--prune` and `--dry-run`). Vault-backed SSH keys are loaded +automatically when configured via `vault.ssh..privkey`. ## Global Config Schema Guard diff --git a/docs/commands/pull.md b/docs/commands/pull.md index e7b6367e2..90c923929 100644 --- a/docs/commands/pull.md +++ b/docs/commands/pull.md @@ -10,7 +10,7 @@ libra pull [--ff-only] [--ff] [--no-ff] [--squash] [--no-commit] [--commit] [--a ## Description -`libra pull` combines `fetch` and the same merge engine used by `libra merge`. It downloads new objects, updates remote-tracking refs, and then integrates the selected upstream into the current branch. +`libra pull` combines `fetch` and the same merge engine used by `libra merge`. It downloads new objects, updates remote-tracking refs, and then integrates the selected upstream into the current branch. A remote URL that points at a Git v2 bundle is re-read on every pull, so replacing the bundle file can fast-forward the current branch. With `--rebase` (`-r`), the integration step instead replays local-only commits on top of the fetched upstream tip. This is equivalent to `libra fetch` followed by `libra rebase `. diff --git a/docs/commands/zh-CN/fetch.md b/docs/commands/zh-CN/fetch.md index f175b9de7..1266c3173 100644 --- a/docs/commands/zh-CN/fetch.md +++ b/docs/commands/zh-CN/fetch.md @@ -14,7 +14,7 @@ libra fetch [OPTIONS] [ []] 不带参数调用时,它从当前分支配置的 upstream 获取。已配置的本地 upstream(`branch..remote=.`)会在任何网络或 `FETCH_HEAD` 写入前被拒绝(`LBR-CLI-003`,退出 129);Git 2.54 的 `fetch` 可以对本地 upstream 操作——该支持延后到 [issues/480 HP-16](https://github.com/libra-tools/libra/issues/480)。显式仓库参数 `.` 仍走现有的 `remote '.' not found`。给出 `--all` 时,会依次获取每个已配置远程。指定某个 `` 时,只联系该远程。可选 `` 选择一个源引用,并可用 `:` 精确映射到本地目标。未显式给出 refspec 时会遵守 `remote..fetch`;该配置不存在时才回退为把所有远程分支映射到 `refs/remotes//*`。 -Fetch 支持 SSH、HTTPS、本地文件和 `git://` 传输。配置了 `vault.ssh..privkey` 时,会自动加载 vault-backed SSH 密钥。 +Fetch 支持 SSH、HTTPS、本地文件、Git v2 bundle 文件和 `git://` 传输。远程 URL 指向 bundle 时,每次 fetch(含 `--prune` 与 `--dry-run`)都会重新读取该文件。配置了 `vault.ssh..privkey` 时,会自动加载 vault-backed SSH 密钥。 ## 全局配置 Schema 保护 diff --git a/docs/commands/zh-CN/pull.md b/docs/commands/zh-CN/pull.md index 38aeffbd6..ce889b612 100644 --- a/docs/commands/zh-CN/pull.md +++ b/docs/commands/zh-CN/pull.md @@ -10,7 +10,7 @@ libra pull [--ff-only] [--ff] [--no-ff] [--squash] [--no-commit] [--commit] [--a ## 说明 -`libra pull` 组合了 `fetch` 和 `libra merge` 使用的同一合并引擎。它下载新对象,更新远程跟踪引用,然后将选中的 upstream 集成到当前分支。 +`libra pull` 组合了 `fetch` 和 `libra merge` 使用的同一合并引擎。它下载新对象,更新远程跟踪引用,然后将选中的 upstream 集成到当前分支。远程 URL 指向 Git v2 bundle 时,每次 pull 都会重新读取该文件,替换 bundle 后可以快进当前分支。 使用 `--rebase`(`-r`)时,集成步骤会改为在获取到的 upstream tip 之上重放仅本地提交。这等价于 `libra fetch` 后跟 `libra rebase `。 diff --git a/docs/development/plan/issues/474.md b/docs/development/plan/issues/474.md index b87318051..7c4e7b9ad 100644 --- a/docs/development/plan/issues/474.md +++ b/docs/development/plan/issues/474.md @@ -1274,7 +1274,7 @@ ER-CL-02 接入 / 豁免清单(2026-09-23): **Task type:** `implementation` -**Lifecycle / Acceptance:** `pending` / 空 +**Lifecycle / Acceptance:** `in-progress` / `locally-accepted` **Description:** 按 ADR-CL-06 第 3 条,当 remote url 指向 bundle 文件时,`fetch`/`pull` 读取该文件的当前内容作为广告与 pack,支持普通、`--prune` 与 `--dry-run`。唯一行为轴是「bundle remote 的增量更新」。 @@ -1290,17 +1290,17 @@ ER-CL-02 接入 / 豁免清单(2026-09-23): **Acceptance criteria:** -- [ ] 判据矩阵 M-BFETCH H1(克隆后 fetch)。 -- [ ] 替换 bundle 后 fetch 与 pull 通过 M-BFETCH H2。 -- [ ] bundle 缺失通过 M-BFETCH H3。 -- [ ] `--prune` 与 `--dry-run` 通过 M-BFETCH H4。 -- [ ] ER-06a 同步:`docs/commands/fetch.md`、`docs/commands/pull.md` 与 zh-CN、`COMPATIBILITY.md:223`、`:225` 原行追加、网站 `fetch.en.md`、`pull.en.md`。 +- [x] 判据矩阵 M-BFETCH H1(克隆后 fetch)。 +- [x] 替换 bundle 后 fetch 与 pull 通过 M-BFETCH H2。 +- [x] bundle 缺失通过 M-BFETCH H3。 +- [x] `--prune` 与 `--dry-run` 通过 M-BFETCH H4。 +- [x] ER-06a 同步:`docs/commands/fetch.md`、`docs/commands/pull.md` 与 zh-CN、`COMPATIBILITY.md:223`、`:225` 原行追加、网站 `fetch.en.md`、`pull.en.md`。 **Verification:** -- [ ] `source .env.test && source .env.live-test && cargo nextest run --test command_test fetch_test`(new:`test_fetch_from_bundle_remote_matrix`) -- [ ] `source .env.test && source .env.live-test && cargo nextest run --test command_test pull_test`(new:`test_pull_from_bundle_remote_fast_forward`) -- [ ] `source .env.test && source .env.live-test && cargo nextest run --test compat_ledger_schema` +- [x] `source .env.test && source .env.live-test && cargo nextest run --test command_test fetch_test`(new:`test_fetch_from_bundle_remote_matrix`) +- [x] `source .env.test && source .env.live-test && cargo nextest run --test command_test pull_test`(new:`test_pull_from_bundle_remote_fast_forward`) +- [x] `source .env.test && source .env.live-test && cargo nextest run --test compat_ledger_schema` **Full-suite trigger:** `none` diff --git a/docs/development/plan/plan-status.md b/docs/development/plan/plan-status.md index f64061edb..1f605f0fa 100644 --- a/docs/development/plan/plan-status.md +++ b/docs/development/plan/plan-status.md @@ -61,7 +61,7 @@ |---|---|---|---| | [`issues/470.md`](issues/470.md) | 工作树物化丢失可执行位与 mode 变化检测 | 未启动 | FM-01/02/05(3 卡) | | [`issues/473.md`](issues/473.md) | `init` 与 Git 对齐 | 未启动 | IN-01..IN-12(12 卡) | -| [`issues/474.md`](issues/474.md) | clone 浅克隆完整性、bundle 源、bare 与 mirror 对齐 | 实施中 | CL-01 `done`/`complete`(`v0.23.47`);CL-02 `done`/`complete`(`v0.23.48`);CL-03 C 组 `v0.23.49`(`compat-network-remotes` 重跑排队中);CL-04..CL-09 `locally-accepted`(未 bump) | +| [`issues/474.md`](issues/474.md) | clone 浅克隆完整性、bundle 源、bare 与 mirror 对齐 | 实施中 | CL-01 `done`/`complete`(`v0.23.47`);CL-02 `done`/`complete`(`v0.23.48`);CL-03 C 组 `v0.23.49`(`compat-network-remotes` 重跑排队中);CL-04..CL-10 `locally-accepted`(未 bump) | | [`issues/475.md`](issues/475.md) | `config` Git 兼容参数层对齐 | 未启动 | CF-01..CF-15(15 卡) | | [`issues/476.md`](issues/476.md) | 工作树命令族与 Git 对齐 | **实施中** | WT-02 `v0.23.29` / WT-04 `v0.23.30` / WT-08 `v0.23.31` / WT-09 `v0.23.32` / WT-10 `v0.23.33` / WT-11 `v0.23.34` / WT-01 `v0.23.35`(`done`/`remote-pending`);WT-03 受 DEP-WT-08 阻塞;intent-to-add 已迁至 plan-20260918 | | [`issues/477.md`](issues/477.md) | 历史改写命令族与 Git 对齐 | **已收口** | HF-01..HF-31(31 卡)全 `done/complete`,聚合发布 v0.22.49;子 issue #495 | @@ -191,7 +191,8 @@ | **CL-07 shallow Git 源克隆** | **`in-progress`/`locally-accepted`** | **未 bump(等 CL-06 发布窗口)** | | **CL-08 bundle create 写入 HEAD** | **`in-progress`/`locally-accepted`** | **未 bump(等 CL-07 发布窗口)** | | **CL-09 clone 接受 bundle 源** | **`in-progress`/`locally-accepted`** | **未 bump(等 CL-08 发布窗口)** | -| CL-10..CL-15 | `pending` | 未 bump | +| **CL-10 以 bundle 为 remote 的 fetch/pull** | **`in-progress`/`locally-accepted`** | **未 bump(等 CL-09 发布窗口)** | +| CL-11..CL-15 | `pending` | 未 bump | ### 3.6 issues/476(工作树命令族) @@ -233,7 +234,7 @@ SBX-01..05 `done/locally-accepted`;**发布步按 DEFER-SBX-06 正式延后** - **当前正在执行:** `plan-20260907` B3-00(发布者:本会话执行该卡的 Agent)。`plan-20260925` SCAP-01 / SCAP-02 已 `done`/`complete`(`v0.23.55` / `fa3849e`,D 组全绿)。 - **SCAP-01 与 B3-00:** SCAP-01 已收口,`runtime.rs` 写集互斥结束;B3-00 继续在 `main` 上推进。 -- **下一步:** 完成 B3-00 focused VER / T-1 全量与发布。`issues/474` 的 `CL-09`..`CL-15` 正按窗口发布(合并后即开下一卡,不等待 release CDN)。 +- **下一步:** 完成 B3-00 focused VER / T-1 全量与发布。`issues/474` 的 `CL-10`..`CL-15` 正按窗口发布(合并后即开下一卡,不等待 release CDN)。 - **并行窗口(不在本执行指针):** `issues/476` WT-03 仍等 DEP-WT-08;`plan-20260918` 其余 add 卡、`plan-20260819` M2 仍登记为实施中,但不抢本卡的 `fsck.rs` 写集。 --- diff --git a/install.ps1 b/install.ps1 index 478358f5d..e026104dc 100644 --- a/install.ps1 +++ b/install.ps1 @@ -22,7 +22,7 @@ $ErrorActionPreference = "Stop" # One of the release version surfaces. `compat_version_surface_sync` pins it # to Cargo.toml: this value is substituted verbatim into the download URL, so # a stale value silently installs an old binary when -Version is not given. -$DefaultVersion = "v0.23.57" +$DefaultVersion = "v0.23.58" # Public-only trust anchor for stable-manifest verification. It deliberately # has no environment override: the install-smoke harness rewrites these # clearly-marked constants in a temporary COPY of this script. diff --git a/install.sh b/install.sh index 4ebacf286..9e95fa5da 100755 --- a/install.sh +++ b/install.sh @@ -18,7 +18,7 @@ INSTALL_DIR="${LIBRA_INSTALL_DIR:-$LIBRA_HOME/bin}" # user opts in with LIBRA_ALLOW_FALLBACK=1. Default behaviour is fail-fast so # offline installs cannot silently regress to a stale version. Bump this on # every release so the opt-in fallback remains useful. -DEFAULT_VERSION="v0.23.57" +DEFAULT_VERSION="v0.23.58" # Public-only trust anchor for stable-manifest verification. It deliberately # has no environment override: the install-smoke harness rewrites these # clearly-marked constants in a temporary COPY of this script, never through diff --git a/tests/command/fetch_test.rs b/tests/command/fetch_test.rs index e38b98484..60a0be3ef 100644 --- a/tests/command/fetch_test.rs +++ b/tests/command/fetch_test.rs @@ -2184,3 +2184,109 @@ fn test_fetch_depth_local_git_boundaries() { "fetch depth 2 boundary" ); } + +fn rev_parse_cli(repo: &Path, rev: &str) -> String { + let output = run_libra_command(&["rev-parse", rev], repo); + assert_cli_success(&output, &format!("rev-parse {rev}")); + String::from_utf8_lossy(&output.stdout).trim().to_string() +} + +fn commit_file_via_cli(repo: &Path, name: &str, contents: &str, message: &str) { + fs::write(repo.join(name), contents).expect("write commit file"); + assert_cli_success( + &run_libra_command(&["add", name], repo), + &format!("add {name}"), + ); + assert_cli_success( + &run_libra_command(&["commit", "-m", message, "--no-verify"], repo), + message, + ); +} + +/// M-BFETCH H1–H4: fetch against a bundle remote. +#[test] +fn test_fetch_from_bundle_remote_matrix() { + let src = create_committed_repo_via_cli(); + assert_cli_success( + &run_libra_command(&["branch", "dev"], src.path()), + "branch dev", + ); + let parent = tempdir().expect("bundle parent"); + let bundle = parent.path().join("remote.bundle"); + assert_cli_success( + &run_libra_command( + &["bundle", "create", bundle.to_str().unwrap(), "--all"], + src.path(), + ), + "create bundle", + ); + let dest = parent.path().join("cloned"); + assert_cli_success( + &run_libra_command( + &["clone", bundle.to_str().unwrap(), dest.to_str().unwrap()], + parent.path(), + ), + "clone from bundle", + ); + + let h1 = run_libra_command(&["fetch"], dest.as_path()); + assert_cli_success(&h1, "H1 fetch after clone"); + + let old_main = rev_parse_cli(&dest, "refs/remotes/origin/main"); + assert_cli_success( + &run_libra_command(&["rev-parse", "refs/remotes/origin/dev"], &dest), + "H4 origin/dev exists before prune", + ); + + commit_file_via_cli(src.path(), "next.txt", "next\n", "next"); + assert_cli_success( + &run_libra_command( + &["bundle", "create", bundle.to_str().unwrap(), "--all"], + src.path(), + ), + "replace bundle with new commit", + ); + let new_src = rev_parse_cli(src.path(), "HEAD"); + + let dry = run_libra_command(&["fetch", "--dry-run"], dest.as_path()); + assert_cli_success(&dry, "H4 fetch --dry-run"); + assert_eq!( + rev_parse_cli(&dest, "refs/remotes/origin/main"), + old_main, + "H4 dry-run must not update tracking" + ); + + let h2 = run_libra_command(&["fetch"], dest.as_path()); + assert_cli_success(&h2, "H2 fetch after bundle replace"); + assert_eq!( + rev_parse_cli(&dest, "refs/remotes/origin/main"), + new_src, + "H2 tracking must move to the new bundle tip" + ); + + // Recreate a main-only bundle so origin/dev is no longer advertised. + assert_cli_success( + &run_libra_command( + &["bundle", "create", bundle.to_str().unwrap(), "main"], + src.path(), + ), + "replace bundle without dev", + ); + let prune = run_libra_command(&["fetch", "--prune"], dest.as_path()); + assert_cli_success(&prune, "H4 fetch --prune"); + let pruned = run_libra_command(&["rev-parse", "refs/remotes/origin/dev"], &dest); + assert!(!pruned.status.success(), "H4 prune must drop origin/dev"); + + fs::remove_file(&bundle).expect("delete bundle"); + let h3 = run_libra_command(&["fetch"], dest.as_path()); + assert!(!h3.status.success(), "H3 missing bundle"); + let h3_text = format!( + "{}{}", + String::from_utf8_lossy(&h3.stdout), + String::from_utf8_lossy(&h3.stderr) + ); + assert!( + h3_text.contains("does not exist") && h3_text.to_ascii_lowercase().contains("bundle"), + "H3 must say the bundle does not exist: {h3_text}" + ); +} diff --git a/tests/command/pull_test.rs b/tests/command/pull_test.rs index 1a5c2b5a1..372c7676e 100644 --- a/tests/command/pull_test.rs +++ b/tests/command/pull_test.rs @@ -1727,3 +1727,58 @@ async fn test_pull_fast_forward_materializes_executable_bit() { "pull fast-forward must materialize the execute bit" ); } + +/// M-BFETCH H2: pull fast-forwards from a replaced bundle remote. +#[test] +fn test_pull_from_bundle_remote_fast_forward() { + let src = create_committed_repo_via_cli(); + let parent = tempdir().expect("bundle parent"); + let bundle = parent.path().join("remote.bundle"); + assert_cli_success( + &run_libra_command( + &["bundle", "create", bundle.to_str().unwrap(), "--all"], + src.path(), + ), + "create bundle", + ); + let dest = parent.path().join("cloned"); + assert_cli_success( + &run_libra_command( + &["clone", bundle.to_str().unwrap(), dest.to_str().unwrap()], + parent.path(), + ), + "clone from bundle", + ); + let old_head = run_libra_command(&["rev-parse", "HEAD"], &dest); + assert_cli_success(&old_head, "old HEAD"); + let old = String::from_utf8_lossy(&old_head.stdout).trim().to_string(); + + fs::write(src.path().join("next.txt"), "next\n").expect("next file"); + assert_cli_success( + &run_libra_command(&["add", "next.txt"], src.path()), + "add next", + ); + assert_cli_success( + &run_libra_command(&["commit", "-m", "next", "--no-verify"], src.path()), + "commit next", + ); + assert_cli_success( + &run_libra_command( + &["bundle", "create", bundle.to_str().unwrap(), "--all"], + src.path(), + ), + "replace bundle", + ); + let new_src = run_libra_command(&["rev-parse", "HEAD"], src.path()); + assert_cli_success(&new_src, "src HEAD"); + let expected = String::from_utf8_lossy(&new_src.stdout).trim().to_string(); + + let pull = run_libra_command(&["pull"], &dest); + assert_cli_success(&pull, "H2 pull from replaced bundle"); + let new_head = run_libra_command(&["rev-parse", "HEAD"], &dest); + assert_cli_success(&new_head, "new HEAD"); + let got = String::from_utf8_lossy(&new_head.stdout).trim().to_string(); + assert_ne!(got, old, "H2 pull must move HEAD"); + assert_eq!(got, expected, "H2 pull must fast-forward to the new tip"); + assert!(dest.join("next.txt").exists(), "H2 pull restores new file"); +}