iso_code/types.rs
1use std::path::{Path, PathBuf};
2
3// ── 4.1 WorktreeHandle ──────────────────────────────────────────────────
4
5/// A handle to a managed git worktree, containing all metadata tracked by iso-code.
6#[derive(Debug, Clone)]
7#[non_exhaustive]
8pub struct WorktreeHandle {
9 /// Absolute path to the worktree directory on disk.
10 pub path: PathBuf,
11 /// Branch name exactly as passed to create() — never transformed.
12 pub branch: String,
13 /// Full 40-char commit SHA at creation time (the --base ref resolved).
14 pub base_commit: String,
15 /// Current lifecycle state.
16 pub state: WorktreeState,
17 /// ISO 8601 creation timestamp (UTC).
18 pub created_at: String,
19 /// PID of the process that called Manager::create().
20 pub creator_pid: u32,
21 /// Human-readable name of the tool that created this worktree.
22 /// Examples: "claude-squad", "workmux", "claude-code", "manual"
23 pub creator_name: String,
24 /// Name of the EcosystemAdapter used, if any.
25 pub adapter: Option<String>,
26 /// Whether adapter.setup() completed without error.
27 pub setup_complete: bool,
28 /// Allocated port number (the actual port, not an offset).
29 /// None if port allocation was not requested.
30 pub port: Option<u16>,
31 /// Stable UUID for this worktree's entire lifetime.
32 /// Used in multi-factor lock identity and port lease keying.
33 pub session_uuid: String,
34}
35
36impl WorktreeHandle {
37 /// Create a new WorktreeHandle. Used internally by Manager.
38 #[allow(clippy::too_many_arguments)]
39 pub fn new(
40 path: PathBuf,
41 branch: String,
42 base_commit: String,
43 state: WorktreeState,
44 created_at: String,
45 creator_pid: u32,
46 creator_name: String,
47 adapter: Option<String>,
48 setup_complete: bool,
49 port: Option<u16>,
50 session_uuid: String,
51 ) -> Self {
52 Self {
53 path,
54 branch,
55 base_commit,
56 state,
57 created_at,
58 creator_pid,
59 creator_name,
60 adapter,
61 setup_complete,
62 port,
63 session_uuid,
64 }
65 }
66}
67
68// ── 4.2 WorktreeState ───────────────────────────────────────────────────
69
70/// Lifecycle state of a managed worktree.
71///
72/// Deserialization is lenient: unknown variant names (written by a newer
73/// version of iso-code) are mapped to `Broken` rather than failing, so old
74/// readers don't reject an otherwise-valid state file. This keeps the enum
75/// forward-compatible despite `#[non_exhaustive]`.
76#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
77#[non_exhaustive]
78pub enum WorktreeState {
79 /// Allocated in state.json but git worktree add not yet run.
80 Pending,
81 /// git worktree add is in progress.
82 Creating,
83 /// Ready for use. Normal operating state.
84 Active,
85 /// A merge operation involving this worktree is in progress.
86 Merging,
87 /// git worktree remove is in progress.
88 Deleting,
89 /// Successfully deleted. Terminal state.
90 Deleted,
91 /// Present on disk but absent from git worktree list, OR
92 /// present in state.json but absent from both disk and git.
93 Orphaned,
94 /// git references broken, metadata corrupt, or post-create
95 /// verification failed (e.g. git-crypt files still encrypted).
96 Broken,
97 /// git worktree lock has been called on this worktree.
98 Locked,
99}
100
101impl<'de> serde::Deserialize<'de> for WorktreeState {
102 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
103 where
104 D: serde::Deserializer<'de>,
105 {
106 let s = String::deserialize(deserializer)?;
107 Ok(match s.as_str() {
108 "Pending" => Self::Pending,
109 "Creating" => Self::Creating,
110 "Active" => Self::Active,
111 "Merging" => Self::Merging,
112 "Deleting" => Self::Deleting,
113 "Deleted" => Self::Deleted,
114 "Orphaned" => Self::Orphaned,
115 "Broken" => Self::Broken,
116 "Locked" => Self::Locked,
117 // Unknown variant from a newer writer — degrade rather than fail.
118 _ => Self::Broken,
119 })
120 }
121}
122
123// ── 4.3 ReflinkMode & CopyOutcome ──────────────────────────────────────
124
125/// Controls Copy-on-Write behavior when copying files into a new worktree.
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
127pub enum ReflinkMode {
128 /// Fail immediately if the filesystem does not support CoW.
129 /// Returns: WorktreeError::ReflinkNotSupported
130 Required,
131 /// Try CoW, fall back to standard copy if unsupported. Default.
132 #[default]
133 Preferred,
134 /// Never attempt CoW. Always use standard copy.
135 Disabled,
136}
137
138/// Returned by Manager::create() to report what actually happened during file copy steps.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum CopyOutcome {
141 Reflinked,
142 StandardCopy { bytes_written: u64 },
143 /// No file copying occurred (worktree created via git checkout only).
144 None,
145}
146
147// ── 4.4 Config ──────────────────────────────────────────────────────────
148
149/// Configuration for a [`Manager`](crate::Manager) instance.
150#[derive(Debug, Clone)]
151#[non_exhaustive]
152pub struct Config {
153 /// Maximum managed worktrees per repository. Default: 20.
154 pub max_worktrees: usize,
155 /// Refuse creation if aggregate worktree disk usage exceeds this
156 /// percentage of the filesystem. Default: 90.
157 pub disk_threshold_percent: u8,
158 /// Auto-GC worktrees older than this many days. Default: 7.
159 pub gc_max_age_days: u32,
160 /// Start of the port range for lease allocation. Default: 3100.
161 pub port_range_start: u16,
162 /// End of the port range for lease allocation (exclusive). Default: 5100.
163 pub port_range_end: u16,
164 /// Minimum free disk space required to create a worktree. Default: 500 MB.
165 pub min_free_disk_mb: u64,
166 /// Override all state file paths (useful for CI and containers).
167 /// Mirrors the ISO_CODE_HOME environment variable.
168 pub home_override: Option<PathBuf>,
169 /// Maximum aggregate disk usage across all managed worktrees in bytes.
170 /// None = unlimited. Default: None.
171 pub max_total_disk_bytes: Option<u64>,
172 /// Trip circuit breaker after this many consecutive git command failures.
173 /// Default: 3.
174 pub circuit_breaker_threshold: u32,
175 /// How long evicted metadata is preserved in state.json before
176 /// permanent deletion. Default: 30 days.
177 pub stale_metadata_ttl_days: u32,
178 /// Total timeout for state.lock acquisition including all retries.
179 /// Default: 30,000 ms.
180 pub lock_timeout_ms: u64,
181 /// Name recorded in state.json as creator_name for this Manager instance.
182 /// Example: "claude-squad", "workmux", "my-orchestrator"
183 pub creator_name: String,
184 /// Skip network operations (e.g. `git fetch` in the five-step unmerged
185 /// commit check). Set to true when running offline or in CI to avoid
186 /// network latency per delete / per-candidate during `gc()`.
187 /// Default: false.
188 pub offline: bool,
189}
190
191impl Default for Config {
192 fn default() -> Self {
193 Self {
194 max_worktrees: 20,
195 disk_threshold_percent: 90,
196 gc_max_age_days: 7,
197 port_range_start: 3100,
198 port_range_end: 5100,
199 min_free_disk_mb: 500,
200 home_override: None,
201 max_total_disk_bytes: None,
202 circuit_breaker_threshold: 3,
203 stale_metadata_ttl_days: 30,
204 lock_timeout_ms: 30_000,
205 creator_name: "iso-code".to_string(),
206 offline: false,
207 }
208 }
209}
210
211// ── 4.5 CreateOptions ───────────────────────────────────────────────────
212
213/// Options for [`Manager::create()`](crate::Manager::create).
214#[derive(Debug, Clone, Default)]
215#[non_exhaustive]
216pub struct CreateOptions {
217 /// Base ref to create the worktree from. Default: HEAD.
218 pub base: Option<String>,
219 /// Run the registered EcosystemAdapter after creation. Default: false.
220 pub setup: bool,
221 /// Skip aggregate disk limit check. Default: false.
222 pub ignore_disk_limit: bool,
223 /// Call git worktree lock immediately after creation (atomic — no race
224 /// window between creation and locking). Default: false.
225 pub lock: bool,
226 /// Reason string for git worktree lock. Requires lock = true.
227 pub lock_reason: Option<String>,
228 /// Controls Copy-on-Write behavior for file operations. Default: Preferred.
229 pub reflink_mode: ReflinkMode,
230 /// Allocate a port lease for this worktree. Default: false.
231 pub allocate_port: bool,
232}
233
234// ── 4.5b AttachOptions ──────────────────────────────────────────────────
235
236/// Options for [`Manager::attach()`](crate::Manager::attach).
237#[derive(Debug, Clone, Default)]
238#[non_exhaustive]
239pub struct AttachOptions {
240 /// Run the registered EcosystemAdapter after attaching. Default: false.
241 pub setup: bool,
242 /// Allocate a port lease for this worktree. Default: false.
243 pub allocate_port: bool,
244}
245
246// ── 4.6 DeleteOptions ───────────────────────────────────────────────────
247
248/// Options for [`Manager::delete()`](crate::Manager::delete).
249#[derive(Debug, Clone, Default)]
250#[non_exhaustive]
251pub struct DeleteOptions {
252 /// Skip the five-step unmerged commits check.
253 /// WARNING: Can cause data loss. Requires explicit opt-in. Default: false.
254 pub force: bool,
255 /// Skip the uncommitted changes check.
256 /// WARNING: Destroys uncommitted work. Default: false.
257 pub force_dirty: bool,
258 /// Delete even when the worktree is locked. Bypasses `check_not_locked`
259 /// and calls `git worktree remove --force` instead of the plain remove.
260 /// WARNING: Locks exist for a reason — only set when the caller owns the lock. Default: false.
261 pub force_locked: bool,
262}
263
264// ── 4.7 GcOptions & GcReport ────────────────────────────────────────────
265
266/// Options for [`Manager::gc()`](crate::Manager::gc).
267#[derive(Debug, Clone)]
268#[non_exhaustive]
269pub struct GcOptions {
270 /// Report what would happen without doing anything. Default: true.
271 /// Always run with dry_run = true first to verify scope.
272 pub dry_run: bool,
273 /// Override gc_max_age_days from Config for this run.
274 pub max_age_days: Option<u32>,
275 /// Skip unmerged commit check during deletion. Default: false.
276 pub force: bool,
277}
278
279impl Default for GcOptions {
280 fn default() -> Self {
281 Self {
282 dry_run: true,
283 max_age_days: None,
284 force: false,
285 }
286 }
287}
288
289/// Report returned by [`Manager::gc()`](crate::Manager::gc).
290#[derive(Debug, Clone)]
291#[non_exhaustive]
292pub struct GcReport {
293 /// Worktrees identified as orphaned.
294 pub orphans: Vec<PathBuf>,
295 /// Worktrees actually removed (empty if dry_run = true).
296 pub removed: Vec<PathBuf>,
297 /// Worktrees moved to stale_worktrees (not deleted; metadata preserved for recovery).
298 pub evicted: Vec<PathBuf>,
299 /// Total disk space freed in bytes (0 if dry_run = true).
300 pub freed_bytes: u64,
301 /// Whether this was a dry run.
302 pub dry_run: bool,
303}
304
305impl GcReport {
306 pub fn new(
307 orphans: Vec<PathBuf>,
308 removed: Vec<PathBuf>,
309 evicted: Vec<PathBuf>,
310 freed_bytes: u64,
311 dry_run: bool,
312 ) -> Self {
313 Self { orphans, removed, evicted, freed_bytes, dry_run }
314 }
315}
316
317// ── 4.8 GitCapabilities & GitVersion ────────────────────────────────────
318
319/// Feature flags derived from the detected git version.
320#[derive(Debug, Clone)]
321#[non_exhaustive]
322pub struct GitCapabilities {
323 pub version: GitVersion,
324 /// git worktree list --porcelain -z (2.36+)
325 /// When false, parser uses newline-delimited output.
326 /// NOTE: Paths containing newlines will fail silently on < 2.36.
327 pub has_list_nul: bool,
328 /// git worktree repair (2.30+)
329 pub has_repair: bool,
330 /// git worktree add --orphan (2.42+)
331 pub has_orphan: bool,
332 /// worktree.useRelativePaths config (2.48+)
333 pub has_relative_paths: bool,
334 /// git merge-tree --write-tree (2.38+). Required for v1.1 conflict detection.
335 pub has_merge_tree_write: bool,
336}
337
338impl GitCapabilities {
339 pub fn new(
340 version: GitVersion,
341 has_list_nul: bool,
342 has_repair: bool,
343 has_orphan: bool,
344 has_relative_paths: bool,
345 has_merge_tree_write: bool,
346 ) -> Self {
347 Self { version, has_list_nul, has_repair, has_orphan, has_relative_paths, has_merge_tree_write }
348 }
349}
350
351/// Parsed semantic version of the git binary.
352#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
353pub struct GitVersion {
354 pub major: u32,
355 pub minor: u32,
356 pub patch: u32,
357}
358
359impl GitVersion {
360 pub const MINIMUM: GitVersion = GitVersion { major: 2, minor: 20, patch: 0 };
361 pub const HAS_LIST_NUL: GitVersion = GitVersion { major: 2, minor: 36, patch: 0 };
362 pub const HAS_REPAIR: GitVersion = GitVersion { major: 2, minor: 30, patch: 0 };
363 pub const HAS_MERGE_TREE_WRITE: GitVersion = GitVersion { major: 2, minor: 38, patch: 0 };
364}
365
366// ── 4.9 PortLease ───────────────────────────────────────────────────────
367
368/// A port allocated to a worktree, with an 8-hour TTL.
369#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
370pub struct PortLease {
371 pub port: u16,
372 pub branch: String,
373 pub session_uuid: String,
374 pub pid: u32,
375 pub created_at: chrono::DateTime<chrono::Utc>,
376 pub expires_at: chrono::DateTime<chrono::Utc>,
377 /// "active" or "stale" (after worktree eviction, before TTL expiry).
378 pub status: String,
379}
380
381// ── 8.3 GitCryptStatus ──────────────────────────────────────────────────
382
383/// Status of git-crypt in a repository or worktree.
384#[derive(Debug, Clone, PartialEq, Eq)]
385pub enum GitCryptStatus {
386 NotUsed,
387 /// Key file absent.
388 LockedNoKey,
389 /// Key exists but files show magic header — smudge filter not run.
390 Locked,
391 /// Key exists and files are decrypted.
392 Unlocked,
393}
394
395// ── 6. EcosystemAdapter Trait ───────────────────────────────────────────
396
397/// Trait for language/framework-specific setup in new worktrees.
398pub trait EcosystemAdapter: Send + Sync {
399 /// Name used in state.json and log messages.
400 fn name(&self) -> &str;
401
402 /// Return true if this adapter should run for the given worktree path.
403 /// Called during auto-detection. Inspect package.json, Cargo.toml, etc.
404 fn detect(&self, worktree_path: &Path) -> bool;
405
406 /// Set up the environment in the new worktree.
407 ///
408 /// source_worktree is the main worktree path (for copying files from).
409 fn setup(
410 &self,
411 worktree_path: &Path,
412 source_worktree: &Path,
413 ) -> Result<(), crate::error::WorktreeError>;
414
415 /// Clean up adapter-managed resources when the worktree is deleted.
416 fn teardown(&self, worktree_path: &Path) -> Result<(), crate::error::WorktreeError>;
417
418 /// Optionally transform the branch name before use.
419 /// Default: identity (no transformation).
420 /// The core library NEVER calls this internally. Only adapters that opt in use it.
421 fn branch_name(&self, input: &str) -> String {
422 input.to_string()
423 }
424}