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}