feat: implement worktree with libfuse-fs overlayfs#346
feat: implement worktree with libfuse-fs overlayfs#346genedna merged 4 commits intoweb3infra-foundation:mainfrom
Conversation
Signed-off-by: KBrynhild <K_Bohemian@outlook.com>
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
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
worktreecommand implementation (src/command/worktree-fuse.rs) that can delegate to the legacy implementation when--fuseis not used. - Wires conditional compilation so
command::worktreeresolves to the FUSE implementation only onunix + 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. |
| [features] | ||
| default = [] | ||
| worktree-fuse = [] # Unix FUSE-backed worktree commands (optional) | ||
| test-network = [] # L2: tests requiring outbound network but no secrets |
There was a problem hiding this comment.
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.
| fn fuse_state_path() -> PathBuf { | ||
| util::storage_path().join("worktrees-fuse.json") | ||
| } | ||
|
|
||
| fn fuse_data_root() -> PathBuf { | ||
| util::storage_path().join("worktrees-fuse") | ||
| } |
There was a problem hiding this comment.
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.
| let mount_path = repo_dir.path().join("wt-fuse-meta"); | ||
| if !is_mounted(&mount_path) || !try_write_probe(&mount_path) { | ||
| return; | ||
| } | ||
|
|
There was a problem hiding this comment.
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”.
Signed-off-by: KBrynhild <K_Bohemian@outlook.com>
There was a problem hiding this comment.
💡 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".
| if let Ok(mut mounts) = active_mounts().lock() { | ||
| mounts.insert(target.to_string_lossy().to_string(), mount_handle); |
There was a problem hiding this comment.
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 👍 / 👎.
| let mount_args = OverlayArgs { | ||
| mountpoint: &target, | ||
| upperdir: &upper_dir, | ||
| lowerdir: vec![lower_dir.clone()], |
There was a problem hiding this comment.
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 👍 / 👎.
| 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}")))?; | ||
| } |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
💡 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".
| pathspec: vec![target.to_string_lossy().to_string()], | ||
| source: Some(checkout_branch.clone()), |
There was a problem hiding this comment.
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 👍 / 👎.
| let mount_args = OverlayArgs { | ||
| mountpoint: &target, | ||
| upperdir: &upper_dir, | ||
| lowerdir: vec![lower_dir.clone()], |
There was a problem hiding this comment.
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 👍 / 👎.
| let checkout_branch = if let Some(name) = create_branch_name.clone().or(branch_name) { | ||
| name |
There was a problem hiding this comment.
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>
| 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 | ||
| ))); | ||
| } |
| if let Ok(mut mounts) = active_mounts().lock() { | ||
| mounts.insert(target.to_string_lossy().to_string(), mount_handle); | ||
| } else { | ||
| rollback_needed = false; |
| fs::canonicalize(normalized) | ||
| } else { | ||
| Ok(normalized) | ||
| } |
| 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, | ||
| }; |
| fn fuse_state_path() -> PathBuf { | ||
| util::storage_path().join("worktrees-fuse.json") | ||
| } | ||
|
|
||
| fn fuse_data_root() -> PathBuf { | ||
| util::storage_path().join("worktrees-fuse") | ||
| } |
| #[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); | ||
| } |
| 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 | ||
| }) | ||
| } |
基于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 实现不变
主要功能: