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