mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-11 03:43:30 +08:00
feat(ecc2): enforce configurable worktree branch prefixes
This commit is contained in:
@@ -33,6 +33,7 @@ pub struct BudgetAlertThresholds {
|
|||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub db_path: PathBuf,
|
pub db_path: PathBuf,
|
||||||
pub worktree_root: PathBuf,
|
pub worktree_root: PathBuf,
|
||||||
|
pub worktree_branch_prefix: String,
|
||||||
pub max_parallel_sessions: usize,
|
pub max_parallel_sessions: usize,
|
||||||
pub max_parallel_worktrees: usize,
|
pub max_parallel_worktrees: usize,
|
||||||
pub session_timeout_secs: u64,
|
pub session_timeout_secs: u64,
|
||||||
@@ -88,6 +89,7 @@ impl Default for Config {
|
|||||||
Self {
|
Self {
|
||||||
db_path: home.join(".claude").join("ecc2.db"),
|
db_path: home.join(".claude").join("ecc2.db"),
|
||||||
worktree_root: PathBuf::from("/tmp/ecc-worktrees"),
|
worktree_root: PathBuf::from("/tmp/ecc-worktrees"),
|
||||||
|
worktree_branch_prefix: "ecc".to_string(),
|
||||||
max_parallel_sessions: 8,
|
max_parallel_sessions: 8,
|
||||||
max_parallel_worktrees: 6,
|
max_parallel_worktrees: 6,
|
||||||
session_timeout_secs: 3600,
|
session_timeout_secs: 3600,
|
||||||
@@ -350,6 +352,10 @@ theme = "Dark"
|
|||||||
let config: Config = toml::from_str(legacy_config).unwrap();
|
let config: Config = toml::from_str(legacy_config).unwrap();
|
||||||
let defaults = Config::default();
|
let defaults = Config::default();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.worktree_branch_prefix,
|
||||||
|
defaults.worktree_branch_prefix
|
||||||
|
);
|
||||||
assert_eq!(config.cost_budget_usd, defaults.cost_budget_usd);
|
assert_eq!(config.cost_budget_usd, defaults.cost_budget_usd);
|
||||||
assert_eq!(config.token_budget, defaults.token_budget);
|
assert_eq!(config.token_budget, defaults.token_budget);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -406,6 +412,13 @@ theme = "Dark"
|
|||||||
assert_eq!(config.pane_layout, PaneLayout::Grid);
|
assert_eq!(config.pane_layout, PaneLayout::Grid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn worktree_branch_prefix_deserializes_from_toml() {
|
||||||
|
let config: Config = toml::from_str(r#"worktree_branch_prefix = "bots/ecc""#).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(config.worktree_branch_prefix, "bots/ecc");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pane_navigation_deserializes_from_toml() {
|
fn pane_navigation_deserializes_from_toml() {
|
||||||
let config: Config = toml::from_str(
|
let config: Config = toml::from_str(
|
||||||
@@ -535,6 +548,7 @@ critical = 1.10
|
|||||||
config.auto_dispatch_limit_per_session = 9;
|
config.auto_dispatch_limit_per_session = 9;
|
||||||
config.auto_create_worktrees = false;
|
config.auto_create_worktrees = false;
|
||||||
config.auto_merge_ready_worktrees = true;
|
config.auto_merge_ready_worktrees = true;
|
||||||
|
config.worktree_branch_prefix = "bots/ecc".to_string();
|
||||||
config.budget_alert_thresholds = BudgetAlertThresholds {
|
config.budget_alert_thresholds = BudgetAlertThresholds {
|
||||||
advisory: 0.45,
|
advisory: 0.45,
|
||||||
warning: 0.70,
|
warning: 0.70,
|
||||||
@@ -553,6 +567,7 @@ critical = 1.10
|
|||||||
assert_eq!(loaded.auto_dispatch_limit_per_session, 9);
|
assert_eq!(loaded.auto_dispatch_limit_per_session, 9);
|
||||||
assert!(!loaded.auto_create_worktrees);
|
assert!(!loaded.auto_create_worktrees);
|
||||||
assert!(loaded.auto_merge_ready_worktrees);
|
assert!(loaded.auto_merge_ready_worktrees);
|
||||||
|
assert_eq!(loaded.worktree_branch_prefix, "bots/ecc");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded.budget_alert_thresholds,
|
loaded.budget_alert_thresholds,
|
||||||
BudgetAlertThresholds {
|
BudgetAlertThresholds {
|
||||||
|
|||||||
@@ -1791,6 +1791,7 @@ mod tests {
|
|||||||
Config {
|
Config {
|
||||||
db_path: root.join("state.db"),
|
db_path: root.join("state.db"),
|
||||||
worktree_root: root.join("worktrees"),
|
worktree_root: root.join("worktrees"),
|
||||||
|
worktree_branch_prefix: "ecc".to_string(),
|
||||||
max_parallel_sessions: 4,
|
max_parallel_sessions: 4,
|
||||||
max_parallel_worktrees: 4,
|
max_parallel_worktrees: 4,
|
||||||
session_timeout_secs: 60,
|
session_timeout_secs: 60,
|
||||||
|
|||||||
@@ -9604,6 +9604,7 @@ diff --git a/src/next.rs b/src/next.rs
|
|||||||
Config {
|
Config {
|
||||||
db_path: root.join("state.db"),
|
db_path: root.join("state.db"),
|
||||||
worktree_root: root.join("worktrees"),
|
worktree_root: root.join("worktrees"),
|
||||||
|
worktree_branch_prefix: "ecc".to_string(),
|
||||||
max_parallel_sessions: 4,
|
max_parallel_sessions: 4,
|
||||||
max_parallel_worktrees: 4,
|
max_parallel_worktrees: 4,
|
||||||
session_timeout_secs: 60,
|
session_timeout_secs: 60,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ pub(crate) fn create_for_session_in_repo(
|
|||||||
cfg: &Config,
|
cfg: &Config,
|
||||||
repo_root: &Path,
|
repo_root: &Path,
|
||||||
) -> Result<WorktreeInfo> {
|
) -> Result<WorktreeInfo> {
|
||||||
let branch = format!("ecc/{session_id}");
|
let branch = branch_name_for_session(session_id, cfg, repo_root)?;
|
||||||
let path = cfg.worktree_root.join(session_id);
|
let path = cfg.worktree_root.join(session_id);
|
||||||
|
|
||||||
// Get current branch as base
|
// Get current branch as base
|
||||||
@@ -80,6 +80,27 @@ pub(crate) fn create_for_session_in_repo(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn branch_name_for_session(
|
||||||
|
session_id: &str,
|
||||||
|
cfg: &Config,
|
||||||
|
repo_root: &Path,
|
||||||
|
) -> Result<String> {
|
||||||
|
let prefix = cfg.worktree_branch_prefix.trim().trim_matches('/');
|
||||||
|
if prefix.is_empty() {
|
||||||
|
anyhow::bail!("worktree_branch_prefix cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
let branch = format!("{prefix}/{session_id}");
|
||||||
|
validate_branch_name(repo_root, &branch).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Invalid worktree branch '{branch}' derived from prefix '{}' and session id '{session_id}'",
|
||||||
|
cfg.worktree_branch_prefix
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(branch)
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove a worktree and its branch.
|
/// Remove a worktree and its branch.
|
||||||
pub fn remove(worktree: &WorktreeInfo) -> Result<()> {
|
pub fn remove(worktree: &WorktreeInfo) -> Result<()> {
|
||||||
let repo_root = match base_checkout_path(worktree) {
|
let repo_root = match base_checkout_path(worktree) {
|
||||||
@@ -461,6 +482,26 @@ fn git_status_short(worktree_path: &Path) -> Result<Vec<String>> {
|
|||||||
Ok(parse_nonempty_lines(&output.stdout))
|
Ok(parse_nonempty_lines(&output.stdout))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_branch_name(repo_root: &Path, branch: &str) -> Result<()> {
|
||||||
|
let output = Command::new("git")
|
||||||
|
.arg("-C")
|
||||||
|
.arg(repo_root)
|
||||||
|
.args(["check-ref-format", "--branch", branch])
|
||||||
|
.output()
|
||||||
|
.context("Failed to validate worktree branch name")?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
|
if stderr.is_empty() {
|
||||||
|
anyhow::bail!("branch name is not a valid git ref");
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("{stderr}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_nonempty_lines(stdout: &[u8]) -> Vec<String> {
|
fn parse_nonempty_lines(stdout: &[u8]) -> Vec<String> {
|
||||||
String::from_utf8_lossy(stdout)
|
String::from_utf8_lossy(stdout)
|
||||||
.lines()
|
.lines()
|
||||||
@@ -576,9 +617,7 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn init_repo(root: &Path) -> Result<PathBuf> {
|
||||||
fn diff_summary_reports_clean_and_dirty_worktrees() -> Result<()> {
|
|
||||||
let root = std::env::temp_dir().join(format!("ecc2-worktree-{}", Uuid::new_v4()));
|
|
||||||
let repo = root.join("repo");
|
let repo = root.join("repo");
|
||||||
fs::create_dir_all(&repo)?;
|
fs::create_dir_all(&repo)?;
|
||||||
|
|
||||||
@@ -589,6 +628,60 @@ mod tests {
|
|||||||
run_git(&repo, &["add", "README.md"])?;
|
run_git(&repo, &["add", "README.md"])?;
|
||||||
run_git(&repo, &["commit", "-m", "init"])?;
|
run_git(&repo, &["commit", "-m", "init"])?;
|
||||||
|
|
||||||
|
Ok(repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_for_session_uses_configured_branch_prefix() -> Result<()> {
|
||||||
|
let root = std::env::temp_dir().join(format!("ecc2-worktree-prefix-{}", Uuid::new_v4()));
|
||||||
|
let repo = init_repo(&root)?;
|
||||||
|
let mut cfg = Config::default();
|
||||||
|
cfg.worktree_root = root.join("worktrees");
|
||||||
|
cfg.worktree_branch_prefix = "bots/ecc".to_string();
|
||||||
|
|
||||||
|
let worktree = create_for_session_in_repo("worker-123", &cfg, &repo)?;
|
||||||
|
assert_eq!(worktree.branch, "bots/ecc/worker-123");
|
||||||
|
|
||||||
|
let branch = Command::new("git")
|
||||||
|
.arg("-C")
|
||||||
|
.arg(&repo)
|
||||||
|
.args(["rev-parse", "--abbrev-ref", "bots/ecc/worker-123"])
|
||||||
|
.output()?;
|
||||||
|
assert!(branch.status.success());
|
||||||
|
assert_eq!(
|
||||||
|
String::from_utf8_lossy(&branch.stdout).trim(),
|
||||||
|
"bots/ecc/worker-123"
|
||||||
|
);
|
||||||
|
|
||||||
|
remove(&worktree)?;
|
||||||
|
let _ = fs::remove_dir_all(root);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_for_session_rejects_invalid_branch_prefix() -> Result<()> {
|
||||||
|
let root =
|
||||||
|
std::env::temp_dir().join(format!("ecc2-worktree-invalid-prefix-{}", Uuid::new_v4()));
|
||||||
|
let repo = init_repo(&root)?;
|
||||||
|
let mut cfg = Config::default();
|
||||||
|
cfg.worktree_root = root.join("worktrees");
|
||||||
|
cfg.worktree_branch_prefix = "bad prefix".to_string();
|
||||||
|
|
||||||
|
let error = create_for_session_in_repo("worker-123", &cfg, &repo).unwrap_err();
|
||||||
|
let message = error.to_string();
|
||||||
|
assert!(message.contains("Invalid worktree branch"));
|
||||||
|
assert!(message.contains("bad prefix"));
|
||||||
|
assert!(!cfg.worktree_root.join("worker-123").exists());
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(root);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diff_summary_reports_clean_and_dirty_worktrees() -> Result<()> {
|
||||||
|
let root = std::env::temp_dir().join(format!("ecc2-worktree-{}", Uuid::new_v4()));
|
||||||
|
let repo = init_repo(&root)?;
|
||||||
|
|
||||||
let worktree_dir = root.join("wt-1");
|
let worktree_dir = root.join("wt-1");
|
||||||
run_git(
|
run_git(
|
||||||
&repo,
|
&repo,
|
||||||
@@ -631,15 +724,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn diff_file_preview_reports_branch_and_working_tree_files() -> Result<()> {
|
fn diff_file_preview_reports_branch_and_working_tree_files() -> Result<()> {
|
||||||
let root = std::env::temp_dir().join(format!("ecc2-worktree-preview-{}", Uuid::new_v4()));
|
let root = std::env::temp_dir().join(format!("ecc2-worktree-preview-{}", Uuid::new_v4()));
|
||||||
let repo = root.join("repo");
|
let repo = init_repo(&root)?;
|
||||||
fs::create_dir_all(&repo)?;
|
|
||||||
|
|
||||||
run_git(&repo, &["init", "-b", "main"])?;
|
|
||||||
run_git(&repo, &["config", "user.email", "ecc@example.com"])?;
|
|
||||||
run_git(&repo, &["config", "user.name", "ECC"])?;
|
|
||||||
fs::write(repo.join("README.md"), "hello\n")?;
|
|
||||||
run_git(&repo, &["add", "README.md"])?;
|
|
||||||
run_git(&repo, &["commit", "-m", "init"])?;
|
|
||||||
|
|
||||||
let worktree_dir = root.join("wt-1");
|
let worktree_dir = root.join("wt-1");
|
||||||
run_git(
|
run_git(
|
||||||
@@ -686,15 +771,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn diff_patch_preview_reports_branch_and_working_tree_sections() -> Result<()> {
|
fn diff_patch_preview_reports_branch_and_working_tree_sections() -> Result<()> {
|
||||||
let root = std::env::temp_dir().join(format!("ecc2-worktree-patch-{}", Uuid::new_v4()));
|
let root = std::env::temp_dir().join(format!("ecc2-worktree-patch-{}", Uuid::new_v4()));
|
||||||
let repo = root.join("repo");
|
let repo = init_repo(&root)?;
|
||||||
fs::create_dir_all(&repo)?;
|
|
||||||
|
|
||||||
run_git(&repo, &["init", "-b", "main"])?;
|
|
||||||
run_git(&repo, &["config", "user.email", "ecc@example.com"])?;
|
|
||||||
run_git(&repo, &["config", "user.name", "ECC"])?;
|
|
||||||
fs::write(repo.join("README.md"), "hello\n")?;
|
|
||||||
run_git(&repo, &["add", "README.md"])?;
|
|
||||||
run_git(&repo, &["commit", "-m", "init"])?;
|
|
||||||
|
|
||||||
let worktree_dir = root.join("wt-1");
|
let worktree_dir = root.join("wt-1");
|
||||||
run_git(
|
run_git(
|
||||||
@@ -740,15 +817,7 @@ mod tests {
|
|||||||
fn merge_readiness_reports_ready_worktree() -> Result<()> {
|
fn merge_readiness_reports_ready_worktree() -> Result<()> {
|
||||||
let root =
|
let root =
|
||||||
std::env::temp_dir().join(format!("ecc2-worktree-merge-ready-{}", Uuid::new_v4()));
|
std::env::temp_dir().join(format!("ecc2-worktree-merge-ready-{}", Uuid::new_v4()));
|
||||||
let repo = root.join("repo");
|
let repo = init_repo(&root)?;
|
||||||
fs::create_dir_all(&repo)?;
|
|
||||||
|
|
||||||
run_git(&repo, &["init", "-b", "main"])?;
|
|
||||||
run_git(&repo, &["config", "user.email", "ecc@example.com"])?;
|
|
||||||
run_git(&repo, &["config", "user.name", "ECC"])?;
|
|
||||||
fs::write(repo.join("README.md"), "hello\n")?;
|
|
||||||
run_git(&repo, &["add", "README.md"])?;
|
|
||||||
run_git(&repo, &["commit", "-m", "init"])?;
|
|
||||||
|
|
||||||
let worktree_dir = root.join("wt-1");
|
let worktree_dir = root.join("wt-1");
|
||||||
run_git(
|
run_git(
|
||||||
@@ -792,15 +861,7 @@ mod tests {
|
|||||||
fn merge_readiness_reports_conflicted_worktree() -> Result<()> {
|
fn merge_readiness_reports_conflicted_worktree() -> Result<()> {
|
||||||
let root =
|
let root =
|
||||||
std::env::temp_dir().join(format!("ecc2-worktree-merge-conflict-{}", Uuid::new_v4()));
|
std::env::temp_dir().join(format!("ecc2-worktree-merge-conflict-{}", Uuid::new_v4()));
|
||||||
let repo = root.join("repo");
|
let repo = init_repo(&root)?;
|
||||||
fs::create_dir_all(&repo)?;
|
|
||||||
|
|
||||||
run_git(&repo, &["init", "-b", "main"])?;
|
|
||||||
run_git(&repo, &["config", "user.email", "ecc@example.com"])?;
|
|
||||||
run_git(&repo, &["config", "user.name", "ECC"])?;
|
|
||||||
fs::write(repo.join("README.md"), "hello\n")?;
|
|
||||||
run_git(&repo, &["add", "README.md"])?;
|
|
||||||
run_git(&repo, &["commit", "-m", "init"])?;
|
|
||||||
|
|
||||||
let worktree_dir = root.join("wt-1");
|
let worktree_dir = root.join("wt-1");
|
||||||
run_git(
|
run_git(
|
||||||
|
|||||||
Reference in New Issue
Block a user