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}