huskies: merge 784

This commit is contained in:
dave
2026-04-28 14:01:24 +00:00
parent cf470f5048
commit 3772c0d03c
5 changed files with 1060 additions and 1170 deletions
+159
View File
@@ -0,0 +1,159 @@
//! Async worktree removal operations.
use crate::config::ProjectConfig;
use std::path::Path;
use super::create::run_teardown_commands;
use super::git::{branch_name, detect_base_branch, remove_worktree_sync};
use super::{WorktreeInfo, worktree_path};
/// Remove a git worktree and its branch.
pub async fn remove_worktree(
project_root: &Path,
info: &WorktreeInfo,
config: &ProjectConfig,
) -> Result<(), String> {
run_teardown_commands(&info.path, config).await?;
let root = project_root.to_path_buf();
let wt_path = info.path.clone();
let branch = info.branch.clone();
tokio::task::spawn_blocking(move || remove_worktree_sync(&root, &wt_path, &branch))
.await
.map_err(|e| format!("spawn_blocking: {e}"))?
}
/// Remove a git worktree by story ID, deriving the path and branch deterministically.
pub async fn remove_worktree_by_story_id(
project_root: &Path,
story_id: &str,
config: &ProjectConfig,
) -> Result<(), String> {
let path = worktree_path(project_root, story_id);
if !path.exists() {
return Err(format!("Worktree not found for story: {story_id}"));
}
let branch = branch_name(story_id);
let base_branch = config
.base_branch
.clone()
.unwrap_or_else(|| detect_base_branch(project_root));
let info = WorktreeInfo {
path,
branch,
base_branch,
};
remove_worktree(project_root, &info, config).await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::WatcherConfig;
use std::fs;
use std::process::Command;
use tempfile::TempDir;
fn init_git_repo(dir: &std::path::Path) {
Command::new("git")
.args(["init"])
.current_dir(dir)
.output()
.expect("git init");
Command::new("git")
.args(["commit", "--allow-empty", "-m", "init"])
.current_dir(dir)
.output()
.expect("git commit");
}
fn empty_config() -> ProjectConfig {
ProjectConfig {
component: vec![],
agent: vec![],
watcher: WatcherConfig::default(),
default_qa: "server".to_string(),
default_coder_model: None,
max_coders: None,
max_retries: 2,
base_branch: None,
rate_limit_notifications: true,
web_ui_status_consumer: true,
matrix_status_consumer: true,
slack_status_consumer: true,
discord_status_consumer: true,
whatsapp_status_consumer: true,
timezone: None,
rendezvous: None,
trusted_keys: Vec::new(),
crdt_require_token: false,
crdt_tokens: Vec::new(),
max_mesh_peers: 3,
gateway_url: None,
gateway_project: None,
}
}
#[tokio::test]
async fn remove_worktree_by_story_id_returns_err_when_not_found() {
let tmp = TempDir::new().unwrap();
let result =
remove_worktree_by_story_id(tmp.path(), "99_nonexistent", &empty_config()).await;
assert!(result.is_err());
assert!(
result
.unwrap_err()
.contains("Worktree not found for story: 99_nonexistent")
);
}
#[tokio::test]
async fn remove_worktree_by_story_id_removes_existing_worktree() {
let tmp = TempDir::new().unwrap();
let project_root = tmp.path().join("my-project");
fs::create_dir_all(&project_root).unwrap();
init_git_repo(&project_root);
super::super::create::create_worktree(
&project_root,
"88_remove_by_id",
&empty_config(),
3001,
)
.await
.unwrap();
let result =
remove_worktree_by_story_id(&project_root, "88_remove_by_id", &empty_config()).await;
assert!(
result.is_ok(),
"Expected removal to succeed: {:?}",
result.err()
);
}
#[tokio::test]
async fn remove_worktree_async_removes_directory() {
let tmp = TempDir::new().unwrap();
let project_root = tmp.path().join("my-project");
fs::create_dir_all(&project_root).unwrap();
init_git_repo(&project_root);
let info = super::super::create::create_worktree(
&project_root,
"77_remove_async",
&empty_config(),
3001,
)
.await
.unwrap();
let path = info.path.clone();
assert!(path.exists());
remove_worktree(&project_root, &info, &empty_config())
.await
.unwrap();
assert!(!path.exists());
}
}