Skip to main content

yarli_cli/yarli-git/src/
error.rs

1//! Error types for yarli-git.
2//!
3//! Covers worktree lifecycle, merge orchestration, submodule policy,
4//! recovery, and forbidden-operation errors (Sections 12.1–12.12).
5
6use std::path::PathBuf;
7
8use thiserror::Error;
9
10use crate::yarli_core::fsm::merge::MergeState;
11use crate::yarli_core::fsm::worktree::WorktreeState;
12
13/// Top-level error type for all git-plane operations.
14#[derive(Debug, Error)]
15pub enum GitError {
16    // ── Worktree errors (Section 12.1–12.3, 12.11) ────────────────
17    /// Failed to create a worktree (Section 12.2).
18    #[error("worktree creation failed: {reason}")]
19    WorktreeCreation { reason: String },
20
21    /// Worktree path already exists on disk.
22    #[error("worktree path already exists: {path}")]
23    WorktreePathExists { path: PathBuf },
24
25    /// Worktree not found at expected path.
26    #[error("worktree not found: {path}")]
27    WorktreeNotFound { path: PathBuf },
28
29    /// A command tried to mutate the repository from an invalid worktree state.
30    /// Section 12.3: mutations denied in WtUnbound/WtClosed/WtCleanupPending.
31    #[error("mutations denied in worktree state {state:?}")]
32    MutationDenied { state: WorktreeState },
33
34    /// Path traversal outside worktree root detected (Section 12.3).
35    #[error("path escapes worktree root: {path} is outside {root}")]
36    PathConfinementViolation { path: PathBuf, root: PathBuf },
37
38    /// The worktree has uncommitted changes when a clean state is required.
39    #[error("worktree is dirty: {path}")]
40    DirtyWorktree { path: PathBuf },
41
42    /// Detached HEAD without explicit override policy (Section 12.3).
43    #[error("detached HEAD in worktree: {path}")]
44    DetachedHead { path: PathBuf },
45
46    /// The `.git` indirection file is missing or invalid (Section 12.2 step 5).
47    #[error("invalid .git indirection in worktree: {path}")]
48    InvalidGitIndirection { path: PathBuf },
49
50    /// Cleanup preconditions not met (Section 12.11).
51    #[error("worktree cleanup blocked: {reason}")]
52    CleanupBlocked { reason: String },
53
54    // ── Merge errors (Section 12.5–12.9) ───────────────────────────
55    /// Merge precheck failed (Section 12.6).
56    #[error("merge precheck failed: {reason}")]
57    MergePrecheckFailed { reason: String },
58
59    /// Target ref moved since precheck — must restart (Section 12.8 step 2).
60    #[error("target ref stale: expected {expected}, found {actual}")]
61    TargetRefStale { expected: String, actual: String },
62
63    /// Merge conflicts detected (Section 12.7, 12.9).
64    #[error("merge conflicts detected: {count} file(s)")]
65    MergeConflict { count: usize },
66
67    /// A git hook rejected the merge commit (e.g. commit-msg, pre-commit).
68    /// Distinct from MergeConflict — the merge content was clean but a hook
69    /// blocked the commit.
70    #[error("git hook rejected merge: {hook} — {stderr}")]
71    HookRejected { hook: String, stderr: String },
72
73    /// Could not acquire merge lock for the target branch (Section 12.8).
74    #[error("merge lock unavailable for branch {branch}")]
75    MergeLockUnavailable { branch: String },
76
77    /// Merge verification failed after apply (Section 12.8 step 4).
78    #[error("merge verification failed: {reason}")]
79    MergeVerifyFailed { reason: String },
80
81    /// Merge operation attempted in an invalid merge state.
82    #[error("invalid merge state for operation: {state:?}")]
83    InvalidMergeState { state: MergeState },
84
85    // ── Submodule errors (Section 12.4) ────────────────────────────
86    /// Uninitialized submodule detected before task start.
87    #[error("uninitialized submodule: {path}")]
88    UninitializedSubmodule { path: String },
89
90    /// Dirty submodule detected before merge.
91    #[error("dirty submodule: {path}")]
92    DirtySubmodule { path: String },
93
94    /// Submodule update violates the configured policy mode.
95    #[error("submodule policy violation at {path}: {reason}")]
96    SubmodulePolicyViolation { path: String, reason: String },
97
98    // ── Forbidden operations (Section 12.12) ───────────────────────
99    /// A forbidden git operation was attempted without policy approval.
100    #[error("forbidden git operation: {operation}")]
101    ForbiddenOperation { operation: ForbiddenOp },
102
103    // ── Recovery errors (Section 12.10) ────────────────────────────
104    /// An interrupted git operation was detected on recovery.
105    #[error("interrupted {operation} detected in {path}")]
106    InterruptedOperation {
107        operation: InterruptedOp,
108        path: PathBuf,
109    },
110
111    /// Recovery action failed.
112    #[error("recovery failed: {reason}")]
113    RecoveryFailed { reason: String },
114
115    // ── Ref resolution ─────────────────────────────────────────────
116    /// A ref (branch/tag/sha) could not be resolved.
117    #[error("ref not found: {refspec}")]
118    RefNotFound { refspec: String },
119
120    /// Branch already exists when expecting to create a new one.
121    #[error("branch already exists: {branch}")]
122    BranchAlreadyExists { branch: String },
123
124    // ── Underlying errors ──────────────────────────────────────────
125    /// Git command returned a non-zero exit code.
126    #[error("git command failed (exit {exit_code}): {stderr}")]
127    CommandFailed { exit_code: i32, stderr: String },
128
129    /// I/O error from filesystem or process operations.
130    #[error("io error: {0}")]
131    Io(#[from] std::io::Error),
132
133    /// State transition error from yarli-core.
134    #[error("transition error: {0}")]
135    Transition(#[from] crate::yarli_core::error::TransitionError),
136
137    /// Exec error from yarli-exec command runner.
138    #[error("exec error: {0}")]
139    Exec(#[from] crate::yarli_exec::error::ExecError),
140}
141
142/// Forbidden git operations (Section 12.12, default-deny).
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub enum ForbiddenOp {
145    /// `git push` without explicit push intent approved.
146    Push,
147    /// `git push --force` or `git push --force-with-lease`.
148    ForcePush,
149    /// `git tag` / release tagging.
150    Tag,
151    /// Branch deletion outside policy.
152    BranchDelete { branch: String },
153    /// `git stash clear` or other global stash operations.
154    StashClear,
155    /// Repository-wide destructive cleanup (e.g. `git clean -fdx`).
156    DestructiveCleanup { command: String },
157}
158
159impl std::fmt::Display for ForbiddenOp {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        match self {
162            ForbiddenOp::Push => write!(f, "git push"),
163            ForbiddenOp::ForcePush => write!(f, "git push --force"),
164            ForbiddenOp::Tag => write!(f, "git tag"),
165            ForbiddenOp::BranchDelete { branch } => {
166                write!(f, "git branch -D {branch}")
167            }
168            ForbiddenOp::StashClear => write!(f, "git stash clear"),
169            ForbiddenOp::DestructiveCleanup { command } => write!(f, "{command}"),
170        }
171    }
172}
173
174/// Interrupted git operations that can be detected on recovery (Section 12.10).
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum InterruptedOp {
177    /// An in-progress merge (`MERGE_HEAD` exists).
178    Merge,
179    /// An in-progress rebase (`.git/rebase-merge` or `.git/rebase-apply` exists).
180    Rebase,
181    /// An in-progress cherry-pick (`CHERRY_PICK_HEAD` exists).
182    CherryPick,
183    /// An in-progress revert (`REVERT_HEAD` exists).
184    Revert,
185}
186
187impl std::fmt::Display for InterruptedOp {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        match self {
190            InterruptedOp::Merge => write!(f, "merge"),
191            InterruptedOp::Rebase => write!(f, "rebase"),
192            InterruptedOp::CherryPick => write!(f, "cherry-pick"),
193            InterruptedOp::Revert => write!(f, "revert"),
194        }
195    }
196}
197
198/// Recovery policy for interrupted operations (Section 12.10).
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum RecoveryAction {
201    /// Abort the interrupted operation and return to a clean state.
202    Abort,
203    /// Attempt to resume/continue the interrupted operation.
204    Resume,
205    /// Block and require manual intervention.
206    ManualBlock,
207}