huskies: merge 784
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
//! Git worktree management — creates, lists, and removes worktrees for agent isolation.
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
mod create;
|
||||
mod git;
|
||||
mod remove;
|
||||
|
||||
pub use create::create_worktree;
|
||||
pub use git::{migrate_slug_paths, prune_worktree_sync};
|
||||
pub use remove::remove_worktree_by_story_id;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorktreeInfo {
|
||||
pub path: PathBuf,
|
||||
pub branch: String,
|
||||
pub base_branch: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorktreeListEntry {
|
||||
pub story_id: String,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
/// Worktree path inside the project: `{project_root}/.huskies/worktrees/{story_id}`.
|
||||
pub fn worktree_path(project_root: &Path, story_id: &str) -> PathBuf {
|
||||
project_root
|
||||
.join(".huskies")
|
||||
.join("worktrees")
|
||||
.join(story_id)
|
||||
}
|
||||
|
||||
/// Write a `.mcp.json` file in the given directory pointing to the MCP server
|
||||
/// at the given port.
|
||||
pub fn write_mcp_json(dir: &Path, port: u16) -> Result<(), String> {
|
||||
let content = format!(
|
||||
"{{\n \"mcpServers\": {{\n \"huskies\": {{\n \"type\": \"http\",\n \"url\": \"http://localhost:{port}/mcp\"\n }}\n }}\n}}\n"
|
||||
);
|
||||
std::fs::write(dir.join(".mcp.json"), content).map_err(|e| format!("Write .mcp.json: {e}"))
|
||||
}
|
||||
|
||||
/// Find the worktree path for a given story ID, if it exists.
|
||||
pub fn find_worktree_path(project_root: &Path, story_id: &str) -> Option<PathBuf> {
|
||||
let wt_path = project_root
|
||||
.join(".huskies")
|
||||
.join("worktrees")
|
||||
.join(story_id);
|
||||
if wt_path.is_dir() {
|
||||
Some(wt_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// List all worktrees under `{project_root}/.huskies/worktrees/`.
|
||||
pub fn list_worktrees(project_root: &Path) -> Result<Vec<WorktreeListEntry>, String> {
|
||||
let worktrees_dir = project_root.join(".huskies").join("worktrees");
|
||||
if !worktrees_dir.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut entries = Vec::new();
|
||||
for entry in std::fs::read_dir(&worktrees_dir).map_err(|e| format!("list worktrees: {e}"))? {
|
||||
let entry = entry.map_err(|e| format!("list worktrees entry: {e}"))?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let story_id = path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
entries.push(WorktreeListEntry { story_id, path });
|
||||
}
|
||||
}
|
||||
entries.sort_by(|a, b| a.story_id.cmp(&b.story_id));
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn write_mcp_json_uses_given_port() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
write_mcp_json(tmp.path(), 4242).unwrap();
|
||||
let content = std::fs::read_to_string(tmp.path().join(".mcp.json")).unwrap();
|
||||
assert!(content.contains("http://localhost:4242/mcp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_mcp_json_default_port() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
write_mcp_json(tmp.path(), 3001).unwrap();
|
||||
let content = std::fs::read_to_string(tmp.path().join(".mcp.json")).unwrap();
|
||||
assert!(content.contains("http://localhost:3001/mcp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worktree_path_is_inside_project() {
|
||||
let project_root = Path::new("/home/user/my-project");
|
||||
let path = worktree_path(project_root, "42_my_story");
|
||||
assert_eq!(
|
||||
path,
|
||||
Path::new("/home/user/my-project/.huskies/worktrees/42_my_story")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_worktrees_empty_when_no_dir() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let entries = list_worktrees(tmp.path()).unwrap();
|
||||
assert!(entries.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_worktrees_returns_subdirs() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let worktrees_dir = tmp.path().join(".huskies").join("worktrees");
|
||||
fs::create_dir_all(worktrees_dir.join("42_story_a")).unwrap();
|
||||
fs::create_dir_all(worktrees_dir.join("43_story_b")).unwrap();
|
||||
// A file (not dir) — should be ignored
|
||||
fs::write(worktrees_dir.join("readme.txt"), "").unwrap();
|
||||
|
||||
let entries = list_worktrees(tmp.path()).unwrap();
|
||||
assert_eq!(entries.len(), 2);
|
||||
assert_eq!(entries[0].story_id, "42_story_a");
|
||||
assert_eq!(entries[1].story_id, "43_story_b");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user