git_claw/workflow/
start.rs1use crate::cli::output::print_success;
4use crate::core::branch::BranchType;
5use crate::core::config::Config;
6use crate::core::port::{calculate_effective_ports, is_shared_service_port};
7use crate::core::slot::allocate_lowest_slot;
8use crate::infra::docker::generate_docker_compose_override;
9use crate::infra::env_file::{
10 copy_and_merge_untracked_files, sanitize_compose_project_name, write_env_worktree,
11};
12use crate::infra::fs::link_shared_cache_directories;
13use crate::infra::git::{resolve_git_common_dir, resolve_repo_name, resolve_toplevel};
14use crate::infra::hook::run_post_start_hook;
15use crate::infra::lock::SlotLock;
16use crate::infra::registry::{SlotRecord, SlotRegistry};
17use crate::infra::worktree::create_worktree;
18use crate::workflow::error::WorkflowError;
19use std::collections::BTreeMap;
20use std::path::{Path, PathBuf};
21use std::time::{SystemTime, UNIX_EPOCH};
22
23pub struct StartOptions<'a> {
24 pub branch_type: BranchType,
25 pub name: &'a str,
26 pub isolated: bool,
27 pub repo_root: Option<&'a Path>,
28}
29
30pub fn start_worktree(options: StartOptions<'_>) -> Result<u32, WorkflowError> {
32 let toplevel = resolve_toplevel(options.repo_root)?;
33 let common_dir = resolve_git_common_dir(Some(&toplevel))?;
34 let repo_name = resolve_repo_name(&toplevel);
35
36 let config_path = toplevel.join(".git-claw.toml");
37 let config = Config::load_or_default(&config_path, &repo_name)?;
38
39 let branch_name = options.branch_type.format_branch_name(options.name)?;
40
41 let raw_worktree_root = config.worktree_root(&repo_name);
42 let worktree_root = if Path::new(&raw_worktree_root).is_absolute() {
43 PathBuf::from(raw_worktree_root)
44 } else {
45 toplevel.join(raw_worktree_root)
46 };
47 let worktree_path = worktree_root.join(options.name);
48
49 let lock_path = common_dir.join("claw/slots.lock");
50 let registry_path = common_dir.join("claw/slots.json");
51
52 let _lock = SlotLock::acquire(&lock_path)?;
54
55 let mut registry = SlotRegistry::load_or_empty(®istry_path)?;
56
57 let _ = registry.auto_gc(®istry_path, Some(&toplevel), true);
59
60 if registry.find_by_name(options.name).is_some() {
61 return Err(WorkflowError::DuplicateSlotName(options.name.to_string()));
62 }
63
64 let slot_id = allocate_lowest_slot(registry.occupied_ids());
65
66 let shiftable_ports: BTreeMap<String, u16> = config
68 .ports
69 .iter()
70 .filter(|(k, _)| !is_shared_service_port(k, &config.docker.shared_services))
71 .map(|(k, &v)| (k.clone(), v))
72 .collect();
73
74 let effective_ports = calculate_effective_ports(&shiftable_ports, slot_id)?;
75
76 create_worktree(
78 &toplevel,
79 &worktree_path,
80 &branch_name,
81 config.main_branch(),
82 )?;
83
84 write_env_worktree(
86 &worktree_path,
87 &repo_name,
88 options.name,
89 slot_id,
90 &effective_ports,
91 )?;
92
93 let compose_project = sanitize_compose_project_name(&repo_name, options.name, slot_id);
94
95 let _ = copy_and_merge_untracked_files(
97 &toplevel,
98 &worktree_path,
99 &config.files.copy,
100 &shiftable_ports,
101 slot_id,
102 Some(&compose_project),
103 );
104
105 let _ = generate_docker_compose_override(&toplevel, &worktree_path, &config.docker);
107
108 let _ =
110 link_shared_cache_directories(&toplevel, &worktree_path, &config.cache, options.isolated);
111
112 let now = SystemTime::now()
114 .duration_since(UNIX_EPOCH)
115 .unwrap_or_default()
116 .as_secs();
117
118 registry.slots.push(SlotRecord {
119 id: slot_id,
120 name: options.name.to_string(),
121 branch_type: options.branch_type.to_string(),
122 branch: branch_name.clone(),
123 path: worktree_path.to_string_lossy().to_string(),
124 allocated_at: now.to_string(),
125 });
126 registry.save_atomic(®istry_path)?;
127
128 drop(_lock);
130
131 let mut hook_env = BTreeMap::new();
133 hook_env.insert("CLAW_SLOT_ID".to_string(), slot_id.to_string());
134 hook_env.insert("CLAW_WORKTREE_NAME".to_string(), options.name.to_string());
135 hook_env.insert(
136 "CLAW_WORKTREE_PATH".to_string(),
137 worktree_path.to_string_lossy().to_string(),
138 );
139 for (k, v) in &effective_ports {
140 hook_env.insert(k.clone(), v.to_string());
141 }
142
143 let _ = run_post_start_hook(
145 config.hooks.post_start.as_deref(),
146 &worktree_path,
147 &hook_env,
148 );
149
150 crate::infra::herdr::open_worktree_in_herdr(&toplevel, &branch_name);
151
152 print_success(format!(
153 "Worktree '{}' started successfully (Slot: {}, Branch: {}, Path: {})",
154 options.name,
155 slot_id,
156 branch_name,
157 worktree_path.display()
158 ));
159
160 Ok(slot_id)
161}