diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 955958bf0..306edda3c 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -161,7 +161,7 @@ there is no promised all-phase 30-second hard deadline. See | Command | Tier | Notes | |---------|------|-------| | init | partial | common initialization plus Git-style safe re-initialization/top-up of an existing repo (prints `Reinitialized existing ...`, re-creates missing layout, re-applies `--shared`, persists `core.sharedRepository`, preserves config/HEAD/refs/objects/vault/repo-id otherwise) are supported; when `-b/--initial-branch` is omitted for a new repo, `init.defaultBranch` is read local→global→system with case-insensitive variable matching, local/global encrypted values decrypted, and `main` fallback; empty/invalid values fail before layout with `LBR-CLI-002`, local/global config DB read failures use `LBR-IO-001` (except a future-schema global store — newer than this binary — which is skipped with a one-time warning, see `LBR-CONFIG-001`), and unreadable/unsupported system scope is skipped; `--from-git-repository` reports the source `HEAD` branch instead of applying the configured default; numeric `--shared=` is prevalidated so non-traversable modes such as `0660` fail before a partial `.libra` is created; recursive submodule init is not; the per-user Libra home and global config directory are reserved: init refuses them as storage roots, discovery skips stray home databases (including commondir targets), and opening an older database restores a missing legacy config table without deleting existing configuration (#472) | -| clone | partial | `--depth`, `--single-branch`/`--no-single-branch` (toggle; `--no-single-branch` countermands `--single-branch`, last wins, and clone fetches all branches by default so `--no-single-branch` alone is a no-op), `--tags`/`--no-tags` (clone fetches all tags by default like Git; `--no-tags` skips them and records `remote..tagOpt=--no-tags`, where `` is the remote name — `origin` by default or the `-o`/`--origin` value), and `--no-progress` (suppresses the fetch "Receiving objects" progress meter during the clone, like `git clone --no-progress`), `--no-checkout` (set up objects/refs/HEAD but do not check out the working tree), and `-o`/`--origin ` (name the remote and its tracking refs instead of `origin`; standard clones only), `-l`/`--local` and `--no-local` (accepted no-ops — Libra never hardlinks objects (it always copies), and how it reads a local-path source is determined by the source type — a local Libra repo is read directly, a local Git repo is read in-process (no `git-upload-pack` dependency) — not by these flags), and `--reject-shallow` (fail when the clone is shallow without `--depth`, i.e. a shallow source — exit 128; `--depth` is allowed and, as a documented narrowing vs Git, suppresses the check since Libra cannot distinguish a shallow source from `--depth`-induced shallowness), and the object-alternates flags: `--shared`/`-s` (lore.md 2.11 — for a LOCAL Libra source, registers the source's object store as an alternate of the clone via lore.md 2.3's guarded path: borrowed reads + base gc/evict/obliterate protection; NON-FATAL on any guard/io failure — the clone still succeeds; v1 STILL COPIES every object, so this only adds the borrow link + base protection, NOT disk savings — copy-avoidance is deferred; a no-op warning for a remote/local-Git source), `--no-shared` (countermands `--shared` / a `clone.shared=true` default), the `clone.shared` config (global default; default OFF), `--dissociate` (forces no-share — a self-contained clone), and `--reference `/`--reference-if-able ` (still accepted no-ops — Libra has no fetch-side alternate negotiation yet, so `--reference` copies + warns and `--reference-if-able` is silently ignored; use `libra alternates add` to borrow), and `--mirror` (implies `--bare`; maps every fetched branch verbatim into `refs/heads/*` and keeps tags, drops the `refs/remotes/*` tracking refs, and records the `remote..mirror=true` marker. Documented narrowings: Libra mirrors only what it fetches — `refs/notes/*` and other un-fetched namespaces are not mirrored, and because fetch collapses `refs/mr/*` into the branch tracking namespace those refs are mirrored as `refs/heads/mr/*`; the marker is informational since `libra fetch` is not yet mirror-aware, so no `+refs/*:refs/*` refspec is recorded), and the fetch-optimization flags `--filter `/`--shallow-since `/`--shallow-exclude ` (accepted no-ops for Git remotes — Libra has no partial-clone/promisor support and its fetch only does `--depth` shallow, so each is ignored with a warning and the optimization is simply not applied — the clone fetches everything those flags would trim, subject only to `--depth` if also given (without `--depth`, a complete clone, a correct superset), matching Git's own full-clone fallback when a server cannot filter), and the dependency-filtered clone flags `--deps-of ` (repeatable) / `--deps-depth-limit ` (lore.md 3.2, intentionally-different — Git has no file-dependency concept; NOT partial-clone/`--filter` and NOT `--sparse`/D10): after a normal FULL, commit-safe checkout, scope the read-only sparse VIEW (2.2) to the forward dependency closure (3.1) of the given roots and record `remote..fetchNotesDeps=true`; implies `--notes` (the graph is fetched to compute the closure). Objects are NEVER wire-filtered — the whole pack is downloaded and the whole tree stays on disk (working-tree disk narrowing is deferred, D18); only a local Libra source can travel the graph (D17), a network/foreign-Git remote performs a full clone without scoping and warns; conflicts with `--no-checkout`/`--bare`/`--mirror` supported; `--sparse` unsupported (see [docs/development/commands/_compatibility.md#d10-clone---sparse-与顶层-sparse-checkout-命令](docs/development/commands/_compatibility.md#d10-clone---sparse-与顶层-sparse-checkout-命令)); `--recurse-submodules` unsupported (see [docs/development/commands/_compatibility.md#d4-clone---recurse-submodules](docs/development/commands/_compatibility.md#d4-clone---recurse-submodules)) checked-out entries carry the tree mode's permission bits (`100755` executable, `100644` plain) under the process `umask`, and replacing an existing file clears a stale execute bit (plan issues/470 FM-01, ADR-FM-02). A local Git source via `file://` or `--no-local` truncates by shortest distance from any want, then one boundary pass (a missing parent, or a root exactly on the depth cutoff, is written to `.libra/shallow`; issues/474 CL-04). `--depth` / `--shallow-since` / `--shallow-exclude` imply `--single-branch` unless `--no-single-branch` or `--mirror`; that clone writes `remote..fetch=+refs/heads/:refs/remotes//` (issues/474 CL-05). A plain filesystem Git path ignores `--depth` / `--shallow-since` / `--shallow-exclude` / `--filter` and prints Git's local-clone warnings (`--quiet` still warns); `file://` and `--no-local` keep transport shallow semantics (issues/474 CL-06). | +| clone | partial | `--depth`, `--single-branch`/`--no-single-branch` (toggle; `--no-single-branch` countermands `--single-branch`, last wins, and clone fetches all branches by default so `--no-single-branch` alone is a no-op), `--tags`/`--no-tags` (clone fetches all tags by default like Git; `--no-tags` skips them and records `remote..tagOpt=--no-tags`, where `` is the remote name — `origin` by default or the `-o`/`--origin` value), and `--no-progress` (suppresses the fetch "Receiving objects" progress meter during the clone, like `git clone --no-progress`), `--no-checkout` (set up objects/refs/HEAD but do not check out the working tree), and `-o`/`--origin ` (name the remote and its tracking refs instead of `origin`; standard clones only), `-l`/`--local` and `--no-local` (accepted no-ops — Libra never hardlinks objects (it always copies), and how it reads a local-path source is determined by the source type — a local Libra repo is read directly, a local Git repo is read in-process (no `git-upload-pack` dependency) — not by these flags), and `--reject-shallow` (fail when the clone is shallow without `--depth`, i.e. a shallow source — exit 128; `--depth` is allowed and, as a documented narrowing vs Git, suppresses the check since Libra cannot distinguish a shallow source from `--depth`-induced shallowness), and the object-alternates flags: `--shared`/`-s` (lore.md 2.11 — for a LOCAL Libra source, registers the source's object store as an alternate of the clone via lore.md 2.3's guarded path: borrowed reads + base gc/evict/obliterate protection; NON-FATAL on any guard/io failure — the clone still succeeds; v1 STILL COPIES every object, so this only adds the borrow link + base protection, NOT disk savings — copy-avoidance is deferred; a no-op warning for a remote/local-Git source), `--no-shared` (countermands `--shared` / a `clone.shared=true` default), the `clone.shared` config (global default; default OFF), `--dissociate` (forces no-share — a self-contained clone), and `--reference `/`--reference-if-able ` (still accepted no-ops — Libra has no fetch-side alternate negotiation yet, so `--reference` copies + warns and `--reference-if-able` is silently ignored; use `libra alternates add` to borrow), and `--mirror` (implies `--bare`; maps every fetched branch verbatim into `refs/heads/*` and keeps tags, drops the `refs/remotes/*` tracking refs, and records the `remote..mirror=true` marker. Documented narrowings: Libra mirrors only what it fetches — `refs/notes/*` and other un-fetched namespaces are not mirrored, and because fetch collapses `refs/mr/*` into the branch tracking namespace those refs are mirrored as `refs/heads/mr/*`; the marker is informational since `libra fetch` is not yet mirror-aware, so no `+refs/*:refs/*` refspec is recorded), and the fetch-optimization flags `--filter `/`--shallow-since `/`--shallow-exclude ` (accepted no-ops for Git remotes — Libra has no partial-clone/promisor support and its fetch only does `--depth` shallow, so each is ignored with a warning and the optimization is simply not applied — the clone fetches everything those flags would trim, subject only to `--depth` if also given (without `--depth`, a complete clone, a correct superset), matching Git's own full-clone fallback when a server cannot filter), and the dependency-filtered clone flags `--deps-of ` (repeatable) / `--deps-depth-limit ` (lore.md 3.2, intentionally-different — Git has no file-dependency concept; NOT partial-clone/`--filter` and NOT `--sparse`/D10): after a normal FULL, commit-safe checkout, scope the read-only sparse VIEW (2.2) to the forward dependency closure (3.1) of the given roots and record `remote..fetchNotesDeps=true`; implies `--notes` (the graph is fetched to compute the closure). Objects are NEVER wire-filtered — the whole pack is downloaded and the whole tree stays on disk (working-tree disk narrowing is deferred, D18); only a local Libra source can travel the graph (D17), a network/foreign-Git remote performs a full clone without scoping and warns; conflicts with `--no-checkout`/`--bare`/`--mirror` supported; `--sparse` unsupported (see [docs/development/commands/_compatibility.md#d10-clone---sparse-与顶层-sparse-checkout-命令](docs/development/commands/_compatibility.md#d10-clone---sparse-与顶层-sparse-checkout-命令)); `--recurse-submodules` unsupported (see [docs/development/commands/_compatibility.md#d4-clone---recurse-submodules](docs/development/commands/_compatibility.md#d4-clone---recurse-submodules)) checked-out entries carry the tree mode's permission bits (`100755` executable, `100644` plain) under the process `umask`, and replacing an existing file clears a stale execute bit (plan issues/470 FM-01, ADR-FM-02). A local Git source via `file://` or `--no-local` truncates by shortest distance from any want, then one boundary pass (a missing parent, or a root exactly on the depth cutoff, is written to `.libra/shallow`; issues/474 CL-04). `--depth` / `--shallow-since` / `--shallow-exclude` imply `--single-branch` unless `--no-single-branch` or `--mirror`; that clone writes `remote..fetch=+refs/heads/:refs/remotes//` (issues/474 CL-05). A plain filesystem Git path ignores `--depth` / `--shallow-since` / `--shallow-exclude` / `--filter` and prints Git's local-clone warnings (`--quiet` still warns); `file://` and `--no-local` keep transport shallow semantics (issues/474 CL-06). A local Git shallow source is cloned with its `.git/shallow` boundaries merged into `.libra/shallow`; `--reject-shallow` inspects the source before creating the destination and leaves no residual (issues/474 CL-07). | | automation | intentionally-different | Libra AI automation rules/history extension, not a Git command. Linked worktrees load `automations.toml` through the unified resolver and dispatch VCS events there; damaged/unreadable scope still fail-closes. | | sandbox | intentionally-different | Libra AI sandbox diagnostics extension, not a Git command | | cache | intentionally-different | Diagnostic for the tiered-storage / LRU cache (`cache info`): reports the resolved `LIBRA_STORAGE_TYPE`, whether a durable tier is active, and the `LIBRA_STORAGE_THRESHOLD` (small/large threshold) + `LIBRA_STORAGE_CACHE_SIZE` (LRU disk budget) tunables. Git has no equivalent; needs no repository. `--json`/`--machine` → `{ storage_type, tiered, threshold_bytes, cache_size_bytes }`. Exit 0 (non-zero if a storage config value cannot be resolved, e.g. an unreadable global config DB); `cache evict [--dry-run] [--max-size ] [--min-age ]` (lore.md 2.9): evicts verified-durable LARGE loose objects oldest-first until under the configured budget — every unlink is gated on an error-aware durability probe run immediately before it (confirmed-absent objects are skipped with a push hint; probe ERRORS are never treated as absence; 3 leading probe failures abort with nothing deleted); presence≠integrity residual documented; tiered `get` now self-heals a vanished local file from the durable tier; local-only repos report nothing-evictable; offline read policy refuses; also runnable as `maintenance run --task cache-evict` (excluded from the default task set) — and `maintenance loose-objects` no longer packs >=threshold cache residents under a tiered config | diff --git a/Cargo.lock b/Cargo.lock index ca926ac37..5eb914cc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3783,7 +3783,7 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libra" -version = "0.23.52" +version = "0.23.53" dependencies = [ "anyhow", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index e14e9799e..d8e040882 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libra" -version = "0.23.52" +version = "0.23.53" 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/clone.md b/docs/commands/clone.md index 6e99b27f8..20ad44053 100644 --- a/docs/commands/clone.md +++ b/docs/commands/clone.md @@ -202,19 +202,15 @@ libra clone --depth 50 git@github.com:user/repo.git ### `--reject-shallow` -Fail if the clone would be a shallow repository that you did not request — i.e. -the source repository is shallow — matching `git clone --reject-shallow` -(exit 128). Combining it with `--depth` is allowed only for transports that can -negotiate shallow boundaries. A local Libra source rejects `--depth` before -object transfer, and no initialized target is left behind. - -Two narrowings vs Git: (1) Libra's clone of a local-path source re-fetches the -full history rather than inheriting the source's shallow marker, so this check -is most meaningful when cloning a shallow *remote*; (2) because Libra cannot -distinguish a shallow source from `--depth`-induced shallowness, passing -`--depth` suppresses the post-fetch `--reject-shallow` check for remotes that -do support shallow negotiation (Git would still reject a shallow source with -`--depth`). +Fail if the **source** repository is shallow — matching `git clone --reject-shallow` +(exit 128) — and leave no destination directory behind. A local Git shallow +source is inspected before the destination is created. Cloning a shallow Git +source without this flag copies the source's `.git/shallow` boundaries into +`.libra/shallow` so `log` / `fsck` stay walkable. A local Libra source with +`--depth` still fails closed with `LBR-REPO-002` before object transfer. + +For network remotes that negotiate shallow boundaries, a post-fetch check still +rejects an unexpected shallow result when `--depth` was not requested. ```bash libra clone --reject-shallow git@github.com:user/repo.git diff --git a/docs/commands/zh-CN/clone.md b/docs/commands/zh-CN/clone.md index 562c8c7ee..f3fa3022f 100644 --- a/docs/commands/zh-CN/clone.md +++ b/docs/commands/zh-CN/clone.md @@ -122,9 +122,9 @@ libra clone --depth 50 git@github.com:user/repo.git ### `--reject-shallow` -若克隆结果是你未请求的浅仓库(即源仓库本身是浅克隆),则失败,对齐 `git clone --reject-shallow`(exit 128)。只有在传输层能协商 shallow boundary 时,才允许与 `--depth` 同用;本地 Libra 源会在对象传输前拒绝 `--depth`,且不会留下已初始化的目标仓库。 +若**源**仓库是浅克隆则失败,对齐 `git clone --reject-shallow`(exit 128),且不留下目标目录。本地 Git 浅源在创建目标前检查;不带该标志克隆浅源时,会把源的 `.git/shallow` 边界并入 `.libra/shallow`,使 `log` / `fsck` 可遍历。本地 Libra 源带 `--depth` 仍在对象传输前以 `LBR-REPO-002` fail-closed。 -相对 Git 的两点收窄:(1) Libra 克隆本地路径源时会重取完整历史、不继承源的浅标记,故该检查主要在克隆浅 *remote* 时有意义;(2) 由于 Libra 无法区分“源是浅克隆”与“`--depth` 导致的浅克隆”,对支持 shallow 协商的远程,给出 `--depth` 会抑制 fetch 后的 `--reject-shallow` 检查(Git 即便带 `--depth` 也会拒绝浅源)。 +对能协商 shallow boundary 的网络远程,未请求 `--depth` 时仍会在 fetch 后拒绝意外的浅结果。 ```bash libra clone --reject-shallow git@github.com:user/repo.git diff --git a/docs/development/plan/issues/474.md b/docs/development/plan/issues/474.md index 28957b6cd..9c00aa62b 100644 --- a/docs/development/plan/issues/474.md +++ b/docs/development/plan/issues/474.md @@ -1075,7 +1075,7 @@ ER-CL-02 接入 / 豁免清单(2026-09-23): **Task type:** `implementation` -**Lifecycle / Acceptance:** `pending` / 空 +**Lifecycle / Acceptance:** `in-progress` / `locally-accepted` **Description:** 按 ADR-CL-02 第 3 条、ADR-CL-03 第 3 条与 ADR-CL-07,让本地 Git 传输读取源仓库的 `shallow` 文件,把源边界视为无父提交并并入目标 `.libra/shallow`;`--reject-shallow` 对 shallow 源输出 Git 原文并清理目标;源读取失败归类为仓库错误。唯一行为轴是「shallow 源的克隆」。 @@ -1092,19 +1092,19 @@ ER-CL-02 接入 / 豁免清单(2026-09-23): **Acceptance criteria:** -- [ ] 判据矩阵 M-SSRC R1(普通克隆 shallow 源)。 -- [ ] `--reject-shallow` 通过 M-SSRC R2。 -- [ ] 正常源不受影响通过 M-SSRC R3。 -- [ ] shallow 源再浅化通过 M-SSRC R4。 -- [ ] 错误归类通过 M-SSRC R5,失败零残留(GC-CL-04)。 -- [ ] ER-06a 同步:`docs/commands/clone.md` 与 zh-CN 的 `--reject-shallow` 说明(删除「`--depth` 抑制检查」的收窄描述,若实现后不再适用)、`COMPATIBILITY.md:165` 原行更新、网站 `clone.en.md`。 +- [x] 判据矩阵 M-SSRC R1(普通克隆 shallow 源)。 +- [x] `--reject-shallow` 通过 M-SSRC R2。 +- [x] 正常源不受影响通过 M-SSRC R3。 +- [x] shallow 源再浅化通过 M-SSRC R4。 +- [x] 错误归类通过 M-SSRC R5,失败零残留(GC-CL-04)。 +- [x] ER-06a 同步:`docs/commands/clone.md` 与 zh-CN 的 `--reject-shallow` 说明、`COMPATIBILITY.md` clone 原行追加、网站 `clone.en.md`(`cf` 已含浅源句时可核对)。 **Verification:** -- [ ] `source .env.test && source .env.live-test && cargo nextest run --lib internal::protocol::local_client`(new:源 shallow 边界读取单测) -- [ ] `source .env.test && source .env.live-test && cargo nextest run --test command_test clone_test`(new:`test_clone_from_shallow_git_source_matrix`、`test_clone_reject_shallow_local_git_source`) -- [ ] `source .env.test && source .env.live-test && cargo nextest run --test compat_clone_shallow_integrity` -- [ ] `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 --lib internal::protocol::local_client`(new:`collect_git_repo_entries_merges_source_shallow_boundaries` PASS) +- [x] `source .env.test && source .env.live-test && cargo nextest run --test command_test clone_test`(new:`test_clone_from_shallow_git_source_matrix`、`test_clone_reject_shallow_local_git_source` PASS) +- [x] `source .env.test && source .env.live-test && cargo nextest run --test compat_clone_shallow_integrity`(2026-09-23 4 passed) +- [x] `source .env.test && source .env.live-test && cargo nextest run --test compat_ledger_schema`(2026-09-23 43 passed) **Full-suite trigger:** `none` diff --git a/docs/development/plan/plan-status.md b/docs/development/plan/plan-status.md index b44a42cfe..f68087d5c 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`;CL-04..CL-06 `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` 自托管 runner 掉线后重跑中);CL-04..CL-07 `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 | @@ -181,7 +181,8 @@ | **CL-04 本地 Git 浅边界** | **`in-progress`/`locally-accepted`** | **未 bump(等 CL-03 发布窗口)** | | **CL-05 `--depth` 隐含单分支** | **`in-progress`/`locally-accepted`** | **未 bump(等 CL-04 发布窗口)** | | **CL-06 普通路径忽略浅化参数** | **`in-progress`/`locally-accepted`** | **未 bump(等 CL-05 发布窗口)** | -| CL-07..CL-15 | `pending` | 未 bump | +| **CL-07 shallow Git 源克隆** | **`in-progress`/`locally-accepted`** | **未 bump(等 CL-06 发布窗口)** | +| CL-08..CL-15 | `pending` | 未 bump | ### 3.6 issues/476(工作树命令族) @@ -221,8 +222,8 @@ SBX-01..05 `done/locally-accepted`;**发布步按 DEFER-SBX-06 正式延后** ## 四、当前执行指针(next action) -- **当前正在执行:** `issues/474` → `CL-03` 发布窗口(`v0.23.49`,PR #512)。`CL-04`、`CL-05`、`CL-06` 已 `locally-accepted`,均未 bump。 -- **下一步(CL-03 D 组落地后):** 按窗口依次 bump 并发布 `CL-04`、`CL-05`、`CL-06`。 +- **当前正在执行:** `issues/474` → `CL-03` 发布窗口(`v0.23.49`,PR #512)。`compat-network-remotes` 因自托管 runner 掉线失败后已 `--failed` 重跑;其余 base.yml 作业此前已绿。`CL-04`..`CL-07` 已 `locally-accepted`,均未 bump。 +- **下一步(CL-03 D 组落地后):** 按窗口依次 bump 并发布 `CL-04` → `CL-07`。 - **并行窗口(不在本执行指针):** `issues/476` WT-03 仍等 DEP-WT-08;`plan-20260918` 其余 add 卡、`plan-20260819` M2 仍登记为实施中,但不抢本卡的 `fsck.rs` 写集。 --- diff --git a/install.ps1 b/install.ps1 index 0793f33fb..2012cf70c 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.52" +$DefaultVersion = "v0.23.53" # 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 1ae8135dc..a153b18b1 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.52" +DEFAULT_VERSION="v0.23.53" # 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/src/command/clone.rs b/src/command/clone.rs index 170beaf97..0a879f88b 100644 --- a/src/command/clone.rs +++ b/src/command/clone.rs @@ -33,6 +33,7 @@ use crate::{ head::Head, protocol::DiscoveryResult, reflog::{ReflogAction, ReflogContext, with_reflog}, + shallow::{ShallowError, ShallowSet}, }, utils::{ error::{CliError, CliResult, StableErrorCode}, @@ -137,14 +138,10 @@ pub struct CloneArgs { #[clap(long = "no-local", overrides_with = "local")] pub no_local: bool, - /// Fail if the clone would be a shallow repository that was not explicitly - /// requested — i.e. the source repository is shallow (matching - /// `git clone --reject-shallow`). Two narrowings vs Git: (1) for remotes - /// that can negotiate shallow boundaries, Libra cannot distinguish a shallow - /// source from `--depth`-induced shallowness, so passing `--depth` - /// suppresses the post-fetch check (Git would still reject); (2) local Libra - /// sources do not advertise shallow boundaries (declined by design, D20), so `--depth` fails - /// closed before this check. + /// Fail if the source repository is shallow (matching + /// `git clone --reject-shallow`). A local Git shallow source is inspected + /// before the destination is created. Local Libra sources do not advertise + /// shallow boundaries (D20), so `--depth` fails closed before this check. #[clap(long = "reject-shallow")] pub reject_shallow: bool, @@ -319,12 +316,34 @@ fn remote_spec_is_plain_local_path(spec: &str) -> bool { fn uses_local_clone_semantics(args: &CloneArgs, remote_client: &fetch::RemoteClient) -> bool { match remote_client { fetch::RemoteClient::Local(client) if !client.is_libra_source() => { + // A shallow Git source must use the transport so its `.git/shallow` + // boundaries are copied (git `builtin/clone.c:1333-1340`). + if git_source_is_shallow(client.repo_path()) { + return false; + } remote_spec_is_plain_local_path(&args.remote_repo) && !args.no_local } _ => false, } } +fn git_source_is_shallow(repo_path: &Path) -> bool { + ShallowSet::load_at(&repo_path.join("shallow")) + .map(|set| !set.oids().is_empty()) + .unwrap_or(true) +} + +fn inspect_local_git_shallow( + remote_client: &fetch::RemoteClient, +) -> Result { + match remote_client { + fetch::RemoteClient::Local(client) if !client.is_libra_source() => { + ShallowSet::load_at(&client.repo_path().join("shallow")) + } + _ => Ok(ShallowSet::empty()), + } +} + fn local_clone_ignored_option_warnings(args: &CloneArgs) -> Vec { let mut warnings = Vec::new(); if args.depth.is_some() { @@ -508,8 +527,10 @@ pub enum CloneError { RestoreDirectory { path: PathBuf, source: io::Error }, #[error("failed to initialize repository")] InitializeRepository { source: InitError }, - #[error("source repository is shallow, reject to clone")] + #[error("source repository is shallow, reject to clone.")] RejectShallow, + #[error("failed to read source shallow metadata: {source}")] + SourceShallow { source: ShallowError }, #[error("remote branch {branch} not found in upstream {remote}")] RemoteBranchNotFound { branch: String, remote: String }, #[error("failed to inspect local branch state after fetch: {source}")] @@ -570,6 +591,10 @@ impl From for CliError { .with_stable_code(StableErrorCode::RepoStateInvalid) .with_exit_code(128) .with_hint("the source is shallow; clone without --reject-shallow, or deepen the source first"), + CloneError::SourceShallow { source } => CliError::fatal(source.to_string()) + .with_stable_code(StableErrorCode::RepoCorrupt) + .with_exit_code(128) + .with_hint(source.hint()), CloneError::RemoteBranchNotFound { ref branch, ref remote, @@ -1166,6 +1191,16 @@ async fn execute_clone_inner( .await .map_err(|source| (CloneError::DiscoverRemote { source }, None))?; + // Inspect a local Git source's `.git/shallow` before creating the dest so + // `--reject-shallow` and corrupt metadata leave no partial clone behind. + match inspect_local_git_shallow(&remote_client) { + Ok(set) if !set.oids().is_empty() && args.reject_shallow => { + return Err((CloneError::RejectShallow, None)); + } + Err(source) => return Err((CloneError::SourceShallow { source }, None)), + _ => {} + } + // --- Step 3: Destination pre-checks --- if metadata_root.exists() && contains_initialized_repo(&metadata_root) { return Err(( diff --git a/src/internal/protocol/local_client.rs b/src/internal/protocol/local_client.rs index a3b06a9a9..d60262cc3 100644 --- a/src/internal/protocol/local_client.rs +++ b/src/internal/protocol/local_client.rs @@ -1853,4 +1853,22 @@ mod tests { "shared parent already sent is not a boundary: {shallow:?}" ); } + + #[test] + fn collect_git_repo_entries_merges_source_shallow_boundaries() { + let gdeep = build_gdeep(); + fs::write(gdeep.git_dir.join("shallow"), format!("{}\n", gdeep.c2)).unwrap(); + let (commits, shallow) = collect_gdeep(&gdeep, &[&gdeep.c3], None); + assert!(commits.contains(&gdeep.c3), "tip is sent"); + assert!(commits.contains(&gdeep.c2), "shallow tip is still sent"); + assert!( + !commits.contains(&gdeep.c1), + "walk must stop at the source shallow boundary" + ); + assert_eq!( + shallow, + vec![gdeep.c2.clone()], + "source shallow merges into the result" + ); + } } diff --git a/tests/command/clone_test.rs b/tests/command/clone_test.rs index df368bb6a..ba1de879e 100644 --- a/tests/command/clone_test.rs +++ b/tests/command/clone_test.rs @@ -1942,3 +1942,170 @@ fn test_clone_local_path_ignores_shallow_options_matrix() { "L5 quiet suppresses the success summary" ); } + +/// M-SSRC: clone a Git-generated shallow source, reject-shallow, and corrupt +/// source metadata. +#[test] +fn test_clone_from_shallow_git_source_matrix() { + use super::{ + assert_cli_success, create_linear_git_repo, git_success, read_shallow_oids, + run_libra_command, + }; + + let (linear, oids) = create_linear_git_repo(3); + let c3 = &oids[2]; + let dest_root = tempdir().expect("ssrc dest"); + let gshallow = dest_root.path().join("gshallow"); + git_success( + dest_root.path(), + &[ + "clone", + "--depth", + "1", + "--single-branch", + "--no-tags", + &format!("file://{}", linear.path().display()), + gshallow.to_str().unwrap(), + ], + ); + assert!( + gshallow.join(".git").join("shallow").is_file(), + "git must produce a shallow source" + ); + + let r1_path = dest_root.path().join("r1-path"); + let out = run_libra_command( + &[ + "clone", + gshallow.to_str().unwrap(), + r1_path.to_str().unwrap(), + ], + dest_root.path(), + ); + assert_cli_success(&out, "R1 clone plain shallow path"); + assert_eq!( + read_shallow_oids(&r1_path), + vec![c3.clone()], + "R1 path shallow" + ); + assert_cli_success( + &run_libra_command(&["log", "--oneline"], &r1_path), + "R1 path log", + ); + assert_cli_success(&run_libra_command(&["fsck"], &r1_path), "R1 path fsck"); + + let r1_file = dest_root.path().join("r1-file"); + let out = run_libra_command( + &[ + "clone", + &format!("file://{}", gshallow.display()), + r1_file.to_str().unwrap(), + ], + dest_root.path(), + ); + assert_cli_success(&out, "R1 clone file:// shallow"); + assert_eq!( + read_shallow_oids(&r1_file), + vec![c3.clone()], + "R1 file shallow" + ); + + let r2 = dest_root.path().join("r2"); + let out = run_libra_command( + &[ + "clone", + "--reject-shallow", + gshallow.to_str().unwrap(), + r2.to_str().unwrap(), + ], + dest_root.path(), + ); + assert!(!out.status.success(), "R2 reject-shallow path"); + assert_eq!(out.status.code(), Some(128)); + let r2_err = String::from_utf8_lossy(&out.stderr); + assert!( + r2_err.contains("source repository is shallow, reject to clone."), + "R2 Git text: {r2_err}" + ); + assert!(!r2.join(".libra").exists(), "R2 must not leave a target"); + + let r2_nl = dest_root.path().join("r2-nl"); + let out = run_libra_command( + &[ + "clone", + "--reject-shallow", + "--no-local", + gshallow.to_str().unwrap(), + r2_nl.to_str().unwrap(), + ], + dest_root.path(), + ); + assert!(!out.status.success(), "R2 reject-shallow --no-local"); + assert!(!r2_nl.join(".libra").exists()); + + let r3 = dest_root.path().join("r3"); + let out = run_libra_command( + &[ + "clone", + "--reject-shallow", + linear.path().to_str().unwrap(), + r3.to_str().unwrap(), + ], + dest_root.path(), + ); + assert_cli_success(&out, "R3 reject-shallow complete source"); + + let r4 = dest_root.path().join("r4"); + let out = run_libra_command( + &[ + "clone", + "--depth", + "1", + &format!("file://{}", gshallow.display()), + r4.to_str().unwrap(), + ], + dest_root.path(), + ); + assert_cli_success(&out, "R4 --depth 1 file:// shallow"); + assert_eq!(read_shallow_oids(&r4), vec![c3.clone()], "R4 boundary"); + assert_cli_success(&run_libra_command(&["log", "--oneline"], &r4), "R4 log"); + assert_cli_success(&run_libra_command(&["fsck"], &r4), "R4 fsck"); +} + +#[test] +fn test_clone_reject_shallow_local_git_source() { + use super::{create_linear_git_repo, git_success, run_libra_command}; + + let (linear, _) = create_linear_git_repo(2); + let dest_root = tempdir().expect("r5 dest"); + let gshallow = dest_root.path().join("gshallow"); + git_success( + dest_root.path(), + &[ + "clone", + "--depth", + "1", + &format!("file://{}", linear.path().display()), + gshallow.to_str().unwrap(), + ], + ); + fs::write(gshallow.join(".git").join("shallow"), "not-an-oid\n").expect("corrupt shallow"); + + let r5 = dest_root.path().join("r5"); + let out = run_libra_command( + &[ + "clone", + &format!("file://{}", gshallow.display()), + r5.to_str().unwrap(), + ], + dest_root.path(), + ); + assert!(!out.status.success(), "R5 corrupt source shallow"); + assert_eq!(out.status.code(), Some(128)); + let err = String::from_utf8_lossy(&out.stderr); + assert!( + err.contains("LBR-REPO-002"), + "R5 must be a repository error: {err}" + ); + assert!(!r5.join(".libra").exists(), "R5 must not leave a target"); +}