Skip to main content

grit_lib/history/
error.rs

1//! Typed failures for history rewrite planning.
2
3use crate::error::Error;
4use crate::objects::ObjectId;
5use thiserror::Error;
6
7/// Failure modes while building a [`super::RewritePlan`](crate::history::RewritePlan).
8#[derive(Debug, Error)]
9#[non_exhaustive]
10pub enum HistoryError {
11    /// Underlying object database, revision walk, or ref operation failed.
12    #[error(transparent)]
13    Library(Box<Error>),
14
15    /// The replacement commit message is empty after whitespace cleanup.
16    #[error("commit message cannot be empty")]
17    EmptyMessage,
18
19    /// [`super::reword::reword_with`](crate::history::reword::reword_with) needs a message and/or author change.
20    #[error("reword requires a new message or author")]
21    NothingToReword,
22
23    /// Author argument could not be parsed (`Name <email>` or bare email).
24    #[error("{0}")]
25    InvalidAuthor(#[from] crate::ident::AuthorArgError),
26
27    /// [`super::fixup`](crate::history::fixup) requires a working tree and index.
28    #[error("history fixup cannot run in a bare repository")]
29    BareRepository,
30
31    /// No staged changes relative to `HEAD`'s tree.
32    #[error("nothing to fixup: no staged changes")]
33    NothingStaged,
34
35    /// Hunk id or line-range selection failed before planning changed refs.
36    #[error(transparent)]
37    HunkSelect(#[from] crate::porcelain::hunk_select::HunkSelectError),
38
39    /// Applying staged changes onto the target would make that commit empty.
40    #[error("fixup makes commit {commit} empty")]
41    FixupMakesTargetEmpty {
42        /// Target commit that would become empty.
43        commit: ObjectId,
44    },
45
46    /// A pathspec split selected no changes from the target commit.
47    #[error("pathspec did not select any changes from the commit")]
48    SplitSelectsNothing,
49
50    /// A pathspec split selected every change in the target commit.
51    #[error("pathspec selected every change in the commit")]
52    SplitSelectsEverything,
53
54    /// Object database, revwalk, or other repository read failed during planning.
55    #[error("history rewrite planning failed: {message}")]
56    Planning {
57        /// Underlying failure summary.
58        message: String,
59    },
60
61    /// `InsertBefore` was requested on a merge commit.
62    #[error("insert-before on merge commit {commit} is not supported")]
63    InsertBeforeOnMerge {
64        /// Merge commit that cannot receive an insert-before edit.
65        commit: ObjectId,
66    },
67
68    /// A three-way replay produced unmerged paths.
69    #[error("replay of commit {commit} produced conflicts")]
70    Conflict {
71        /// Original commit that could not be replayed cleanly.
72        commit: ObjectId,
73        /// Repository-relative paths that conflicted, sorted and deduplicated.
74        paths: Vec<String>,
75    },
76
77    /// A merge commit cannot be dropped, reordered, or otherwise edited in an unsupported way.
78    #[error("merge commit {commit} cannot be dropped or reordered in this rewrite")]
79    MergeCommitInReplay {
80        /// Merge commit that cannot be replayed.
81        commit: ObjectId,
82    },
83
84    /// A root commit was dropped or otherwise needs a missing parent.
85    #[error("root commit {commit} cannot be dropped or replayed without a parent")]
86    RootCommit {
87        /// Root commit involved in the error.
88        commit: ObjectId,
89    },
90
91    /// Empty-policy [`super::EmptyPolicy::Abort`](crate::history::EmptyPolicy::Abort) tripped.
92    #[error("commit {commit} became empty after replay")]
93    BecameEmpty {
94        /// Commit that would become empty.
95        commit: ObjectId,
96    },
97
98    /// An edited commit is not on the history reachable from the active scope.
99    #[error("commit {commit} is not reachable from HEAD for this rewrite scope")]
100    NotInHistory {
101        /// Commit named in the edit map but outside scope.
102        commit: ObjectId,
103    },
104
105    /// A move anchor is not on the first-parent chain of `HEAD`.
106    #[error("commit {commit} is not on the linear first-parent history of HEAD")]
107    MoveTargetNotInHistory {
108        /// Anchor commit that is missing from the chain.
109        commit: ObjectId,
110    },
111
112    /// The move would leave commit order unchanged.
113    #[error("commit {commit} is already at the requested position")]
114    NoOp {
115        /// Commit that would not move.
116        commit: ObjectId,
117    },
118
119    /// Commits are not on the same first-parent chain or a merge sits in the segment.
120    #[error("history move requires a linear first-parent range without merge commits")]
121    NotLinear,
122
123    /// A scoped branch tip is not on the ancestry path being rewritten.
124    #[error("branch {name} diverged before the rewritten commits and cannot be updated")]
125    DivergentBranch {
126        /// Short or full branch name (`refs/heads/...`).
127        name: String,
128    },
129
130    /// Another linked worktree has this branch checked out.
131    #[error("branch {name} is checked out in worktree {worktree}")]
132    BranchCheckedOutElsewhere {
133        /// Branch ref short or full name.
134        name: String,
135        /// Path to the blocking worktree.
136        worktree: String,
137    },
138
139    /// Commit signing failed while writing a replacement commit object.
140    #[error("commit signing failed: {message}")]
141    Signing {
142        /// Underlying signing or config failure summary.
143        message: String,
144    },
145
146    /// Applying the plan would overwrite staged or unstaged worktree changes on affected paths.
147    #[error("local changes would be overwritten")]
148    LocalChangesWouldBeOverwritten {
149        /// Repository-relative paths that block the checkout.
150        paths: Vec<String>,
151    },
152
153    /// The index or worktree differs from `HEAD` before `rebase --exec` runs.
154    #[error("you have uncommitted changes — commit them before rebasing with --exec")]
155    UncommittedChanges,
156
157    /// A `rebase --exec` command exited non-zero; repository state was restored.
158    #[error("rebase --exec command failed at commit {commit} (exit {exit_code})")]
159    ExecFailed {
160        /// Replacement commit that was checked out when the command failed.
161        commit: ObjectId,
162        /// Subprocess exit code.
163        exit_code: i32,
164        /// Subprocess stdout from the failing command.
165        stdout: Vec<u8>,
166        /// Subprocess stderr from the failing command.
167        stderr: Vec<u8>,
168        /// Successful `--exec` runs before the failure (stdout/stderr captured).
169        completed_runs: Vec<super::ExecRunRecord>,
170    },
171
172    /// Selected commits do not form one connected component with allowed parents.
173    #[error("the selected commits are not a connected revision range")]
174    RangeNotConnected,
175
176    /// More than one tip commit appears in the selected range.
177    #[error("the revision range contains more than one tip commit")]
178    MultipleTips,
179
180    /// A fixup/amend/squash commit targets a commit outside the range.
181    #[error("cannot squash {commit}: its fixup/amend/squash target is outside the range")]
182    MarkerTargetOutsideRange {
183        /// Marker commit that could not be folded.
184        commit: ObjectId,
185    },
186
187    /// A `squash!` commit would lose its message without combining interactively.
188    #[error("squashing {commit} would discard its message; use --combine or -m")]
189    SquashDiscardsMessage {
190        /// Commit whose message would be dropped.
191        commit: ObjectId,
192    },
193
194    /// A planned ref no longer matches the stored value (compare-and-swap failure).
195    #[error("ref {name} changed since the rewrite plan was built")]
196    RefCasMismatch {
197        /// Ref that failed the expected-old check.
198        name: String,
199    },
200
201    /// `HEAD` no longer points at the ref or commit recorded in the plan.
202    #[error("HEAD moved since the rewrite plan was built")]
203    HeadMovedSincePlan {
204        /// Ref `HEAD` pointed at when planning, if symbolic.
205        planned_ref: Option<String>,
206        /// Commit `HEAD` pointed at when planning.
207        planned_commit: ObjectId,
208    },
209
210    /// Ref storage rejected the update batch.
211    #[error("ref update failed: {message}")]
212    RefApply {
213        /// Backend error summary.
214        message: String,
215    },
216
217    /// Index or worktree update failed after refs were already moved.
218    #[error("worktree update failed: {message}")]
219    WorktreeApply {
220        /// Underlying failure.
221        message: String,
222    },
223
224    /// No `.git/grit/history-last` record exists.
225    #[error("no history rewrite to undo")]
226    NoUndoRecord,
227
228    /// A ref or `HEAD` moved since the recorded rewrite.
229    #[error("ref {name} changed since the last history rewrite — undo is no longer safe")]
230    UndoStale {
231        /// Ref that no longer matches the recorded post-rewrite tip.
232        name: String,
233    },
234
235    /// Failed to read `.git/grit/history-last`.
236    #[error("could not read history undo record: {message}")]
237    UndoRecordRead {
238        /// Parse or I/O failure summary.
239        message: String,
240    },
241
242    /// Failed to write `.git/grit/history-last`.
243    #[error("could not write history undo record: {message}")]
244    UndoRecordWrite {
245        /// I/O or lock failure summary.
246        message: String,
247    },
248
249    /// Refs were updated but the worktree checkout did not finish (see `.git/grit/rewrite-apply/`).
250    #[error("history rewrite checkout is incomplete")]
251    RewriteApplyIncomplete,
252
253    /// Checkout failed after refs were updated; rollback or worktree restore may also have failed.
254    #[error("checkout failed after refs were updated")]
255    ApplyRollbackIncomplete {
256        /// Primary checkout failure.
257        checkout: String,
258        /// Ref rollback error, when rolling back failed.
259        ref_rollback: Option<String>,
260        /// Worktree/index restore error after checkout failed.
261        worktree_restore: Option<String>,
262    },
263}
264
265impl HistoryError {
266    /// Whether the rewrite was cancelled ([`Error::Cancelled`]).
267    #[must_use]
268    pub fn is_cancelled(&self) -> bool {
269        match self {
270            Self::Library(e) => e.is_cancelled(),
271            _ => false,
272        }
273    }
274}
275
276impl From<Error> for HistoryError {
277    /// Unwraps [`Error::History`] back to the history failure it carries; every other library
278    /// error becomes [`HistoryError::Library`].
279    fn from(err: Error) -> Self {
280        match err {
281            Error::History(inner) => inner,
282            other => Self::Library(Box::new(other)),
283        }
284    }
285}