huskies: merge 1199 story Orphaned build-dir GC: reclaim dead worktree targets without touching warm caches
This commit is contained in:
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
mod squash;
|
||||
|
||||
pub(crate) use squash::run_squash_merge;
|
||||
pub(crate) use squash::{merge_lock_is_free, run_squash_merge};
|
||||
|
||||
/// Typed outcome of a completed squash-merge operation.
|
||||
///
|
||||
|
||||
@@ -17,6 +17,19 @@ use crate::config::ProjectConfig;
|
||||
/// causing `git cherry-pick merge-queue/…` to fail with "bad revision".
|
||||
static MERGE_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Returns `true` when no squash-merge is currently running, i.e. the merge
|
||||
/// lock is free.
|
||||
///
|
||||
/// Used by the build-directory GC pass (story 1199) to decide whether
|
||||
/// `.huskies/merge_workspace` is safe to reclaim. This is a best-effort,
|
||||
/// momentary check — the lock is not held across the reclaim itself, so a
|
||||
/// merge that starts immediately afterward can still race with GC. That's
|
||||
/// acceptable: the GC pass tolerates races and skips on error rather than
|
||||
/// failing the whole pass.
|
||||
pub(crate) fn merge_lock_is_free() -> bool {
|
||||
MERGE_LOCK.try_lock().is_ok()
|
||||
}
|
||||
|
||||
/// Resolve the base branch for `project_root` from config, or auto-detect it.
|
||||
fn resolve_base_branch(project_root: &Path) -> String {
|
||||
let configured = crate::config::ProjectConfig::load(project_root)
|
||||
|
||||
@@ -157,6 +157,12 @@ pub(crate) async fn on_coding_transition(project_root: &Path, port: u16, story_i
|
||||
}
|
||||
|
||||
/// Remove the worktree and feature branch for `story_id` after it reaches a terminal stage.
|
||||
///
|
||||
/// Story 1199: bytes-reclaimed accounting for the worktree's `target/` dir
|
||||
/// happens one layer down, in `remove_worktree` — the single choke point
|
||||
/// shared by this subscriber, the `remove_worktree` MCP tool, and
|
||||
/// `worktree::cleanup`/`sweep`. Logging it there (once) avoids a duplicate
|
||||
/// log line here.
|
||||
pub(crate) async fn on_terminal_transition(project_root: &Path, story_id: &str) {
|
||||
let config = match crate::config::ProjectConfig::load(project_root) {
|
||||
Ok(c) => c,
|
||||
@@ -270,6 +276,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Story 1199 AC4: on_terminal_transition reclaims the worktree's target/
|
||||
/// dir (and everything else in the worktree) when removing it.
|
||||
#[tokio::test]
|
||||
async fn terminal_transition_reclaims_target_dir() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let root = setup_project(&tmp);
|
||||
let story_id = "1006_test_reclaim_target";
|
||||
|
||||
on_coding_transition(&root, 3001, story_id).await;
|
||||
let wt_path = crate::worktree::worktree_path(&root, story_id);
|
||||
let target_dir = wt_path.join("target");
|
||||
fs::create_dir_all(&target_dir).unwrap();
|
||||
fs::write(target_dir.join("build_artifact.bin"), vec![0u8; 512]).unwrap();
|
||||
assert!(target_dir.exists(), "target/ must exist before cleanup");
|
||||
|
||||
on_terminal_transition(&root, story_id).await;
|
||||
|
||||
assert!(
|
||||
!target_dir.exists(),
|
||||
"target/ dir must be reclaimed when the worktree is removed"
|
||||
);
|
||||
assert!(!wt_path.exists());
|
||||
}
|
||||
|
||||
/// AC2: on_terminal_transition is a no-op (non-fatal) when no worktree exists.
|
||||
#[tokio::test]
|
||||
async fn terminal_transition_noop_when_no_worktree() {
|
||||
|
||||
Reference in New Issue
Block a user