Skip to content

feat: implement worktree with libfuse-fs overlayfs#346

Merged
genedna merged 4 commits intoweb3infra-foundation:mainfrom
KBrynhild:feat/worktree-fuse
Apr 10, 2026
Merged

feat: implement worktree with libfuse-fs overlayfs#346
genedna merged 4 commits intoweb3infra-foundation:mainfrom
KBrynhild:feat/worktree-fuse

Conversation

@KBrynhild
Copy link
Copy Markdown
Contributor

@KBrynhild KBrynhild commented Apr 8, 2026

基于libfuse-fs实现的worktree功能

为 libra worktree 增加可选的 FUSE 工作树能力(Unix/Linux),并保持默认行为兼容。启用 worktree-fuse feature 后,可通过 --fuse/-f 使用新路径;未启用时仍使用原有实现。

  • 树状结构图:

<repo_root>/
├── .libra/
│ ├── worktrees_fuse.json
│ └── worktrees_fuse/
│ └── <worktree_name>/
│ ├── upper/
│ │ ├── .git
│ │ │ └── gitdir: <repo_root>/.libra/worktrees_fuse/<worktree_name>/meta
│ │ └── ...(该 worktree 的写时复制层,隔离修改)
│ └── meta/
│ ├── HEAD
│ └── index
├── .git/
│ └── objects/
│ ├── info/ (空)
│ └── pack/
│ ├── pack-xxxx.idx
│ ├── pack-xxxx.pack
│ └── pack-xxxx.rev
└── <mount_dir>/ (独立 FUSE 挂载点)
├── ... (overlay 视图)
└── [overlay 规则]
├── lowerdir = <repo_root>
└── upperdir = <repo_root>/.libra/worktrees_fuse/<worktree_name>/upper

  • 兼容性与开关:

  • Feature: worktree-fuse

  • 条件编译接线:unix + feature=worktree-fuse 使用 FUSE 实现

  • 启用特性运行命令(一次性):如使用cargo run 临时运行:cargo run --features worktree-fuse -- worktree

  • 其他场景保持原 worktree 实现不变

  • 主要功能:

  1. worktree add -f 命令:
  • 在指定路径创建独立挂载点
  • 支持 --branch / --create-branch / --from
  • 使用 libfuse-fs::overlayfs::mount_fs 挂载
  • 每个 worktree 独立 upper,共享 lowerdir
  1. worktree list 命令:
  • 列出活动工作树
  • 显示路径、分支、挂载状态、锁状态
  1. worktree remove 命令:
  • 正确卸载 FUSE 挂载点(含回退卸载路径)
  • 清理 upper 目录与状态
  1. worktree lock/unlock 命令:
  • 防误删锁定与解锁后删除
  1. 测试
  • 单元/状态测试:worktree 元数据管理
  • 集成测试:add/list/remove 完整流程
  • FUSE 测试:多 worktree 同时挂载与访问
  • 并发测试:并行 add/remove 正确性

Signed-off-by: KBrynhild <K_Bohemian@outlook.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 29b7aa321f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an optional FUSE-backed overlay worktree implementation for Unix, wired behind the worktree-fuse Cargo feature, while keeping the existing (legacy) worktree behavior as the default.

Changes:

  • Adds a new FUSE-aware worktree command implementation (src/command/worktree-fuse.rs) that can delegate to the legacy implementation when --fuse is not used.
  • Wires conditional compilation so command::worktree resolves to the FUSE implementation only on unix + feature=worktree-fuse.
  • Introduces feature-gated integration tests for the FUSE worktree flow.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
Cargo.toml Adds worktree-fuse feature flag.
src/command/mod.rs Conditionally maps command::worktree to the FUSE implementation on unix + feature.
src/command/worktree.rs Adds an attribute to avoid dead-code warnings when legacy execute isn’t used under FUSE wiring.
src/command/worktree-fuse.rs Implements FUSE overlay worktree add/list/remove/lock/unlock plus mount/unmount handling and state persistence.
tests/command/mod.rs Conditionally includes the new FUSE worktree tests.
tests/command/worktree_fuse_test.rs Adds feature-gated integration tests for FUSE worktree operations.

Comment on lines 6 to 9
[features]
default = []
worktree-fuse = [] # Unix FUSE-backed worktree commands (optional)
test-network = [] # L2: tests requiring outbound network but no secrets
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The worktree-fuse feature is declared as empty, but the FUSE-related crates are included for all Unix builds under [target.'cfg(unix)'.dependencies]. This means building on Unix will always pull in/compile libfuse-fs/rfuse3/libc even when the feature is disabled, which contradicts the “optional” intent. Consider making these dependencies optional = true and enabling them from the worktree-fuse feature (or moving them under a cfg(all(unix, feature = "worktree-fuse")) target dependency section) so default Unix builds remain unaffected.

Copilot uses AI. Check for mistakes.
Comment on lines +289 to +295
fn fuse_state_path() -> PathBuf {
util::storage_path().join("worktrees-fuse.json")
}

fn fuse_data_root() -> PathBuf {
util::storage_path().join("worktrees-fuse")
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description documents .libra/worktrees_fuse.json and .libra/worktrees_fuse/… (underscore naming), but the implementation uses worktrees-fuse.json and worktrees-fuse/… (dash naming). Please align the on-disk naming with the documented layout (or update the PR description) to avoid user confusion and accidental incompatibility with any existing tooling/scripts.

Copilot uses AI. Check for mistakes.
Comment on lines +92 to +96
let mount_path = repo_dir.path().join("wt-fuse-meta");
if !is_mounted(&mount_path) || !try_write_probe(&mount_path) {
return;
}

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests will silently pass if the mount check/probe fails (early return after is_mounted / try_write_probe). If LIBRA_RUN_FUSE_TESTS is set, a failure to mount or to access the mountpoint should usually fail the test (or at least retry/poll for a short period) so regressions don’t get masked as “skipped”.

Copilot uses AI. Check for mistakes.
Signed-off-by: KBrynhild <K_Bohemian@outlook.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d49b73126f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +483 to +484
if let Ok(mut mounts) = active_mounts().lock() {
mounts.insert(target.to_string_lossy().to_string(), mount_handle);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep FUSE mounts alive beyond the add command process

The new mount handle is only retained in the process-local active_mounts map, so this works only while the current CLI process is alive. In the normal libra worktree add --fuse ... flow, the command exits immediately after add, which drops the runtime/process that serves the FUSE session, leaving subsequent invocations with a stale/disconnected mount instead of a durable worktree. This needs a long-lived mount supervisor/daemon (or equivalent lifecycle management) rather than in-process state only.

Useful? React with 👍 / 👎.

Comment on lines +438 to +441
let mount_args = OverlayArgs {
mountpoint: &target,
upperdir: &upper_dir,
lowerdir: vec![lower_dir.clone()],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude .libra from COW upper layer before mounting

This mount overlays the entire repository root (lowerdir) including .libra, so repo metadata mutations done inside the fused worktree (refs/index/objects) are written into that worktree's disposable upperdir copy-on-write layer. Because remove later deletes upper_dir, those metadata updates can be silently discarded, which risks history/state loss for users who run write commands in the fused worktree. The mount layout should keep shared repository storage outside per-worktree COW data.

Useful? React with 👍 / 👎.

Comment on lines +412 to +416
if let Some(new_branch) = create_branch_name.as_ref() {
branch::create_branch_safe(new_branch.clone(), from.clone())
.await
.map_err(|e| io::Error::other(format!("failed to create branch: {e}")))?;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Roll back created branches when add --fuse fails

Branch creation is executed before the FUSE mount is established and validated. If mounting or initial population fails afterward, the command returns an error but the new branch remains, so a failed worktree add --fuse --create-branch ... still mutates repository branch state. Either defer branch creation until after mount success or add rollback for the created branch on failure paths.

Useful? React with 👍 / 👎.

Signed-off-by: KBrynhild <K_Bohemian@outlook.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16dc658488

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +445 to +446
pathspec: vec![target.to_string_lossy().to_string()],
source: Some(checkout_branch.clone()),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore branch contents relative to mounted worktree root

The branch population step passes the mountpoint path itself as the restore pathspec, so restore matches against paths like wt-fuse/... while commit tree entries are repo-relative (for example src/...). In practice this means worktree add --fuse --branch ... (and --create-branch) can return success but leave files on the lowerdir branch, while state records the requested branch. Please run restore from inside the mounted worktree (or rewrite pathspecs to mount-root-relative paths) so the selected branch is actually materialized.

Useful? React with 👍 / 👎.

Comment on lines +420 to +423
let mount_args = OverlayArgs {
mountpoint: &target,
upperdir: &upper_dir,
lowerdir: vec![lower_dir.clone()],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject mountpoints nested under overlay lowerdir

The overlay is mounted with lowerdir set to the repository root and mountpoint set to a user-provided target that is typically inside that same root (for example ./wt). That makes the mounted tree contain itself as a child (wt/wt/...), which can trigger unbounded recursive traversal and path explosion in directory walks (including the immediate restore flow). Add a validation that the mountpoint is outside all lowerdirs, or build a lowerdir view that excludes the mountpoint path.

Useful? React with 👍 / 👎.

Comment on lines +400 to +401
let checkout_branch = if let Some(name) = create_branch_name.clone().or(branch_name) {
name
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make --branch conflict with --create-branch

When both --branch and --create-branch are provided, the command silently prefers --create-branch (create_branch.or(branch)) and still mutates refs by creating a new branch. This is ambiguous CLI behavior and can create an unintended branch while ignoring the explicitly requested checkout branch. Add a clap conflict (or explicit runtime validation) so this combination is rejected with a usage error.

Useful? React with 👍 / 👎.

…global state leak

Signed-off-by: KBrynhild <K_Bohemian@outlook.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.

Comment on lines +442 to +461
let mut rollback_needed = true;
if Head::current_commit().await.is_some() {
if let Err(err) = restore::execute_checked(RestoreArgs {
pathspec: vec![target.to_string_lossy().to_string()],
source: Some(checkout_branch.clone()),
worktree: true,
staged: false,
})
.await
{
let _ = mount_handle.unmount().await;
let _ = fs::remove_dir_all(&upper_dir);
if created_target {
let _ = fs::remove_dir_all(&target);
}
return Err(io::Error::other(format!(
"failed to populate FUSE worktree from '{}': {err}",
checkout_branch
)));
}
Comment on lines +464 to +467
if let Ok(mut mounts) = active_mounts().lock() {
mounts.insert(target.to_string_lossy().to_string(), mount_handle);
} else {
rollback_needed = false;
Comment on lines +287 to +290
fs::canonicalize(normalized)
} else {
Ok(normalized)
}
Comment on lines +419 to +428
let lower_dir = canonicalize_like_worktree(util::working_dir())?;
let mount_args = OverlayArgs {
mountpoint: &target,
upperdir: &upper_dir,
lowerdir: vec![lower_dir.clone()],
privileged,
mapping: None::<&str>,
name: Some("libra-worktree-fuse"),
allow_other,
};
Comment on lines +293 to +299
fn fuse_state_path() -> PathBuf {
util::storage_path().join("worktrees-fuse.json")
}

fn fuse_data_root() -> PathBuf {
util::storage_path().join("worktrees-fuse")
}
Comment on lines +74 to +104
#[tokio::test]
#[serial]
async fn test_fuse_worktree_metadata_management() {
if !can_run_fuse_tests() {
return;
}

let repo_dir = tempdir().expect("create temp repo");
test::setup_with_new_libra_in(repo_dir.path()).await;
let _guard = test::ChangeDirGuard::new(repo_dir.path());

if let Err(err) = exec_async(vec!["worktree", "add", "wt-fuse-meta", "--fuse"]).await {
if is_known_fuse_env_error(&err.to_string()) {
return;
}
panic!("fuse add should succeed: {err}");
}

let mount_path = repo_dir.path().join("wt-fuse-meta");
if !is_mounted(&mount_path) || !try_write_probe(&mount_path) {
return;
}

let state = read_fuse_state();
assert_eq!(state.worktrees.len(), 1);
let entry = &state.worktrees[0];
assert!(entry.path.ends_with("wt-fuse-meta"));
assert!(!entry.upper_dir.is_empty());
assert!(!entry.lower_dirs.is_empty());
assert!(!entry.locked);
}
Comment on lines +60 to +72
fn is_mounted(path: &Path) -> bool {
let Ok(content) = fs::read_to_string("/proc/self/mountinfo") else {
return false;
};
let target = path.to_string_lossy().to_string();
content.lines().any(|line| {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() < 5 {
return false;
}
fields[4].replace("\\040", " ") == target
})
}
@genedna genedna merged commit d03929b into web3infra-foundation:main Apr 10, 2026
11 checks passed
@KBrynhild KBrynhild deleted the feat/worktree-fuse branch April 10, 2026 09:29
@KBrynhild KBrynhild restored the feat/worktree-fuse branch April 10, 2026 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants