use crate::cli::output::print_success;
use crate::core::branch::BranchType;
use crate::core::config::Config;
use crate::core::port::{calculate_effective_ports, is_shared_service_port};
use crate::core::slot::allocate_lowest_slot;
use crate::infra::docker::generate_docker_compose_override;
use crate::infra::env_file::{
copy_and_merge_untracked_files, sanitize_compose_project_name, write_env_worktree,
};
use crate::infra::fs::link_shared_cache_directories;
use crate::infra::git::{resolve_git_common_dir, resolve_repo_name, resolve_toplevel};
use crate::infra::hook::run_post_start_hook;
use crate::infra::lock::SlotLock;
use crate::infra::registry::{SlotRecord, SlotRegistry};
use crate::infra::worktree::create_worktree;
use crate::workflow::error::WorkflowError;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
pub struct StartOptions<'a> {
pub branch_type: BranchType,
pub name: &'a str,
pub isolated: bool,
pub repo_root: Option<&'a Path>,
}
pub fn start_worktree(options: StartOptions<'_>) -> Result<u32, WorkflowError> {
let toplevel = resolve_toplevel(options.repo_root)?;
let common_dir = resolve_git_common_dir(Some(&toplevel))?;
let repo_name = resolve_repo_name(&toplevel);
let config_path = toplevel.join(".git-claw.toml");
let config = Config::load_or_default(&config_path, &repo_name)?;
let branch_name = options.branch_type.format_branch_name(options.name)?;
let raw_worktree_root = config.worktree_root(&repo_name);
let worktree_root = if Path::new(&raw_worktree_root).is_absolute() {
PathBuf::from(raw_worktree_root)
} else {
toplevel.join(raw_worktree_root)
};
let worktree_path = worktree_root.join(options.name);
let lock_path = common_dir.join("claw/slots.lock");
let registry_path = common_dir.join("claw/slots.json");
let _lock = SlotLock::acquire(&lock_path)?;
let mut registry = SlotRegistry::load_or_empty(®istry_path)?;
let _ = registry.auto_gc(®istry_path, Some(&toplevel), true);
if registry.find_by_name(options.name).is_some() {
return Err(WorkflowError::DuplicateSlotName(options.name.to_string()));
}
let slot_id = allocate_lowest_slot(registry.occupied_ids());
let shiftable_ports: BTreeMap<String, u16> = config
.ports
.iter()
.filter(|(k, _)| !is_shared_service_port(k, &config.docker.shared_services))
.map(|(k, &v)| (k.clone(), v))
.collect();
let effective_ports = calculate_effective_ports(&shiftable_ports, slot_id)?;
create_worktree(
&toplevel,
&worktree_path,
&branch_name,
config.main_branch(),
)?;
write_env_worktree(
&worktree_path,
&repo_name,
options.name,
slot_id,
&effective_ports,
)?;
let compose_project = sanitize_compose_project_name(&repo_name, options.name, slot_id);
let _ = copy_and_merge_untracked_files(
&toplevel,
&worktree_path,
&config.files.copy,
&shiftable_ports,
slot_id,
Some(&compose_project),
);
let _ = generate_docker_compose_override(&toplevel, &worktree_path, &config.docker);
let _ =
link_shared_cache_directories(&toplevel, &worktree_path, &config.cache, options.isolated);
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
registry.slots.push(SlotRecord {
id: slot_id,
name: options.name.to_string(),
branch_type: options.branch_type.to_string(),
branch: branch_name.clone(),
path: worktree_path.to_string_lossy().to_string(),
allocated_at: now.to_string(),
});
registry.save_atomic(®istry_path)?;
drop(_lock);
let mut hook_env = BTreeMap::new();
hook_env.insert("CLAW_SLOT_ID".to_string(), slot_id.to_string());
hook_env.insert("CLAW_WORKTREE_NAME".to_string(), options.name.to_string());
hook_env.insert(
"CLAW_WORKTREE_PATH".to_string(),
worktree_path.to_string_lossy().to_string(),
);
for (k, v) in &effective_ports {
hook_env.insert(k.clone(), v.to_string());
}
let _ = run_post_start_hook(
config.hooks.post_start.as_deref(),
&worktree_path,
&hook_env,
);
print_success(format!(
"Worktree '{}' started successfully (Slot: {}, Branch: {}, Path: {})",
options.name,
slot_id,
branch_name,
worktree_path.display()
));
Ok(slot_id)
}