mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-10 03:13:29 +08:00
feat: add ecc2 worktree pruning command
This commit is contained in:
@@ -203,6 +203,12 @@ enum Commands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
check: bool,
|
check: bool,
|
||||||
},
|
},
|
||||||
|
/// Prune worktrees for inactive sessions and report any active sessions still holding one
|
||||||
|
PruneWorktrees {
|
||||||
|
/// Emit machine-readable JSON instead of the human summary
|
||||||
|
#[arg(long)]
|
||||||
|
json: bool,
|
||||||
|
},
|
||||||
/// Stop a running session
|
/// Stop a running session
|
||||||
Stop {
|
Stop {
|
||||||
/// Session ID or alias
|
/// Session ID or alias
|
||||||
@@ -684,6 +690,14 @@ async fn main() -> Result<()> {
|
|||||||
std::process::exit(worktree_status_reports_exit_code(&reports));
|
std::process::exit(worktree_status_reports_exit_code(&reports));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(Commands::PruneWorktrees { json }) => {
|
||||||
|
let outcome = session::manager::prune_inactive_worktrees(&db).await?;
|
||||||
|
if json {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&outcome)?);
|
||||||
|
} else {
|
||||||
|
println!("{}", format_prune_worktrees_human(&outcome));
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(Commands::Stop { session_id }) => {
|
Some(Commands::Stop { session_id }) => {
|
||||||
session::manager::stop_session(&db, &session_id).await?;
|
session::manager::stop_session(&db, &session_id).await?;
|
||||||
println!("Session stopped: {session_id}");
|
println!("Session stopped: {session_id}");
|
||||||
@@ -1051,6 +1065,36 @@ fn worktree_status_reports_exit_code(reports: &[WorktreeStatusReport]) -> i32 {
|
|||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_prune_worktrees_human(outcome: &session::manager::WorktreePruneOutcome) -> String {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
|
||||||
|
if outcome.cleaned_session_ids.is_empty() {
|
||||||
|
lines.push("Pruned 0 inactive worktree(s)".to_string());
|
||||||
|
} else {
|
||||||
|
lines.push(format!(
|
||||||
|
"Pruned {} inactive worktree(s)",
|
||||||
|
outcome.cleaned_session_ids.len()
|
||||||
|
));
|
||||||
|
for session_id in &outcome.cleaned_session_ids {
|
||||||
|
lines.push(format!("- cleaned {}", short_session(session_id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if outcome.active_with_worktree_ids.is_empty() {
|
||||||
|
lines.push("No active sessions are holding worktrees".to_string());
|
||||||
|
} else {
|
||||||
|
lines.push(format!(
|
||||||
|
"Skipped {} active session(s) still holding worktrees",
|
||||||
|
outcome.active_with_worktree_ids.len()
|
||||||
|
));
|
||||||
|
for session_id in &outcome.active_with_worktree_ids {
|
||||||
|
lines.push(format!("- active {}", short_session(session_id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
fn summarize_coordinate_backlog(
|
fn summarize_coordinate_backlog(
|
||||||
outcome: &session::manager::CoordinateBacklogOutcome,
|
outcome: &session::manager::CoordinateBacklogOutcome,
|
||||||
) -> CoordinateBacklogPassSummary {
|
) -> CoordinateBacklogPassSummary {
|
||||||
@@ -1455,6 +1499,19 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_parses_prune_worktrees_json_flag() {
|
||||||
|
let cli = Cli::try_parse_from(["ecc", "prune-worktrees", "--json"])
|
||||||
|
.expect("prune-worktrees --json should parse");
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Some(Commands::PruneWorktrees { json }) => {
|
||||||
|
assert!(json);
|
||||||
|
}
|
||||||
|
_ => panic!("expected prune-worktrees subcommand"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_worktree_status_human_includes_readiness_and_conflicts() {
|
fn format_worktree_status_human_includes_readiness_and_conflicts() {
|
||||||
let report = WorktreeStatusReport {
|
let report = WorktreeStatusReport {
|
||||||
@@ -1489,6 +1546,19 @@ mod tests {
|
|||||||
assert!(text.contains("--- Branch diff vs main ---"));
|
assert!(text.contains("--- Branch diff vs main ---"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_prune_worktrees_human_reports_cleaned_and_active_sessions() {
|
||||||
|
let text = format_prune_worktrees_human(&session::manager::WorktreePruneOutcome {
|
||||||
|
cleaned_session_ids: vec!["deadbeefcafefeed".to_string()],
|
||||||
|
active_with_worktree_ids: vec!["facefeed12345678".to_string()],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(text.contains("Pruned 1 inactive worktree(s)"));
|
||||||
|
assert!(text.contains("- cleaned deadbeef"));
|
||||||
|
assert!(text.contains("Skipped 1 active session(s) still holding worktrees"));
|
||||||
|
assert!(text.contains("- active facefeed"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_worktree_status_human_handles_missing_worktree() {
|
fn format_worktree_status_human_handles_missing_worktree() {
|
||||||
let report = WorktreeStatusReport {
|
let report = WorktreeStatusReport {
|
||||||
|
|||||||
@@ -610,6 +610,40 @@ pub async fn cleanup_session_worktree(db: &StateStore, id: &str) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
pub struct WorktreePruneOutcome {
|
||||||
|
pub cleaned_session_ids: Vec<String>,
|
||||||
|
pub active_with_worktree_ids: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn prune_inactive_worktrees(db: &StateStore) -> Result<WorktreePruneOutcome> {
|
||||||
|
let sessions = db.list_sessions()?;
|
||||||
|
let mut cleaned_session_ids = Vec::new();
|
||||||
|
let mut active_with_worktree_ids = Vec::new();
|
||||||
|
|
||||||
|
for session in sessions {
|
||||||
|
let Some(_) = session.worktree.as_ref() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
session.state,
|
||||||
|
SessionState::Pending | SessionState::Running | SessionState::Idle
|
||||||
|
) {
|
||||||
|
active_with_worktree_ids.push(session.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_session_worktree(db, &session.id).await?;
|
||||||
|
cleaned_session_ids.push(session.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(WorktreePruneOutcome {
|
||||||
|
cleaned_session_ids,
|
||||||
|
active_with_worktree_ids,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn delete_session(db: &StateStore, id: &str) -> Result<()> {
|
pub async fn delete_session(db: &StateStore, id: &str) -> Result<()> {
|
||||||
let session = resolve_session(db, id)?;
|
let session = resolve_session(db, id)?;
|
||||||
|
|
||||||
@@ -1745,6 +1779,83 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn prune_inactive_worktrees_cleans_stopped_sessions_only() -> Result<()> {
|
||||||
|
let tempdir = TestDir::new("manager-prune-worktrees")?;
|
||||||
|
let repo_root = tempdir.path().join("repo");
|
||||||
|
init_git_repo(&repo_root)?;
|
||||||
|
|
||||||
|
let cfg = build_config(tempdir.path());
|
||||||
|
let db = StateStore::open(&cfg.db_path)?;
|
||||||
|
let (fake_claude, _) = write_fake_claude(tempdir.path())?;
|
||||||
|
|
||||||
|
let active_id = create_session_in_dir(
|
||||||
|
&db,
|
||||||
|
&cfg,
|
||||||
|
"active worktree",
|
||||||
|
"claude",
|
||||||
|
true,
|
||||||
|
&repo_root,
|
||||||
|
&fake_claude,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let stopped_id = create_session_in_dir(
|
||||||
|
&db,
|
||||||
|
&cfg,
|
||||||
|
"stopped worktree",
|
||||||
|
"claude",
|
||||||
|
true,
|
||||||
|
&repo_root,
|
||||||
|
&fake_claude,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
stop_session_with_options(&db, &stopped_id, false).await?;
|
||||||
|
|
||||||
|
let active_before = db
|
||||||
|
.get_session(&active_id)?
|
||||||
|
.context("active session should exist")?;
|
||||||
|
let active_path = active_before
|
||||||
|
.worktree
|
||||||
|
.clone()
|
||||||
|
.context("active session worktree missing")?
|
||||||
|
.path;
|
||||||
|
|
||||||
|
let stopped_before = db
|
||||||
|
.get_session(&stopped_id)?
|
||||||
|
.context("stopped session should exist")?;
|
||||||
|
let stopped_path = stopped_before
|
||||||
|
.worktree
|
||||||
|
.clone()
|
||||||
|
.context("stopped session worktree missing")?
|
||||||
|
.path;
|
||||||
|
|
||||||
|
let outcome = prune_inactive_worktrees(&db).await?;
|
||||||
|
|
||||||
|
assert_eq!(outcome.cleaned_session_ids, vec![stopped_id.clone()]);
|
||||||
|
assert_eq!(outcome.active_with_worktree_ids, vec![active_id.clone()]);
|
||||||
|
assert!(active_path.exists(), "active worktree should remain");
|
||||||
|
assert!(!stopped_path.exists(), "stopped worktree should be removed");
|
||||||
|
|
||||||
|
let active_after = db
|
||||||
|
.get_session(&active_id)?
|
||||||
|
.context("active session should still exist")?;
|
||||||
|
assert!(
|
||||||
|
active_after.worktree.is_some(),
|
||||||
|
"active session should keep worktree metadata"
|
||||||
|
);
|
||||||
|
|
||||||
|
let stopped_after = db
|
||||||
|
.get_session(&stopped_id)?
|
||||||
|
.context("stopped session should still exist")?;
|
||||||
|
assert!(
|
||||||
|
stopped_after.worktree.is_none(),
|
||||||
|
"stopped session worktree metadata should be cleared"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn delete_session_removes_inactive_session_and_worktree() -> Result<()> {
|
async fn delete_session_removes_inactive_session_and_worktree() -> Result<()> {
|
||||||
let tempdir = TestDir::new("manager-delete-session")?;
|
let tempdir = TestDir::new("manager-delete-session")?;
|
||||||
|
|||||||
Reference in New Issue
Block a user