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    /// Generating a change id for a replacement commit failed (for example the
41    /// [`ChangeIdGenerator`](crate::change_id::ChangeIdGenerator) had no entropy).
42    #[error(transparent)]
43    ChangeId(#[from] crate::change_id::ChangeIdError),
44
45    /// Applying staged changes onto the target would make that commit empty.
46    #[error("fixup makes commit {commit} empty")]
47    FixupMakesTargetEmpty {
48        /// Target commit that would become empty.
49        commit: ObjectId,
50    },
51
52    /// A pathspec split selected no changes from the target commit.
53    #[error("pathspec did not select any changes from the commit")]
54    SplitSelectsNothing,
55
56    /// A pathspec split selected every change in the target commit.
57    #[error("pathspec selected every change in the commit")]
58    SplitSelectsEverything,
59
60    /// A planning input is invalid (for example a squash range that is not `<base>..<tip>`), or
61    /// [`plan_rewrite`](crate::history::plan_rewrite) hit a repository read failure other than a
62    /// missing object. Other entry points report library failures as [`Self::Library`].
63    #[error("history rewrite planning failed: {message}")]
64    Planning {
65        /// Underlying failure summary.
66        message: String,
67    },
68
69    /// `InsertBefore` was requested on a merge commit.
70    #[error("insert-before on merge commit {commit} is not supported")]
71    InsertBeforeOnMerge {
72        /// Merge commit that cannot receive an insert-before edit.
73        commit: ObjectId,
74    },
75
76    /// A three-way replay produced unmerged paths. Nothing was written to refs.
77    #[error("replay of commit {commit} produced conflicts")]
78    Conflict {
79        /// Original commit that could not be replayed cleanly.
80        commit: ObjectId,
81        /// First line of that commit's message.
82        subject: String,
83        /// Conflicted paths and how they conflicted, sorted by path.
84        conflicts: Vec<Conflict>,
85        /// Full names of the scoped branches whose replay failed, sorted, when `commit` is not
86        /// on `HEAD`'s history: under [`RefScope::Branches`](super::RefScope::Branches) another
87        /// branch built on the edited commits could not be moved onto the new history, and
88        /// [`RefScope::Head`](super::RefScope::Head) would rewrite only the current branch.
89        /// Empty when the conflict is on the checked-out history itself.
90        refs: Vec<String>,
91    },
92
93    /// A merge commit cannot be dropped, reordered, or otherwise edited in an unsupported way.
94    #[error("merge commit {commit} cannot be dropped or reordered in this rewrite")]
95    MergeCommitInReplay {
96        /// Merge commit that cannot be replayed.
97        commit: ObjectId,
98    },
99
100    /// A root commit was dropped or otherwise needs a missing parent.
101    #[error("root commit {commit} cannot be dropped or replayed without a parent")]
102    RootCommit {
103        /// Root commit involved in the error.
104        commit: ObjectId,
105    },
106
107    /// Empty-policy [`super::EmptyPolicy::Abort`](crate::history::EmptyPolicy::Abort) tripped.
108    #[error("commit {commit} became empty after replay")]
109    BecameEmpty {
110        /// Commit that would become empty.
111        commit: ObjectId,
112    },
113
114    /// An edited commit is not on the history reachable from the active scope.
115    #[error("commit {commit} is not reachable from HEAD for this rewrite scope")]
116    NotInHistory {
117        /// Commit named in the edit map but outside scope.
118        commit: ObjectId,
119    },
120
121    /// A move anchor is not on the first-parent chain of `HEAD`.
122    #[error("commit {commit} is not on the linear first-parent history of HEAD")]
123    MoveTargetNotInHistory {
124        /// Anchor commit that is missing from the chain.
125        commit: ObjectId,
126    },
127
128    /// The move would leave commit order unchanged.
129    #[error("commit {commit} is already at the requested position")]
130    NoOp {
131        /// Commit that would not move.
132        commit: ObjectId,
133    },
134
135    /// Commits are not on the same first-parent chain or a merge sits in the segment.
136    #[error("history move requires a linear first-parent range without merge commits")]
137    NotLinear,
138
139    /// A scoped branch tip is not on the ancestry path being rewritten.
140    #[error("branch {name} diverged before the rewritten commits and cannot be updated")]
141    DivergentBranch {
142        /// Short or full branch name (`refs/heads/...`).
143        name: String,
144    },
145
146    /// Another linked worktree has this branch checked out.
147    #[error("branch {name} is checked out in worktree {worktree}")]
148    BranchCheckedOutElsewhere {
149        /// Branch ref short or full name.
150        name: String,
151        /// Path to the blocking worktree.
152        worktree: String,
153    },
154
155    /// Commit signing failed while writing a replacement commit object.
156    #[error("commit signing failed: {message}")]
157    Signing {
158        /// Underlying signing or config failure summary.
159        message: String,
160    },
161
162    /// Applying the plan would overwrite staged or unstaged worktree changes on affected paths.
163    #[error("local changes would be overwritten")]
164    LocalChangesWouldBeOverwritten {
165        /// Repository-relative paths that block the checkout.
166        paths: Vec<String>,
167    },
168
169    /// The index or worktree differs from `HEAD` before `rebase --exec` runs.
170    #[error("you have uncommitted changes — commit them before rebasing with --exec")]
171    UncommittedChanges,
172
173    /// A successful exec changed tracked paths. Edits and the replay HEAD are preserved;
174    /// original branch refs are unchanged and rebase recovery state is persisted.
175    #[error("rebase --exec left tracked changes at commit {commit}; save the edits before continuing, or abort the rebase")]
176    ExecDirty {
177        /// Detached `HEAD` after the command: the replay commit, or the commit the command
178        /// amended it into. Recovery expects `HEAD` here.
179        commit: ObjectId,
180        /// Successful command that changed the index or worktree.
181        command: String,
182        /// Changed paths relative to `commit`, sorted and deduplicated.
183        paths: Vec<String>,
184        /// Captured invocations through the dirty command, in execution order.
185        runs: Vec<super::ExecRunRecord>,
186    },
187
188    /// Replaying a later commit onto one that an `--exec` command amended or extended
189    /// produced conflicts. The conflicted replay is checked out with conflict markers on
190    /// top of the rewritten commit, branch refs are unchanged, and recovery state is
191    /// persisted: resolve and stage, then [`super::continue_rebase`]; or
192    /// [`super::skip_rebase`] to drop the commit, or [`super::abort_rebase`].
193    #[error(
194        "replay of commit {commit} onto the commit rewritten by rebase --exec produced conflicts"
195    )]
196    ExecConflict {
197        /// Original commit that could not be replayed cleanly.
198        commit: ObjectId,
199        /// Conflicted paths and how they conflicted, sorted by path.
200        conflicts: Vec<Conflict>,
201        /// Captured invocations before the conflict, in execution order.
202        runs: Vec<super::ExecRunRecord>,
203    },
204
205    /// A successful `--exec` command left `HEAD` somewhere other than the replay commit, an
206    /// amended replacement of it, or new commits on top of either (for example it switched
207    /// branches or reset away the commit). Repository state was restored; commits the
208    /// command created remain in the object database.
209    #[error("rebase --exec command moved HEAD away from commit {commit}; it may only amend or add commits on top of it")]
210    ExecHeadMoved {
211        /// Replay commit checked out when the command ran.
212        commit: ObjectId,
213        /// Commit `HEAD` resolved to after the command, when readable.
214        head: Option<ObjectId>,
215    },
216
217    /// A `rebase --exec` command exited non-zero; repository state was restored.
218    #[error("rebase --exec command failed at commit {commit} (exit {exit_code})")]
219    ExecFailed {
220        /// Replacement commit that was checked out when the command failed.
221        commit: ObjectId,
222        /// Subprocess exit code.
223        exit_code: i32,
224        /// Subprocess stdout from the failing command.
225        stdout: Vec<u8>,
226        /// Subprocess stderr from the failing command.
227        stderr: Vec<u8>,
228        /// Successful `--exec` runs before the failure (stdout/stderr captured).
229        completed_runs: Vec<super::ExecRunRecord>,
230    },
231
232    /// Selected commits do not form one connected component with allowed parents.
233    #[error("the selected commits are not a connected revision range")]
234    RangeNotConnected,
235
236    /// More than one tip commit appears in the selected range.
237    #[error("the revision range contains more than one tip commit")]
238    MultipleTips,
239
240    /// A fixup/amend/squash commit targets a commit outside the range.
241    #[error("cannot squash {commit}: its fixup/amend/squash target is outside the range")]
242    MarkerTargetOutsideRange {
243        /// Marker commit that could not be folded.
244        commit: ObjectId,
245    },
246
247    /// A `squash!` commit would lose its message without combining interactively.
248    #[error("squashing {commit} would discard its message; use --combine or -m")]
249    SquashDiscardsMessage {
250        /// Commit whose message would be dropped.
251        commit: ObjectId,
252    },
253
254    /// A planned ref no longer matches the stored value (compare-and-swap failure).
255    #[error("ref {name} changed since the rewrite plan was built")]
256    RefCasMismatch {
257        /// Ref that failed the expected-old check.
258        name: String,
259    },
260
261    /// `HEAD` no longer points at the ref or commit recorded in the plan.
262    #[error("HEAD moved since the rewrite plan was built")]
263    HeadMovedSincePlan {
264        /// Ref `HEAD` pointed at when planning, if symbolic.
265        planned_ref: Option<String>,
266        /// Commit `HEAD` pointed at when planning.
267        planned_commit: ObjectId,
268    },
269
270    /// Ref storage rejected the update batch.
271    #[error("ref update failed: {message}")]
272    RefApply {
273        /// Backend error summary.
274        message: String,
275    },
276
277    /// Index or worktree update failed after refs were already moved.
278    #[error("worktree update failed: {message}")]
279    WorktreeApply {
280        /// Underlying failure.
281        message: String,
282    },
283
284    /// No `.git/grit/history-last` record exists.
285    #[error("no history rewrite to undo")]
286    NoUndoRecord,
287
288    /// A ref or `HEAD` moved since the recorded rewrite.
289    #[error("ref {name} changed since the last history rewrite — undo is no longer safe")]
290    UndoStale {
291        /// Ref that no longer matches the recorded post-rewrite tip.
292        name: String,
293    },
294
295    /// Failed to read `.git/grit/history-last`.
296    #[error("could not read history undo record: {message}")]
297    UndoRecordRead {
298        /// Parse or I/O failure summary.
299        message: String,
300    },
301
302    /// Failed to write `.git/grit/history-last`.
303    #[error("could not write history undo record: {message}")]
304    UndoRecordWrite {
305        /// I/O or lock failure summary.
306        message: String,
307    },
308
309    /// Refs were updated but the worktree checkout did not finish (see `.git/grit/rewrite-apply/`).
310    #[error("history rewrite checkout is incomplete")]
311    RewriteApplyIncomplete,
312
313    /// Checkout failed after refs were updated; rollback or worktree restore may also have failed.
314    #[error("checkout failed after refs were updated")]
315    ApplyRollbackIncomplete {
316        /// Primary checkout failure.
317        checkout: String,
318        /// Ref rollback error, when rolling back failed.
319        ref_rollback: Option<String>,
320        /// Worktree/index restore error after checkout failed.
321        worktree_restore: Option<String>,
322    },
323}
324
325impl HistoryError {
326    /// Whether the rewrite was cancelled ([`Error::Cancelled`]).
327    #[must_use]
328    pub fn is_cancelled(&self) -> bool {
329        match self {
330            Self::Library(e) => e.is_cancelled(),
331            _ => false,
332        }
333    }
334}
335
336impl From<Error> for HistoryError {
337    /// Unwraps [`Error::History`] back to the history failure it carries; every other library
338    /// error becomes [`HistoryError::Library`].
339    fn from(err: Error) -> Self {
340        match err {
341            Error::History(inner) => inner,
342            other => Self::Library(Box::new(other)),
343        }
344    }
345}