docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
This commit is contained in:
@@ -14,9 +14,7 @@ use crate::copy::CopyStats;
|
||||
pub use crate::copy::DirtyFilesReport;
|
||||
use crate::copy::ParallelCopyConfig;
|
||||
|
||||
// ============================================================================
|
||||
// BtrfsDelegate – delegate privileged btrfs ops to an external service
|
||||
// ============================================================================
|
||||
|
||||
/// Result from a delegated btrfs snapshot creation.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -64,7 +62,7 @@ pub trait BtrfsDelegate: Send + Sync {
|
||||
anyhow::bail!("overlay mount delegation not supported by this delegate")
|
||||
}
|
||||
|
||||
/// Unmount an overlay worktree previously mounted via [`Self::mount_overlay`]
|
||||
/// Unmount an overlay worktree that was mounted via [`Self::mount_overlay`]
|
||||
/// (in the caller's mount namespace).
|
||||
fn unmount_overlay(&self, target: &Path) -> Result<()> {
|
||||
let _ = target;
|
||||
@@ -353,7 +351,7 @@ impl WorktreeBuilder {
|
||||
/// modes when the source is on a BTRFS subvolume. This method is only
|
||||
/// needed to *force* or *disable* that auto-detection.
|
||||
pub fn btrfs_mode(self, mode: BtrfsMode) -> Self {
|
||||
// BtrfsMode is now handled inside execute.rs based on CreationMode.
|
||||
// BtrfsMode is handled inside execute.rs based on CreationMode.
|
||||
// This method is kept for backward compatibility with the CLI.
|
||||
tracing::warn!(
|
||||
?mode,
|
||||
@@ -665,7 +663,8 @@ fn remove_worktree_from_disk(
|
||||
worktree_path.display()
|
||||
))?;
|
||||
}
|
||||
Err(_) => {} // nothing at the path
|
||||
// nothing at the path
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
// Deregister: remove the `.git/worktrees/<name>/` directory.
|
||||
@@ -995,7 +994,8 @@ fn try_btrfs_remove(
|
||||
// Case 2 & 3: Check if the worktree path is a btrfs subvolume.
|
||||
let btrfs_info = match btrfs::is_btrfs_subvolume(worktree_path) {
|
||||
Ok(Some(info)) => info,
|
||||
Ok(None) => return Ok(None), // Not a btrfs subvolume, fall back
|
||||
// Not a btrfs subvolume, fall back
|
||||
Ok(None) => return Ok(None),
|
||||
Err(e) => {
|
||||
tracing::debug!(
|
||||
path = %worktree_path.display(),
|
||||
@@ -1672,7 +1672,7 @@ pub mod gc {
|
||||
}
|
||||
// Dry run: count the candidate without touching disk or DB. Skip
|
||||
// a missing path — a real run sweeps it to dead first, so it's
|
||||
// already counted in dead_removed (don't double-count here).
|
||||
// already counted in `dead_removed` (don't double-count here).
|
||||
if opts.dry_run {
|
||||
if path.exists() {
|
||||
report.expired_removed += 1;
|
||||
@@ -2652,7 +2652,7 @@ mod tests {
|
||||
super_options: String::new(),
|
||||
}];
|
||||
|
||||
// `snapshot_path` is intentionally never created, so the privileged
|
||||
// `snapshot_path` is deliberately never created, so the privileged
|
||||
// `btrfs subvolume delete` is gated out (btrfs is unavailable in CI; real
|
||||
// subvolume deletion is exercised only on a btrfs-capable host). The
|
||||
// discriminating signals here are the metadata + dir cleanup.
|
||||
@@ -2705,7 +2705,7 @@ mod tests {
|
||||
super_options: String::new(),
|
||||
}];
|
||||
|
||||
// NOTE: `snapshot_path` is intentionally never created here, so the
|
||||
// NOTE: `snapshot_path` is deliberately never created here, so the
|
||||
// privileged `btrfs subvolume delete` is gated out (btrfs is unavailable
|
||||
// in CI). This test covers the symlink-vs-dir branch selection and the
|
||||
// symlink + metadata cleanup; the real subvolume deletion is exercised
|
||||
@@ -2962,7 +2962,7 @@ mod tests {
|
||||
head_commit: None,
|
||||
session_id: None,
|
||||
creator_pid: Some(my_pid),
|
||||
created_at: 1, // very old
|
||||
created_at: 1,
|
||||
last_accessed_at: None,
|
||||
status: crate::db::WorktreeStatus::Alive,
|
||||
metadata: None,
|
||||
@@ -2972,7 +2972,7 @@ mod tests {
|
||||
// sweep_dead will mark it dead (path doesn't exist),
|
||||
// but gc with max_age should still check liveness for expiry.
|
||||
// Since the path doesn't exist, sweep_dead marks it dead first,
|
||||
// then dead_removed cleans it. Let's use a real existing path instead.
|
||||
// then `dead_removed` cleans it. Let's use a real existing path instead.
|
||||
let dir = tmp.path().join("real-wt");
|
||||
std::fs::create_dir(&dir).unwrap();
|
||||
let mut record2 = record.clone();
|
||||
@@ -2983,7 +2983,8 @@ mod tests {
|
||||
let report = gc::gc_worktrees(
|
||||
&db,
|
||||
&gc::GcOptions {
|
||||
max_age_secs: Some(0), // everything is expired
|
||||
// everything is expired
|
||||
max_age_secs: Some(0),
|
||||
force: false,
|
||||
dry_run: false,
|
||||
},
|
||||
@@ -3028,7 +3029,8 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(report.dead_removed, 1); // counted as would-be-removed
|
||||
// counted as would-be-removed
|
||||
assert_eq!(report.dead_removed, 1);
|
||||
// Dry run must NOT mutate: the record is still present AND still
|
||||
// Alive (it was never swept to Dead).
|
||||
let all = db
|
||||
@@ -3059,8 +3061,8 @@ mod tests {
|
||||
git_ref: None,
|
||||
head_commit: None,
|
||||
session_id: None,
|
||||
creator_pid: Some(std::process::id()), // our own PID
|
||||
created_at: 1, // very old
|
||||
creator_pid: Some(std::process::id()),
|
||||
created_at: 1,
|
||||
last_accessed_at: None,
|
||||
status: crate::db::WorktreeStatus::Alive,
|
||||
metadata: None,
|
||||
@@ -3138,8 +3140,10 @@ mod tests {
|
||||
git_ref: None,
|
||||
head_commit: None,
|
||||
session_id: None,
|
||||
creator_pid: None, // no liveness guard: isolate the age logic
|
||||
created_at: 1, // both are old by creation time
|
||||
// no liveness guard: isolate the age logic
|
||||
creator_pid: None,
|
||||
// both are old by creation time
|
||||
created_at: 1,
|
||||
last_accessed_at: None,
|
||||
status: crate::db::WorktreeStatus::Alive,
|
||||
metadata: None,
|
||||
@@ -3147,14 +3151,16 @@ mod tests {
|
||||
db.register(&crate::db::WorktreeRecord {
|
||||
id: "fresh".to_string(),
|
||||
path: fresh.clone(),
|
||||
last_accessed_at: Some(i64::MAX), // touched within the window
|
||||
// touched within the window
|
||||
last_accessed_at: Some(i64::MAX),
|
||||
..base.clone()
|
||||
})
|
||||
.unwrap();
|
||||
db.register(&crate::db::WorktreeRecord {
|
||||
id: "stale".to_string(),
|
||||
path: stale.clone(),
|
||||
last_accessed_at: Some(1), // never re-touched
|
||||
// never re-touched
|
||||
last_accessed_at: Some(1),
|
||||
..base
|
||||
})
|
||||
.unwrap();
|
||||
@@ -3199,8 +3205,10 @@ mod tests {
|
||||
git_ref: None,
|
||||
head_commit: None,
|
||||
session_id: None,
|
||||
creator_pid: None, // creator gone: only the CWD guard can protect it
|
||||
created_at: 1, // very old → expired
|
||||
// creator gone: only the CWD guard can protect it
|
||||
creator_pid: None,
|
||||
// very old → expired
|
||||
created_at: 1,
|
||||
last_accessed_at: None,
|
||||
status: crate::db::WorktreeStatus::Alive,
|
||||
metadata: None,
|
||||
@@ -3267,8 +3275,9 @@ mod tests {
|
||||
git_ref: None,
|
||||
head_commit: None,
|
||||
session_id: None,
|
||||
creator_pid: None, // no liveness guard
|
||||
created_at: 1, // very old
|
||||
// no liveness guard
|
||||
creator_pid: None,
|
||||
created_at: 1,
|
||||
last_accessed_at: None,
|
||||
status: crate::db::WorktreeStatus::Alive,
|
||||
metadata: None,
|
||||
@@ -3300,7 +3309,7 @@ mod tests {
|
||||
fn gc_dry_run_missing_and_expired_counted_once() {
|
||||
// A record that is Alive, has a MISSING path, AND is expired must be
|
||||
// counted EXACTLY once (a real run sweeps it to dead and unregisters
|
||||
// it before the expired loop). It belongs to dead_removed, not both.
|
||||
// it before the expired loop). It belongs to `dead_removed`, not both.
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let db = db_at(&tmp);
|
||||
|
||||
@@ -3315,7 +3324,8 @@ mod tests {
|
||||
head_commit: None,
|
||||
session_id: None,
|
||||
creator_pid: None,
|
||||
created_at: 1, // very old → expired
|
||||
// very old → expired
|
||||
created_at: 1,
|
||||
last_accessed_at: None,
|
||||
status: crate::db::WorktreeStatus::Alive,
|
||||
metadata: None,
|
||||
@@ -3344,7 +3354,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn gc_expired_failed_removal_keeps_record() {
|
||||
// When the expired worktree can't be removed, expired_removed must
|
||||
// When the expired worktree can't be removed, `expired_removed` must
|
||||
// NOT be counted and the DB record must survive (so it stays
|
||||
// visible to a later gc).
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
@@ -3520,7 +3530,8 @@ mod tests {
|
||||
head_commit: None,
|
||||
session_id: None,
|
||||
creator_pid: None,
|
||||
created_at: 1, // very old → expired
|
||||
// very old → expired
|
||||
created_at: 1,
|
||||
last_accessed_at: None,
|
||||
status: crate::db::WorktreeStatus::Alive,
|
||||
metadata: None,
|
||||
|
||||
@@ -15,7 +15,6 @@ use tracing::{Level, info};
|
||||
|
||||
use kigi_fast_worktree::{BtrfsMode, IgnoredFilesMode, WorkingTreeMode, WorktreeBuilder};
|
||||
|
||||
/// CLI enum for BTRFS mode selection
|
||||
#[derive(Clone, Debug, Default, ValueEnum)]
|
||||
enum CliBtrfsMode {
|
||||
/// Auto-detect: use BTRFS snapshot if source is on a BTRFS subvolume
|
||||
@@ -98,7 +97,6 @@ enum Commands {
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Initialize tracing
|
||||
let level = if cli.verbose {
|
||||
Level::DEBUG
|
||||
} else {
|
||||
@@ -167,7 +165,7 @@ fn main() -> Result<()> {
|
||||
println!(" Path: {}", result.worktree_path.display());
|
||||
println!(" Commit: {}", &result.commit[..12]);
|
||||
|
||||
// For snapshot methods (btrfs/overlay), files_copied will be 0
|
||||
// Snapshot methods (btrfs/overlay) copy nothing, so files_copied is 0.
|
||||
if result.unignored_copy.files_copied > 0 {
|
||||
println!(
|
||||
" Files: {} copied, {} dirs",
|
||||
|
||||
@@ -23,9 +23,7 @@ use clap::Parser;
|
||||
|
||||
use kigi_fast_worktree::{CreationMode, WorktreeBuilder, WorktreeSync, remove_worktree};
|
||||
|
||||
// ============================================================================
|
||||
// CLI
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "pool-perf-bench")]
|
||||
@@ -60,9 +58,7 @@ struct Cli {
|
||||
json: bool,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Timing structs
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct PhaseTiming {
|
||||
@@ -97,9 +93,7 @@ struct BenchmarkSummary {
|
||||
bottleneck: (String, f64),
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Phase runners
|
||||
// ============================================================================
|
||||
|
||||
/// Phase 1: Create a linked worktree via GitCheckout mode (what the pool fill task does)
|
||||
fn phase_create(source: &Path, dest: &Path, parallelism: usize) -> Result<PhaseTiming> {
|
||||
@@ -306,9 +300,7 @@ fn phase_cleanup(_source: &Path, worktree: &Path) -> Result<PhaseTiming> {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// A/B mode: two worktrees concurrently
|
||||
// ============================================================================
|
||||
|
||||
fn run_ab_iteration(
|
||||
source: &Path,
|
||||
@@ -499,9 +491,7 @@ fn run_single_iteration(
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
fn count_tracked_files(source: &Path) -> Result<usize> {
|
||||
kigi_fast_worktree::count_tracked_files(source)
|
||||
@@ -553,9 +543,7 @@ fn compute_summary(iterations: &[IterationResult]) -> BenchmarkSummary {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Output
|
||||
// ============================================================================
|
||||
|
||||
fn print_iteration(result: &IterationResult) {
|
||||
println!();
|
||||
@@ -683,9 +671,7 @@ fn print_json(result: &BenchmarkResult) {
|
||||
println!("}}");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main
|
||||
// ============================================================================
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
@@ -153,7 +153,8 @@ fn resolve_bind_mount_source(target: &Path) -> Result<Option<PathBuf>> {
|
||||
}
|
||||
|
||||
// Found our mount point
|
||||
let root = parts[3]; // The root within the filesystem
|
||||
// The root within the filesystem
|
||||
let root = parts[3];
|
||||
let fstype_idx = parts.iter().position(|&p| p == "-").map(|i| i + 1);
|
||||
|
||||
if let Some(fstype_idx) = fstype_idx {
|
||||
@@ -664,7 +665,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Unit tests for resolve_via_subvol_mount ─────────────────────────
|
||||
// Unit tests for resolve_via_subvol_mount
|
||||
|
||||
#[test]
|
||||
fn test_resolve_via_subvol_mount_exact_match() {
|
||||
|
||||
@@ -234,9 +234,11 @@ pub fn create_snapshot_with_symlink(btrfs_info: &BtrfsInfo, dest: &Path) -> Resu
|
||||
/// the identical layout.
|
||||
pub fn snapshot_dest_path(btrfs_mount: &Path, subvolume_root: &Path, dest: &Path) -> PathBuf {
|
||||
let subdir = if btrfs_mount == subvolume_root {
|
||||
BTRFS_SNAPSHOT_SUBDIRS[1] // ".kigi-snapshots"
|
||||
// ".kigi-snapshots"
|
||||
BTRFS_SNAPSHOT_SUBDIRS[1]
|
||||
} else {
|
||||
BTRFS_SNAPSHOT_SUBDIRS[0] // "worktrees"
|
||||
// "worktrees"
|
||||
BTRFS_SNAPSHOT_SUBDIRS[0]
|
||||
};
|
||||
let basename = dest
|
||||
.file_name()
|
||||
|
||||
@@ -104,7 +104,7 @@ mod tests {
|
||||
let dst = temp.path().join("link");
|
||||
|
||||
std::fs::write(&dst, "stale").unwrap();
|
||||
// Target is intentionally dangling; it must still be created.
|
||||
// Target is deliberately dangling; it must still be created.
|
||||
replace_symlink(Path::new("does-not-exist"), &dst).unwrap();
|
||||
|
||||
let meta = std::fs::symlink_metadata(&dst).unwrap();
|
||||
|
||||
@@ -98,16 +98,20 @@ pub(crate) fn copy_parallel(
|
||||
// Deep directory trees (15+ levels) amplify this significantly.
|
||||
let mut builder = WalkBuilder::new(source);
|
||||
builder
|
||||
.hidden(false) // Include hidden files.
|
||||
// Include hidden files.
|
||||
.hidden(false)
|
||||
.git_ignore(config.respect_gitignore)
|
||||
.git_global(false) // Never use global gitignore (~/.config/git/ignore) —
|
||||
// Never use global gitignore (~/.config/git/ignore) —
|
||||
.git_global(false)
|
||||
// it contains personal preferences irrelevant to worktree creation.
|
||||
.git_exclude(false) // Never use .git/info/exclude — external tooling
|
||||
// Never use .git/info/exclude — external tooling
|
||||
.git_exclude(false)
|
||||
// can append broad patterns (*.min.js, *.zip) that
|
||||
// incorrectly skip git-tracked files. The `ignore` crate doesn't
|
||||
// check tracking status, so tracked files matching these patterns
|
||||
// get silently dropped during the copy.
|
||||
.threads(num_workers) // Limit walker parallelism to avoid FD exhaustion
|
||||
// Limit walker parallelism to avoid FD exhaustion
|
||||
.threads(num_workers)
|
||||
.filter_entry(|entry| {
|
||||
// Always skip .git directory.
|
||||
entry.file_name() != ".git"
|
||||
@@ -274,7 +278,8 @@ mod tests {
|
||||
assert!(dest.path().join("file1.txt").exists());
|
||||
assert!(dest.path().join("file2.txt").exists());
|
||||
assert!(dest.path().join("subdir/file3.txt").exists());
|
||||
assert_eq!(result.copied_paths.len(), 4); // 3 files + 1 dir
|
||||
// 3 files + 1 dir
|
||||
assert_eq!(result.copied_paths.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
//! Copies essential git internal files using reflink (CoW) when supported,
|
||||
//! skipping transient state, lock files, and stale worktree registrations.
|
||||
//!
|
||||
//! The `objects/` directory (often the largest subtree) is copied in parallel
|
||||
//! using a thread pool for better throughput on SSDs.
|
||||
//! The tree is walked once to enumerate entries, then the copies are sharded
|
||||
//! across scoped threads for throughput on SSDs.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
@@ -13,7 +13,6 @@ use anyhow::{Context, Result};
|
||||
|
||||
use crate::copy::cow::clone_file;
|
||||
|
||||
/// Statistics from copying the `.git/` directory.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct GitDirCopyStats {
|
||||
pub files_copied: u64,
|
||||
@@ -27,9 +26,7 @@ pub(crate) struct GitDirCopyStats {
|
||||
/// These are either transient state (merge/rebase in-progress markers) or
|
||||
/// linked-worktree metadata that would be stale in the copy.
|
||||
const SKIP_TOP_LEVEL: &[&str] = &[
|
||||
// Linked worktree registrations — stale in a standalone copy
|
||||
"worktrees",
|
||||
// Transient HEAD-like state files
|
||||
"FETCH_HEAD",
|
||||
"ORIG_HEAD",
|
||||
"MERGE_HEAD",
|
||||
@@ -38,11 +35,9 @@ const SKIP_TOP_LEVEL: &[&str] = &[
|
||||
"REBASE_HEAD",
|
||||
"AUTO_MERGE",
|
||||
"BISECT_LOG",
|
||||
// In-progress multi-step operation state
|
||||
"sequencer",
|
||||
"rebase-merge",
|
||||
"rebase-apply",
|
||||
// GC state
|
||||
"gc.log",
|
||||
// fsmonitor daemon state — a host-local Unix-domain IPC socket
|
||||
// (`fsmonitor--daemon.ipc`, which cannot be reflinked/copied) plus its
|
||||
@@ -52,27 +47,16 @@ const SKIP_TOP_LEVEL: &[&str] = &[
|
||||
"fsmonitor--daemon.ipc",
|
||||
];
|
||||
|
||||
/// A work item for the parallel copy pool.
|
||||
struct CopyWork {
|
||||
source: PathBuf,
|
||||
dest: PathBuf,
|
||||
}
|
||||
|
||||
/// Copy `.git/` directory contents using CoW, skipping unnecessary entries.
|
||||
/// Build a standalone git repository's `.git/` at `dest_git` by selectively
|
||||
/// copying from `source_git`, using reflink (CoW) where the filesystem
|
||||
/// supports it and falling back to a regular copy otherwise.
|
||||
///
|
||||
/// Creates a standalone git repository's `.git/` at `dest_git` by selectively
|
||||
/// copying from `source_git`. Files are copied using reflink (CoW) when the
|
||||
/// filesystem supports it, falling back to regular copy otherwise.
|
||||
///
|
||||
/// The `objects/` subtree is copied in parallel (it's typically the largest
|
||||
/// part and has no ordering dependencies). Other top-level entries are copied
|
||||
/// sequentially.
|
||||
///
|
||||
/// Skips:
|
||||
/// - Lock files (`*.lock`) at any depth
|
||||
/// - Stale worktree registrations (`worktrees/`)
|
||||
/// - Transient state files (`MERGE_HEAD`, `CHERRY_PICK_HEAD`, etc.)
|
||||
/// - In-progress rebase/cherry-pick state (`sequencer/`, `rebase-merge/`)
|
||||
/// Lock files at any depth and the [`SKIP_TOP_LEVEL`] entries are left behind.
|
||||
pub(crate) fn copy_git_dir(source_git: &Path, dest_git: &Path) -> Result<GitDirCopyStats> {
|
||||
copy_git_dir_with_workers(source_git, dest_git, num_cpus::get())
|
||||
}
|
||||
@@ -95,8 +79,6 @@ fn copy_git_dir_with_workers(
|
||||
let symlinks_copied = AtomicU64::new(0);
|
||||
let entries_skipped = AtomicU64::new(0);
|
||||
|
||||
// First pass: collect work items for parallel copy.
|
||||
// We collect all (source, dest) pairs, then process them in parallel.
|
||||
let mut work_items: Vec<CopyWork> = Vec::new();
|
||||
collect_work_recursive(
|
||||
source_git,
|
||||
@@ -107,7 +89,6 @@ fn copy_git_dir_with_workers(
|
||||
&entries_skipped,
|
||||
)?;
|
||||
|
||||
// Process file copies in parallel using scoped threads.
|
||||
let num_workers = max_workers.min(work_items.len().max(1));
|
||||
|
||||
if num_workers <= 1 || work_items.len() < 64 {
|
||||
@@ -182,10 +163,9 @@ fn copy_git_dir_with_workers(
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
/// Recursively collect work items (files/symlinks to copy), creating directories eagerly.
|
||||
///
|
||||
/// Directories are created immediately (they must exist before files are written),
|
||||
/// but file copies are deferred to the work list for parallel processing.
|
||||
/// Walk `source`, creating every directory eagerly (they must exist before the
|
||||
/// copy workers write into them) while deferring files and symlinks to
|
||||
/// `work_items` for parallel copying.
|
||||
fn collect_work_recursive(
|
||||
source: &Path,
|
||||
dest: &Path,
|
||||
@@ -230,7 +210,6 @@ fn collect_work_recursive(
|
||||
entries_skipped,
|
||||
)?;
|
||||
} else if file_type.is_file() || file_type.is_symlink() {
|
||||
// Regular file or symlink — add to work list.
|
||||
work_items.push(CopyWork {
|
||||
source: source_path,
|
||||
dest: dest_path,
|
||||
@@ -249,14 +228,12 @@ fn collect_work_recursive(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy a single file or symlink entry.
|
||||
fn copy_single_entry(
|
||||
source_path: &Path,
|
||||
dest_path: &Path,
|
||||
files_copied: &AtomicU64,
|
||||
symlinks_copied: &AtomicU64,
|
||||
) -> Result<()> {
|
||||
// Check if it's a symlink by querying symlink metadata.
|
||||
let metadata = std::fs::symlink_metadata(source_path)
|
||||
.with_context(|| format!("failed to stat {}", source_path.display()))?;
|
||||
|
||||
@@ -301,13 +278,10 @@ fn copy_single_entry(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decide whether to skip a `.git/` entry based on its name and depth.
|
||||
fn should_skip(name: &str, depth: usize) -> bool {
|
||||
// Skip lock files at any depth
|
||||
if name.ends_with(".lock") {
|
||||
return true;
|
||||
}
|
||||
// Skip known top-level entries
|
||||
if depth == 0 && SKIP_TOP_LEVEL.contains(&name) {
|
||||
return true;
|
||||
}
|
||||
@@ -325,7 +299,6 @@ mod tests {
|
||||
let source_git = temp.path().join("source/.git");
|
||||
let dest_git = temp.path().join("dest/.git");
|
||||
|
||||
// Create a minimal .git structure
|
||||
std::fs::create_dir_all(source_git.join("objects/pack")).unwrap();
|
||||
std::fs::create_dir_all(source_git.join("refs/heads")).unwrap();
|
||||
std::fs::write(source_git.join("HEAD"), "ref: refs/heads/main\n").unwrap();
|
||||
@@ -539,7 +512,6 @@ mod tests {
|
||||
|
||||
let err = copy_git_dir_with_workers(&source_git, &dest_git, 4)
|
||||
.expect_err("a failed .git/ entry copy must propagate as an error");
|
||||
// The error names the failing entry, not some unrelated setup failure.
|
||||
let chain = format!("{err:#}");
|
||||
assert!(
|
||||
chain.contains("obj0"),
|
||||
|
||||
@@ -17,17 +17,13 @@ fn rapidhash_path(path: &Path) -> u64 {
|
||||
rapidhash_v3(bytes)
|
||||
}
|
||||
|
||||
/// Compute the shard index for a path based on its parent directory.
|
||||
///
|
||||
/// Files in the same directory will always be assigned to the same shard,
|
||||
/// which avoids lock contention when creating parent directories.
|
||||
/// Sharded on the parent directory, so files in the same directory always land
|
||||
/// in the same shard and never contend on creating their parent.
|
||||
pub(crate) fn shard_for_path(path: &Path, num_shards: usize) -> usize {
|
||||
let parent = path.parent().unwrap_or(path);
|
||||
(rapidhash_path(parent) as usize) % num_shards
|
||||
}
|
||||
|
||||
/// Deterministic 16-hex-char (full 64-bit) hash of a path's full bytes.
|
||||
///
|
||||
/// Disambiguates same-basename worktrees that share a basename-derived key (btrfs
|
||||
/// snapshot name, worktree DB id). Full 64 bits keep a collision astronomically
|
||||
/// unlikely.
|
||||
@@ -53,7 +49,6 @@ mod tests {
|
||||
let shard2 = shard_for_path(&file2, num_shards);
|
||||
let shard3 = shard_for_path(&file3, num_shards);
|
||||
|
||||
// All files in src/ should go to the same shard
|
||||
assert_eq!(shard1, shard2);
|
||||
assert_eq!(shard2, shard3);
|
||||
}
|
||||
@@ -65,10 +60,10 @@ mod tests {
|
||||
|
||||
let num_shards = 8;
|
||||
|
||||
// Different directories may (but don't have to) produce different shards
|
||||
// Different directories may collide onto one shard, so there is nothing
|
||||
// to assert beyond "does not panic".
|
||||
let _shard1 = shard_for_path(&file1, num_shards);
|
||||
let _shard2 = shard_for_path(&file2, num_shards);
|
||||
// Just verify it doesn't panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -7,7 +7,6 @@ use anyhow::Result;
|
||||
use dashmap::DashSet;
|
||||
use ignore::{WalkBuilder, WalkState};
|
||||
|
||||
/// Build a globset matcher for skip patterns.
|
||||
pub(crate) fn build_skip_matcher(patterns: &[String]) -> Result<globset::GlobSet> {
|
||||
let mut builder = globset::GlobSetBuilder::new();
|
||||
for pattern in patterns {
|
||||
@@ -16,10 +15,10 @@ pub(crate) fn build_skip_matcher(patterns: &[String]) -> Result<globset::GlobSet
|
||||
Ok(builder.build()?)
|
||||
}
|
||||
|
||||
/// Collect all *unignored* paths in `source` (relative).
|
||||
/// Collect all *unignored* paths in `source`, relative to it.
|
||||
///
|
||||
/// This is used to implement an "ignored-only" copy: by collecting unignored paths
|
||||
/// and then skipping them during a second pass with `respect_gitignore=false`.
|
||||
/// Backs the "ignored-only" copy: a second pass runs with
|
||||
/// `respect_gitignore=false` and skips everything in this set.
|
||||
pub(crate) fn collect_unignored_paths(
|
||||
source: &Path,
|
||||
parallelism: usize,
|
||||
@@ -93,7 +92,6 @@ mod tests {
|
||||
std::fs::create_dir_all(&info_dir).unwrap();
|
||||
std::fs::write(info_dir.join("exclude"), "*.zip\n").unwrap();
|
||||
|
||||
// A truly-ignored (gitignored, untracked) artifact.
|
||||
std::fs::create_dir(repo.join("build")).unwrap();
|
||||
std::fs::write(repo.join("build/out.o"), "obj").unwrap();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::sync::Arc;
|
||||
|
||||
use dashmap::{DashMap, DashSet};
|
||||
|
||||
/// A structured report about dirty (modified/untracked/deleted) files in the source worktree.
|
||||
/// Counts of dirty files in the *source* worktree.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DirtyFilesReport {
|
||||
pub modified_files: u64,
|
||||
@@ -14,7 +14,6 @@ pub struct DirtyFilesReport {
|
||||
pub deleted_files: u64,
|
||||
}
|
||||
|
||||
/// Statistics from a copy operation.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CopyStats {
|
||||
pub files_copied: u64,
|
||||
@@ -26,7 +25,6 @@ pub struct CopyStats {
|
||||
}
|
||||
|
||||
impl CopyStats {
|
||||
/// Merge another stats into this one.
|
||||
pub fn merge(&mut self, other: CopyStats) {
|
||||
self.files_copied += other.files_copied;
|
||||
self.dirs_created += other.dirs_created;
|
||||
@@ -36,7 +34,6 @@ impl CopyStats {
|
||||
}
|
||||
}
|
||||
|
||||
/// Kind of filesystem entry to replicate.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum CopyEntryKind {
|
||||
File,
|
||||
@@ -44,25 +41,22 @@ pub(crate) enum CopyEntryKind {
|
||||
Symlink,
|
||||
}
|
||||
|
||||
/// Entry to be processed by a worker.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CopyEntry {
|
||||
pub(crate) rel_path: PathBuf,
|
||||
pub(crate) kind: CopyEntryKind,
|
||||
}
|
||||
|
||||
/// Configuration for the parallel copy operation.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct ParallelCopyConfig {
|
||||
/// Number of parallel workers (0 = num_cpus)
|
||||
/// 0 means "one per CPU".
|
||||
pub num_workers: usize,
|
||||
/// Channel buffer size per shard
|
||||
/// Channel buffer size, per shard.
|
||||
pub channel_buffer: usize,
|
||||
/// Files to skip (relative paths)
|
||||
/// Paths relative to the source root.
|
||||
pub skip_files: Option<Arc<DashSet<PathBuf>>>,
|
||||
/// Whether to respect `.gitignore` rules
|
||||
pub respect_gitignore: bool,
|
||||
/// Additional patterns to skip (glob patterns)
|
||||
/// Globs, applied on top of `skip_files`.
|
||||
pub skip_patterns: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ pub(crate) struct WorkerCtx {
|
||||
}
|
||||
|
||||
pub(crate) fn run_worker(rx: crossbeam::channel::Receiver<CopyEntry>, ctx: WorkerCtx) {
|
||||
// Track created directories to avoid redundant mkdir calls.
|
||||
let mut created_dirs: HashSet<PathBuf> = HashSet::new();
|
||||
|
||||
for entry in rx {
|
||||
@@ -59,7 +58,8 @@ fn process_entry(
|
||||
issues: &std::sync::Mutex<Vec<String>>,
|
||||
file_metadata: &DashMap<PathBuf, Metadata>,
|
||||
) -> bool {
|
||||
// Ensure parent directory exists.
|
||||
// `created_dirs.insert` yields false for a parent this worker already made,
|
||||
// short-circuiting the chain so the mkdir syscall is skipped.
|
||||
if let Some(parent) = dst.parent()
|
||||
&& !parent.as_os_str().is_empty()
|
||||
&& created_dirs.insert(parent.to_path_buf())
|
||||
@@ -116,7 +116,6 @@ fn process_entry(
|
||||
Ok(()) => {
|
||||
files_copied.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
// Collect file metadata for index updates
|
||||
if let Ok(metadata) = std::fs::metadata(dst) {
|
||||
file_metadata.insert(entry.rel_path.clone(), metadata);
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ impl WorktreeDb {
|
||||
.with_context(|| format!("failed to open worktree DB: {}", path.display()))?;
|
||||
let db = Self { conn };
|
||||
db.set_journal_mode(journal_mode)?;
|
||||
// Normal statement timeout, now that the conversion budget is done.
|
||||
// Normal statement timeout, because the conversion budget is done.
|
||||
db.conn
|
||||
.busy_timeout(std::time::Duration::from_millis(5000))?;
|
||||
db.init_schema()?;
|
||||
|
||||
@@ -166,6 +166,7 @@ pub fn list(conn: &Connection, filter: &ListFilter) -> Result<Vec<WorktreeRecord
|
||||
}
|
||||
sql.push_str(" ORDER BY created_at DESC");
|
||||
|
||||
// Push order must match the ?N numbering handed out by the clauses above.
|
||||
let mut params: Vec<&dyn rusqlite::types::ToSql> = Vec::with_capacity(idx);
|
||||
if let Some(ref s) = status_str {
|
||||
params.push(s);
|
||||
|
||||
@@ -68,7 +68,8 @@ fn unregister_by_id() {
|
||||
|
||||
assert!(db.unregister("a").unwrap());
|
||||
assert!(db.get("a").unwrap().is_none());
|
||||
assert!(!db.unregister("a").unwrap()); // second call returns false
|
||||
// second call returns false
|
||||
assert!(!db.unregister("a").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -629,7 +630,7 @@ fn network_mode_uses_fresh_per_host_truncate_db() {
|
||||
|
||||
let db = WorktreeDb::open_at_with_journal_mode(&path, JournalMode::Truncate).unwrap();
|
||||
assert_eq!(journal_mode(&db), "truncate");
|
||||
// Fresh per-host DB: legacy rows are intentionally not visible.
|
||||
// Fresh per-host DB: legacy rows are deliberately not visible.
|
||||
assert!(db.get("wt-legacy").unwrap().is_none());
|
||||
db.register(&make_record("wt-nfs", "/tmp/wt-nfs", WorktreeKind::Manual))
|
||||
.unwrap();
|
||||
|
||||
@@ -299,7 +299,6 @@ mod tests {
|
||||
assert!(db.get(&wt_a.to_string_lossy()).unwrap().is_some());
|
||||
assert!(db.get(&wt_b.to_string_lossy()).unwrap().is_some());
|
||||
|
||||
// Idempotent: a second rebuild finds both already tracked, skips neither.
|
||||
let report2 = rebuild_worktree_db(&db, kigi_home).unwrap();
|
||||
assert_eq!(report2.registered, 0);
|
||||
assert_eq!(report2.already_tracked, 2);
|
||||
|
||||
@@ -445,7 +445,7 @@ fn rehydrate_worktree_from_ref_inner(
|
||||
) -> Result<WorktreeReport> {
|
||||
let dest_str = dest.to_string_lossy();
|
||||
|
||||
// A previously-disposed worktree can leave a stale registration for this
|
||||
// An earlier-disposed worktree can leave a stale registration for this
|
||||
// path; prune it so re-adding the original `subagent-<id>` dir succeeds.
|
||||
snapshot_git(source_repo, &["worktree", "prune"], &[])?;
|
||||
|
||||
|
||||
@@ -145,7 +145,8 @@ mod tests {
|
||||
git_commit_all(temp.path(), "initial");
|
||||
|
||||
let commit = get_head_commit(temp.path()).unwrap();
|
||||
assert_eq!(commit.len(), 40); // SHA-1 hex string
|
||||
// SHA-1 hex string
|
||||
assert_eq!(commit.len(), 40);
|
||||
assert!(commit.chars().all(|c| c.is_ascii_hexdigit()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
//! Git operations used by fast worktree creation.
|
||||
//!
|
||||
//! This module isolates git-specific functionality (worktree creation, status, index refresh)
|
||||
//! from filesystem copy logic and orchestration.
|
||||
|
||||
pub(crate) mod checkout;
|
||||
pub(crate) mod discovery;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
//! Git worktree operations.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::git::checkout::git_command;
|
||||
|
||||
/// Create a git worktree with `--no-checkout`. Blocking.
|
||||
pub(crate) fn worktree_add_no_checkout(source: &Path, dest: &str, git_ref: &str) -> Result<()> {
|
||||
let output = git_command()
|
||||
.current_dir(source)
|
||||
|
||||
@@ -52,11 +52,10 @@ pub use sync::{SourceDirtyState, SyncReport, WorktreeSync, collect_source_dirty_
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use worktree::execute::cleanup_snapshot_git_state;
|
||||
|
||||
/// Count the number of tracked files in a git repository's index.
|
||||
/// Count the tracked files in a git repository's index.
|
||||
///
|
||||
/// Reads the index header via `gix`, which contains the entry count — this
|
||||
/// is an O(1) read (no directory walk). Useful for deciding whether a repo
|
||||
/// is large enough to benefit from worktree pooling.
|
||||
/// Reads the entry count out of the index header via `gix`, so this is an
|
||||
/// O(1) read with no directory walk.
|
||||
pub fn count_tracked_files(repo_path: &std::path::Path) -> anyhow::Result<usize> {
|
||||
let repo = gix::discover(repo_path)
|
||||
.map_err(|e| anyhow::anyhow!("failed to discover git repo: {e}"))?;
|
||||
|
||||
@@ -15,7 +15,6 @@ use anyhow::{Context, Result};
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MountEntry {
|
||||
/// Mount ID.
|
||||
#[allow(dead_code)]
|
||||
pub mount_id: u32,
|
||||
/// Parent mount ID.
|
||||
@@ -243,7 +242,7 @@ pub fn is_fuse_mount(entries: &[MountEntry], path: &Path) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
// ── Internal helpers ─────────────────────────────────────────────────────
|
||||
// Internal helpers
|
||||
|
||||
/// Parse a single mountinfo line.
|
||||
fn parse_line(line: &str) -> Option<MountEntry> {
|
||||
|
||||
@@ -30,10 +30,10 @@ pub struct OverlayInfo {
|
||||
pub overlay_root: PathBuf,
|
||||
}
|
||||
|
||||
/// Detect if `path` is on a FUSE+overlayfs stack with btrfs upper.
|
||||
/// Detect whether `path` is on a FUSE+overlayfs stack with a btrfs upper.
|
||||
///
|
||||
/// Returns `Ok(Some(OverlayInfo))` if all conditions are met, `Ok(None)` otherwise.
|
||||
/// Handles `EIO`/`ENOTCONN` from a crashed FUSE daemon gracefully by returning `Ok(None)`.
|
||||
/// A crashed FUSE daemon (`EIO`/`ENOTCONN`) is not an error here: it yields
|
||||
/// `Ok(None)` like any other unsuitable mount.
|
||||
pub fn detect_fuse_overlay(path: &Path) -> Result<Option<OverlayInfo>> {
|
||||
let entries = match mount_info::parse_mountinfo() {
|
||||
Ok(entries) => entries,
|
||||
@@ -46,12 +46,12 @@ pub fn detect_fuse_overlay(path: &Path) -> Result<Option<OverlayInfo>> {
|
||||
detect_fuse_overlay_from_entries(path, &entries)
|
||||
}
|
||||
|
||||
/// Testable version that takes pre-parsed entries.
|
||||
/// Split out from `detect_fuse_overlay` so tests can drive it with synthetic
|
||||
/// mountinfo instead of the host's real mount table.
|
||||
pub(crate) fn detect_fuse_overlay_from_entries(
|
||||
path: &Path,
|
||||
entries: &[mount_info::MountEntry],
|
||||
) -> Result<Option<OverlayInfo>> {
|
||||
// Step 1: Find overlay mount containing this path.
|
||||
let overlay = match mount_info::find_overlay_mount(entries, path) {
|
||||
Some(info) => info,
|
||||
None => {
|
||||
@@ -60,7 +60,6 @@ pub(crate) fn detect_fuse_overlay_from_entries(
|
||||
}
|
||||
};
|
||||
|
||||
// Step 2: Verify the lower layer is a FUSE mount.
|
||||
if !mount_info::is_fuse_mount(entries, &overlay.lower_dir) {
|
||||
tracing::debug!(
|
||||
lower = %overlay.lower_dir.display(),
|
||||
@@ -69,12 +68,12 @@ pub(crate) fn detect_fuse_overlay_from_entries(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Step 3: Verify the upper layer is on btrfs.
|
||||
let upper_on_btrfs = match crate::btrfs::is_btrfs(&overlay.upper_dir) {
|
||||
Ok(true) => true,
|
||||
Ok(false) => false,
|
||||
Err(e) => {
|
||||
// EIO / ENOTCONN from crashed FUSE — treat as "not available"
|
||||
// EIO/ENOTCONN from a crashed FUSE daemon lands here; treat any
|
||||
// probe failure as "not btrfs" rather than failing detection.
|
||||
tracing::debug!(
|
||||
upper = %overlay.upper_dir.display(),
|
||||
error = %e,
|
||||
@@ -92,7 +91,6 @@ pub(crate) fn detect_fuse_overlay_from_entries(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Derive overlay_root — parent of upper_dir (sibling of upper/ and work/).
|
||||
let overlay_root = overlay
|
||||
.upper_dir
|
||||
.parent()
|
||||
@@ -151,7 +149,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_detect_overlay_without_fuse_lower() {
|
||||
// Overlay where lower is ext4, not FUSE — should return None.
|
||||
let mountinfo = "\
|
||||
22 1 8:1 / / rw - ext4 /dev/sda1 rw
|
||||
30 22 8:2 / /lower rw - ext4 /dev/sda2 rw
|
||||
@@ -176,15 +173,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_overlay_info_fields() {
|
||||
// We can't run the btrfs check in unit tests (no btrfs fs), but we
|
||||
// can verify the parsing portion works by calling the internal function
|
||||
// and checking that step 3 (btrfs) is the failing point.
|
||||
// The sample upper path does not exist on the test host, so the btrfs
|
||||
// probe fails and detection stops short of `Some`. This only covers
|
||||
// the mountinfo parsing path; the result is deliberately unasserted.
|
||||
let entries = parse_mountinfo_from(FUSE_OVERLAY_MOUNTINFO);
|
||||
// This will return None because the sample upper path doesn't exist,
|
||||
// so is_btrfs will fail — but that's expected in a unit test.
|
||||
let result = detect_fuse_overlay_from_entries(Path::new("/workspace/repo"), &entries);
|
||||
assert!(result.is_ok());
|
||||
// On a system without the actual btrfs mount, this returns None.
|
||||
// On a host with a live FUSE+overlay stack it would return Some.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,7 +513,7 @@ pub fn cleanup_orphaned_overlay_snapshots() -> crate::api::CleanupReport {
|
||||
report
|
||||
}
|
||||
|
||||
// ── Internal helpers ─────────────────────────────────────────────────────
|
||||
// Internal helpers
|
||||
|
||||
/// Mount overlayfs using `libc::mount()` syscall.
|
||||
fn mount_overlay(lower: &Path, upper: &Path, work: &Path, target: &Path) -> Result<()> {
|
||||
|
||||
@@ -95,7 +95,7 @@ pub struct SyncReport {
|
||||
/// Whether dirty sync was skipped because pre-computed state was empty.
|
||||
pub dirty_skipped: bool,
|
||||
|
||||
// ── Per-phase timing (milliseconds) ─────────────────────────────────
|
||||
// Per-phase timing (milliseconds)
|
||||
/// Time to resolve HEAD commits on source + worktree (gix).
|
||||
pub head_resolve_ms: u64,
|
||||
/// Time for `git reset --hard` (0 if HEAD didn't move).
|
||||
@@ -1310,14 +1310,12 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// skip_clean=true tests (pool path)
|
||||
//
|
||||
// The worktree pool calls sync_worktree_opts(copy_dirty, skip_clean=true)
|
||||
// because pool worktrees are known-clean (freshly created or just
|
||||
// released). These tests verify that commits, dirty files, and untracked
|
||||
// files are correctly replicated through that code path.
|
||||
// ========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_skip_clean_commit_replication() {
|
||||
@@ -1513,7 +1511,7 @@ mod tests {
|
||||
|
||||
let worktree = create_linked_worktree(&source, "wt1");
|
||||
|
||||
// --- First sync: source advanced ---
|
||||
// First sync: source advanced
|
||||
std::fs::write(source.join("file.txt"), "v2").unwrap();
|
||||
git_commit_all(&source, "second");
|
||||
|
||||
@@ -1525,7 +1523,7 @@ mod tests {
|
||||
"v2"
|
||||
);
|
||||
|
||||
// --- Simulate release: reset --hard + clean (what the pool does) ---
|
||||
// Simulate release: reset --hard + clean (what the pool does)
|
||||
Command::new("git")
|
||||
.current_dir(&worktree)
|
||||
.args(["reset", "--hard"])
|
||||
@@ -1537,7 +1535,7 @@ mod tests {
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
// --- Second sync: source advanced again with dirty state ---
|
||||
// Second sync: source advanced again with dirty state
|
||||
std::fs::write(source.join("file.txt"), "v3").unwrap();
|
||||
git_commit_all(&source, "third");
|
||||
std::fs::write(source.join("file.txt"), "v3-dirty").unwrap();
|
||||
@@ -1666,7 +1664,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── sync_from_precomputed tests ──
|
||||
// sync_from_precomputed tests
|
||||
|
||||
#[test]
|
||||
fn test_sync_from_precomputed_none_skips_dirty() {
|
||||
|
||||
@@ -277,7 +277,6 @@ fn execute_create_worktree_dispatch(plan: WorktreePlan) -> Result<CreateWorktree
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try BTRFS snapshot (O(1), no file copies)
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
match try_btrfs_worktree(&plan) {
|
||||
@@ -293,7 +292,6 @@ fn execute_create_worktree_dispatch(plan: WorktreePlan) -> Result<CreateWorktree
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fall back to file-by-file copy
|
||||
#[cfg(target_os = "linux")]
|
||||
if !skipped_reasons.is_empty() {
|
||||
tracing::info!(
|
||||
@@ -502,8 +500,10 @@ fn execute_overlay_worktree(
|
||||
Ok(CreateWorktreeResult {
|
||||
worktree_path: plan.dest,
|
||||
commit,
|
||||
copy_stats: CopyStats::default(), // 0 files copied!
|
||||
ignored_stats: None, // overlay includes everything
|
||||
// 0 files copied!
|
||||
copy_stats: CopyStats::default(),
|
||||
// overlay includes everything
|
||||
ignored_stats: None,
|
||||
dirty_files_report: None,
|
||||
})
|
||||
}
|
||||
@@ -760,9 +760,12 @@ fn execute_btrfs_worktree(
|
||||
Ok(CreateWorktreeResult {
|
||||
worktree_path: plan.dest,
|
||||
commit,
|
||||
copy_stats: CopyStats::default(), // No files copied - instant snapshot!
|
||||
ignored_stats: None, // Snapshot includes everything
|
||||
dirty_files_report: None, // Not tracked for BTRFS snapshots
|
||||
// No files copied - instant snapshot!
|
||||
copy_stats: CopyStats::default(),
|
||||
// Snapshot includes everything
|
||||
ignored_stats: None,
|
||||
// Not tracked for BTRFS snapshots
|
||||
dirty_files_report: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -897,7 +900,8 @@ fn execute_copy_worktree(plan: WorktreePlan) -> Result<CreateWorktreeResult> {
|
||||
num_workers: effective_ignored_parallelism,
|
||||
channel_buffer,
|
||||
skip_files: Some(Arc::new(already_copied)),
|
||||
respect_gitignore: false, // We want all files.
|
||||
// We want all files.
|
||||
respect_gitignore: false,
|
||||
skip_patterns,
|
||||
};
|
||||
|
||||
|
||||
@@ -973,7 +973,7 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&repo_path);
|
||||
}
|
||||
|
||||
// ─── Standalone mode tests ───────────────────────────────────────────
|
||||
// Standalone mode tests
|
||||
|
||||
#[test]
|
||||
fn test_standalone_worktree_simple() {
|
||||
@@ -1218,7 +1218,7 @@ mod tests {
|
||||
assert!(result.ignored_copy.is_some());
|
||||
}
|
||||
|
||||
// ─── Cancellation / partial-creation cleanup tests ───────────────────
|
||||
// Cancellation / partial-creation cleanup tests
|
||||
|
||||
#[test]
|
||||
fn test_linked_cancel_after_worktree_add_deregisters() {
|
||||
@@ -1307,7 +1307,8 @@ mod tests {
|
||||
let result = WorktreeBuilder::new(repo_path.clone(), dest.clone())
|
||||
.creation_mode(CreationMode::Linked)
|
||||
.ignored_files_mode(IgnoredFilesMode::Copy {
|
||||
skip_patterns: vec!["[".to_string()], // invalid glob → build fails
|
||||
// invalid glob → build fails
|
||||
skip_patterns: vec!["[".to_string()],
|
||||
})
|
||||
.create();
|
||||
assert!(result.is_err(), "invalid skip glob must fail creation");
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
//! Worktree execution planning.
|
||||
//!
|
||||
//! `WorktreePlan` makes the worktree creation pipeline explicit and testable.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
@@ -9,9 +7,10 @@ use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{BtrfsDelegate, CreationMode, IgnoredFilesMode, WorkingTreeMode};
|
||||
|
||||
// Debug cannot be derived: `Arc<dyn BtrfsDelegate>` is not Debug, so there is a
|
||||
// hand-written impl below.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct WorktreePlan {
|
||||
// Note: manual Debug impl below (Arc<dyn BtrfsDelegate> isn't Debug)
|
||||
pub source: PathBuf,
|
||||
pub dest: PathBuf,
|
||||
pub git_ref: String,
|
||||
@@ -20,13 +19,12 @@ pub(crate) struct WorktreePlan {
|
||||
pub working_tree: WorkingTreeMode,
|
||||
pub ignored_files: IgnoredFilesMode,
|
||||
pub ignored_parallelism: usize,
|
||||
/// Strategy for worktree creation (linked, standalone, or git checkout).
|
||||
pub creation_mode: CreationMode,
|
||||
/// Cancellation token for aborting file copy mid-flight.
|
||||
/// Aborts the file copy mid-flight.
|
||||
pub cancellation_token: CancellationToken,
|
||||
/// Optional delegate for privileged btrfs operations (used when the caller
|
||||
/// lacks CAP_SYS_ADMIN, e.g., inside a bwrap sandbox).
|
||||
/// Only read on Linux (in `try_btrfs_delegate`).
|
||||
/// Performs privileged btrfs operations when the caller lacks
|
||||
/// CAP_SYS_ADMIN, e.g. inside a bwrap sandbox. Only read on Linux, in
|
||||
/// `try_btrfs_delegate`, hence the `dead_code` allowance elsewhere.
|
||||
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
pub btrfs_delegate: Option<Arc<dyn BtrfsDelegate>>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user