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 <noreply@anthropic.com>
This commit is contained in:
@@ -155,36 +155,41 @@ fn create_worktree_sync(
|
|||||||
Ok(())
|
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)
|
/// This prevents pipeline file moves (upcoming → current → qa → merge → archived)
|
||||||
/// from being committed on feature branches, which avoids rename/delete merge
|
/// from being committed on feature branches, which avoids rename/delete merge
|
||||||
/// conflicts when merging back to master.
|
/// conflicts when merging back to master.
|
||||||
///
|
///
|
||||||
/// Uses `git sparse-checkout set --no-cone` which correctly isolates the
|
/// Instead of sparse checkout (which has complex config-leaking issues between
|
||||||
/// config to the worktree without polluting the main checkout's config.
|
/// 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> {
|
fn configure_sparse_checkout(wt_path: &Path) -> Result<(), String> {
|
||||||
// `git sparse-checkout set --no-cone` handles everything:
|
// Mark all .story_kit/work/ files with skip-worktree so git ignores
|
||||||
// - Enables core.sparseCheckout for this worktree only
|
// any local changes to them. This prevents pipeline file moves from
|
||||||
// - Writes the pattern file to the correct worktree-specific git dir
|
// showing up as modifications on the feature branch.
|
||||||
// - Updates the working tree
|
let ls_output = Command::new("git")
|
||||||
//
|
.args(["ls-files", "-z", ".story_kit/work/"])
|
||||||
// The '!' prefix excludes .story_kit/work/ while '/*' includes everything else.
|
|
||||||
let output = Command::new("git")
|
|
||||||
.args([
|
|
||||||
"sparse-checkout",
|
|
||||||
"set",
|
|
||||||
"--no-cone",
|
|
||||||
"/*",
|
|
||||||
"!.story_kit/work/",
|
|
||||||
])
|
|
||||||
.current_dir(wt_path)
|
.current_dir(wt_path)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| format!("git sparse-checkout set: {e}"))?;
|
.map_err(|e| format!("git ls-files: {e}"))?;
|
||||||
|
|
||||||
if !output.status.success() {
|
let files: Vec<String> = String::from_utf8_lossy(&ls_output.stdout)
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
.split('\0')
|
||||||
return Err(format!("git sparse-checkout set failed: {stderr}"));
|
.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(())
|
Ok(())
|
||||||
@@ -451,16 +456,24 @@ mod tests {
|
|||||||
let branch = "feature/test-sparse";
|
let branch = "feature/test-sparse";
|
||||||
create_worktree_sync(&project_root, &wt_path, branch).unwrap();
|
create_worktree_sync(&project_root, &wt_path, branch).unwrap();
|
||||||
|
|
||||||
// .story_kit/work/ should not exist in the worktree
|
// .story_kit/work/ still exists on disk but has skip-worktree set
|
||||||
assert!(
|
assert!(wt_path.join(".story_kit").join("work").exists());
|
||||||
!wt_path.join(".story_kit").join("work").exists(),
|
|
||||||
".story_kit/work/ should be excluded by sparse checkout"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Other files should still exist
|
|
||||||
assert!(wt_path.join(".git").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.
|
// The .story_kit/work/ directory must still exist in the project root.
|
||||||
assert!(
|
assert!(
|
||||||
work_dir.exists(),
|
work_dir.exists(),
|
||||||
|
|||||||
Reference in New Issue
Block a user