Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion COMPATIBILITY.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "libra"
version = "0.23.53"
version = "0.23.54"
edition = "2024"
license = "MIT"
description = "AI agent-native version control system with Git on-disk compatibility, SQLite-backed metadata, and tiered cloud storage"
Expand Down
12 changes: 7 additions & 5 deletions docs/commands/bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ libra bundle unbundle <file>

- `create` writes a full, non-thin bundle. Explicit revisions may be combined
with `--all`, `--branches`, or `--tags`; at least one selector is required.
Annotated tag heads retain the tag-object OID and the pack includes tag target
closure. Output uses a private temporary file, syncs it, then renames it into
place.
`--all` and an explicit `HEAD` revision advertise a `HEAD` line (detached
`HEAD` points at the detached commit; attached `HEAD` stays `HEAD`, not
`refs/heads/<branch>`). Annotated tag heads retain the tag-object OID and the
pack includes tag target closure. Output uses a private temporary file, syncs
it, then renames it into place.
- `verify` validates the v2 header, local prerequisites, pack version, and the
complete pack checksum.
- `list-heads` prints the advertised `<oid> <ref>` lines without importing.
Expand All @@ -37,8 +39,8 @@ full-history only; prerequisite/thin/incremental range creation remains deferred

| Option | Description |
|---|---|
| `<rev>...` | Include explicit revisions as advertised heads. |
| `--all` | Include all local branches and tags. |
| `<rev>...` | Include explicit revisions as advertised heads. `HEAD` is advertised as `HEAD`. |
| `--all` | Include all local branches and tags, plus a `HEAD` line. |
| `--branches` | Include all local branches. |
| `--tags` | Include all local tags, preserving annotated objects. |

Expand Down
6 changes: 3 additions & 3 deletions docs/commands/zh-CN/bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ libra bundle unbundle <file>

## 说明

- `create` 写完整、非 thin bundle。显式修订可与 `--all`、`--branches`、`--tags` 组合,且至少需要一种选择。annotated tag head 保留 tag 对象 OID,pack 包含其目标闭包。输出先写入私有临时文件并同步,再 rename 到目标。
- `create` 写完整、非 thin bundle。显式修订可与 `--all`、`--branches`、`--tags` 组合,且至少需要一种选择。`--all` 与显式 `HEAD` 会广告一行 `HEAD`(分离 HEAD 指向该提交;附着 HEAD 仍写 `HEAD`,不改写成 `refs/heads/<branch>`)。annotated tag head 保留 tag 对象 OID,pack 包含其目标闭包。输出先写入私有临时文件并同步,再 rename 到目标。
- `verify` 校验 v2 头、本地 prerequisite、pack 版本与完整 pack checksum。
- `list-heads` 只打印 `<oid> <ref>` advertised heads,不导入对象。
- `unbundle` 校验 prerequisite/checksum,构建正确的 SHA-1 或 SHA-256 pack index,并把 pack/index 对装入对象库。它打印 heads,但按 `git bundle unbundle` 语义**不更新 refs**。重复导入会先核对已安装 pair,再报告成功。
Expand All @@ -24,8 +24,8 @@ bundle 输入、收集的原始对象数据和最终输出各自以 1 GiB 为上

| 选项 | 说明 |
|---|---|
| `<rev>...` | 把显式修订作为 advertised heads 包含。 |
| `--all` | 包含全部本地分支和 tag。 |
| `<rev>...` | 把显式修订作为 advertised heads 包含。`HEAD` 广告为 `HEAD`。 |
| `--all` | 包含全部本地分支和 tag,并写入一行 `HEAD`。 |
| `--branches` | 包含全部本地分支。 |
| `--tags` | 包含全部本地 tag,并保留 annotated 对象。 |

Expand Down
18 changes: 9 additions & 9 deletions docs/development/plan/issues/474.md
Original file line number Diff line number Diff line change
Expand Up @@ -1142,7 +1142,7 @@ ER-CL-02 接入 / 豁免清单(2026-09-23):

**Task type:** `implementation`

**Lifecycle / Acceptance:** `pending` /
**Lifecycle / Acceptance:** `in-progress` / `locally-accepted`

**Description:** 按 ADR-CL-06 第 4 条,让 `bundle create --all` 与显式 `HEAD` 参数在 bundle 头部写入 `HEAD` 行(分离 HEAD 时指向提交),使 Git 与 Libra 都能从中检出。唯一行为轴是「bundle 的 HEAD 广告」。

Expand All @@ -1158,17 +1158,17 @@ ER-CL-02 接入 / 豁免清单(2026-09-23):

**Acceptance criteria:**

- [ ] 判据矩阵 M-BCREATE C1(`--all` 含 `HEAD`)。
- [ ] 显式 `HEAD` 通过 M-BCREATE C2。
- [ ] 分离 HEAD 通过 M-BCREATE C3。
- [ ] Git 可检出(夹具验证:由测试写出 bundle 后比对头部格式,Git 克隆结果作为手工证据)通过 M-BCREATE C4。
- [ ] ER-06a 同步:`docs/commands/bundle.md` 与 zh-CN、`COMPATIBILITY.md:207` 原行追加、网站 `bundle.en.md`;`bundle verify`/`unbundle` 回归(C5)。
- [x] 判据矩阵 M-BCREATE C1(`--all` 含 `HEAD`)。
- [x] 显式 `HEAD` 通过 M-BCREATE C2。
- [x] 分离 HEAD 通过 M-BCREATE C3。
- [x] Git 可检出(夹具验证:由测试写出 bundle 后比对头部格式,Git 克隆结果作为手工证据)通过 M-BCREATE C4。
- [x] ER-06a 同步:`docs/commands/bundle.md` 与 zh-CN、`COMPATIBILITY.md` bundle 原行追加、网站 `bundle.en.md`(`cf` 已含 HEAD 句);`bundle verify`/`unbundle` 回归(C5)。

**Verification:**

- [ ] `source .env.test && source .env.live-test && cargo nextest run --lib command::bundle`(new:heads 收集含 `HEAD` 单测
- [ ] `source .env.test && source .env.live-test && cargo nextest run --test command_test bundle_test`(new:`test_bundle_create_advertises_head_matrix`)
- [ ] 手工证据:git 2.54 从 Libra bundle 克隆的输出(存 `LIBRA_TEST_SCRATCH_DIR`)
- [x] `source .env.test && source .env.live-test && cargo nextest run --lib command::bundle`(new:`advertised_name_for_explicit_head_is_head`
- [x] `source .env.test && source .env.live-test && cargo nextest run --test command_test bundle_test`(new:`test_bundle_create_advertises_head_matrix`)
- [x] 手工证据:git 2.54 从 Libra bundle 克隆的输出(矩阵 C4 在测试内执行 `git clone`)

**Full-suite trigger:** `none`

Expand Down
9 changes: 5 additions & 4 deletions docs/development/plan/plan-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 自托管 runner 掉线后重跑中);CL-04..CL-07 `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-08 `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 |
Expand Down Expand Up @@ -182,7 +182,8 @@
| **CL-05 `--depth` 隐含单分支** | **`in-progress`/`locally-accepted`** | **未 bump(等 CL-04 发布窗口)** |
| **CL-06 普通路径忽略浅化参数** | **`in-progress`/`locally-accepted`** | **未 bump(等 CL-05 发布窗口)** |
| **CL-07 shallow Git 源克隆** | **`in-progress`/`locally-accepted`** | **未 bump(等 CL-06 发布窗口)** |
| CL-08..CL-15 | `pending` | 未 bump |
| **CL-08 bundle create 写入 HEAD** | **`in-progress`/`locally-accepted`** | **未 bump(等 CL-07 发布窗口)** |
| CL-09..CL-15 | `pending` | 未 bump |

### 3.6 issues/476(工作树命令族)

Expand Down Expand Up @@ -222,8 +223,8 @@ SBX-01..05 `done/locally-accepted`;**发布步按 DEFER-SBX-06 正式延后**

## 四、当前执行指针(next action)

- **当前正在执行:** `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/474` → `CL-03` 发布窗口(`v0.23.49`,PR #512)。`compat-network-remotes` 因自托管 runner 掉线失败后已 `--failed` 重跑;其余 base.yml 作业此前已绿。`CL-04`..`CL-08` 已 `locally-accepted`,均未 bump。
- **下一步(CL-03 D 组落地后):** 按窗口依次 bump 并发布 `CL-04` → `CL-08`。
- **并行窗口(不在本执行指针):** `issues/476` WT-03 仍等 DEP-WT-08;`plan-20260918` 其余 add 卡、`plan-20260819` M2 仍登记为实施中,但不抢本卡的 `fsck.rs` 写集。

---
Expand Down
2 changes: 1 addition & 1 deletion install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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.53"
$DefaultVersion = "v0.23.54"
# 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.
Expand Down
2 changes: 1 addition & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.53"
DEFAULT_VERSION="v0.23.54"
# 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
Expand Down
60 changes: 42 additions & 18 deletions src/command/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ async fn create(
include_branches: bool,
include_tags: bool,
) -> CliResult<()> {
let heads = collect_bundle_heads(revs, all || include_branches, all || include_tags).await?;
let heads =
collect_bundle_heads(revs, all || include_branches, all || include_tags, all).await?;

// Collect every object reachable from the tips (deduplicated).
let mut seen: HashSet<ObjectHash> = HashSet::new();
Expand Down Expand Up @@ -226,6 +227,7 @@ async fn collect_bundle_heads(
revs: &[String],
include_branches: bool,
include_tags: bool,
include_head: bool,
) -> CliResult<Vec<BundleHead>> {
let mut branches = Branch::list_branches_result(None).await.map_err(|error| {
CliError::fatal(format!("failed to list branches for bundle: {error}"))
Expand Down Expand Up @@ -274,6 +276,9 @@ async fn collect_bundle_heads(
heads.push(head);
}
};
if include_head && let Some(head) = current_head_advertisement().await? {
add(head);
}
if include_branches {
for branch in &branches {
add(BundleHead {
Expand Down Expand Up @@ -314,14 +319,10 @@ async fn collect_bundle_heads(
.with_exit_code(128)
.with_stable_code(StableErrorCode::CliInvalidTarget)
})?;
let name = if rev == "HEAD" {
resolve_ref_name(rev).await
} else if rev.starts_with("refs/") {
rev.clone()
} else {
format!("refs/heads/bundle-export-{}", index + 1)
};
add(BundleHead { oid, name });
add(BundleHead {
oid,
name: advertised_name_for_rev(rev, index),
});
}

if heads.is_empty() {
Expand Down Expand Up @@ -908,21 +909,31 @@ fn split_oid_rest(line: &str) -> (String, String) {
// shared helpers
// ----------------------------------------------------------------------------

/// Resolve the ref name a revision should be recorded under in the header.
async fn resolve_ref_name(rev: &str) -> String {
/// Advertised header name for an explicit revision after branch/tag lookup.
/// `HEAD` stays `HEAD` (Git bundle create), including when attached to a branch.
fn advertised_name_for_rev(rev: &str, index: usize) -> String {
if rev == "HEAD" {
return match Head::current().await {
Head::Branch(name) => format!("refs/heads/{name}"),
Head::Detached(_) => "HEAD".to_string(),
};
}
if rev.starts_with("refs/") {
"HEAD".to_string()
} else if rev.starts_with("refs/") {
rev.to_string()
} else {
format!("refs/heads/{rev}")
format!("refs/heads/bundle-export-{}", index + 1)
}
}

/// Current repository HEAD as a bundle advertisement, or `None` when unborn.
async fn current_head_advertisement() -> CliResult<Option<BundleHead>> {
let oid = Head::current_commit_result().await.map_err(|error| {
CliError::fatal(format!("failed to resolve HEAD for bundle: {error}"))
.with_exit_code(128)
.with_stable_code(StableErrorCode::RepoCorrupt)
})?;
Ok(oid.map(|oid| BundleHead {
oid,
name: "HEAD".to_string(),
}))
}

fn object_error(id: &ObjectHash, error: git_internal::errors::GitError) -> CliError {
CliError::fatal(format!("failed to load object {id}: {error}"))
.with_exit_code(128)
Expand All @@ -947,6 +958,19 @@ fn write_err(error: std::io::Error) -> CliError {
mod tests {
use super::*;

#[test]
fn advertised_name_for_explicit_head_is_head() {
assert_eq!(advertised_name_for_rev("HEAD", 0), "HEAD");
assert_eq!(
advertised_name_for_rev("refs/heads/main", 0),
"refs/heads/main"
);
assert_eq!(
advertised_name_for_rev("orphan-oid", 2),
"refs/heads/bundle-export-3"
);
}

#[test]
fn parses_a_v2_header_with_heads() {
let oid = "a".repeat(get_hash_kind().hex_len());
Expand Down
Loading
Loading