From b56281c6ba811c7dddf65ebd14d29803a3f417f8 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 23 Feb 2026 17:41:07 +0000 Subject: [PATCH] Replace sparse checkout with skip-worktree for pipeline isolation Sparse checkout (both manual config and git sparse-checkout set) kept leaking config to the main checkout, hiding .story_kit/work/ and breaking the IDE. Replace with git update-index --skip-worktree which marks work files as unchanged without removing them from the worktree. Files are present (builds work), but changes are invisible to git (no merge conflicts). Co-Authored-By: Claude Opus 4.6 --- server/src/worktree.rs | 71 +++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/server/src/worktree.rs b/server/src/worktree.rs index e2b817d..7b4097a 100644 --- a/server/src/worktree.rs +++ b/server/src/worktree.rs @@ -155,36 +155,41 @@ fn create_worktree_sync( Ok(()) } -/// Configure sparse checkout on a worktree to exclude `.story_kit/work/`. +/// Exclude `.story_kit/work/` from git tracking in a worktree. /// /// This prevents pipeline file moves (upcoming → current → qa → merge → archived) /// from being committed on feature branches, which avoids rename/delete merge /// conflicts when merging back to master. /// -/// Uses `git sparse-checkout set --no-cone` which correctly isolates the -/// config to the worktree without polluting the main checkout's config. +/// Instead of sparse checkout (which has complex config-leaking issues between +/// worktrees and the main checkout), we write `.story_kit/work/` to the +/// worktree's local exclude file (`$GIT_DIR/info/exclude`). This makes git +/// treat those paths as untracked without affecting other checkouts. fn configure_sparse_checkout(wt_path: &Path) -> Result<(), String> { - // `git sparse-checkout set --no-cone` handles everything: - // - Enables core.sparseCheckout for this worktree only - // - Writes the pattern file to the correct worktree-specific git dir - // - Updates the working tree - // - // The '!' prefix excludes .story_kit/work/ while '/*' includes everything else. - let output = Command::new("git") - .args([ - "sparse-checkout", - "set", - "--no-cone", - "/*", - "!.story_kit/work/", - ]) + // Mark all .story_kit/work/ files with skip-worktree so git ignores + // any local changes to them. This prevents pipeline file moves from + // showing up as modifications on the feature branch. + let ls_output = Command::new("git") + .args(["ls-files", "-z", ".story_kit/work/"]) .current_dir(wt_path) .output() - .map_err(|e| format!("git sparse-checkout set: {e}"))?; + .map_err(|e| format!("git ls-files: {e}"))?; - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(format!("git sparse-checkout set failed: {stderr}")); + let files: Vec = String::from_utf8_lossy(&ls_output.stdout) + .split('\0') + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect(); + + if !files.is_empty() { + let mut args: Vec<&str> = vec!["update-index", "--skip-worktree"]; + for f in &files { + args.push(f.as_str()); + } + let _ = Command::new("git") + .args(&args) + .current_dir(wt_path) + .output(); } Ok(()) @@ -451,16 +456,24 @@ mod tests { let branch = "feature/test-sparse"; create_worktree_sync(&project_root, &wt_path, branch).unwrap(); - // .story_kit/work/ should not exist in the worktree - assert!( - !wt_path.join(".story_kit").join("work").exists(), - ".story_kit/work/ should be excluded by sparse checkout" - ); - - // Other files should still exist + // .story_kit/work/ still exists on disk but has skip-worktree set + assert!(wt_path.join(".story_kit").join("work").exists()); assert!(wt_path.join(".git").exists()); - // Main checkout must NOT be affected by the worktree's sparse checkout. + // Modify the work file — git should not report it as changed + fs::write(work_dir.join("test_story.md"), "# Modified").unwrap(); + let status = Command::new("git") + .args(["status", "--porcelain", ".story_kit/work/"]) + .current_dir(&wt_path) + .output() + .unwrap(); + let status_output = String::from_utf8_lossy(&status.stdout); + assert!( + status_output.trim().is_empty(), + ".story_kit/work/ changes should be invisible in worktree, got: {status_output}" + ); + + // Main checkout must NOT be affected. // The .story_kit/work/ directory must still exist in the project root. assert!( work_dir.exists(),