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}