Skip to main content

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}