Skip to main content

grit_lib/history/
rebase_session.rs

1//! Stop/resume rebase sessions with Git-compatible `.git/rebase-merge/` state.
2
3use std::collections::{HashMap, HashSet};
4use std::fs;
5use std::path::Path;
6
7use serde::{Deserialize, Serialize};
8
9use crate::change_id::logical_change_id_for_commit;
10use crate::hooks::{
11    run_post_checkout, run_post_rewrite, HookName, PostOperationHooks, RewriteKind,
12};
13use crate::lockfile::{LockFile, LockWait};
14use crate::objects::ObjectId;
15use crate::operation::conflicts_from_tree_merge;
16use crate::porcelain::checkout::checkout_hard_to_tree_from_head_tree;
17use crate::porcelain::merge::apply_conflict_merge_worktree;
18use crate::porcelain::reset::{reset_to_commit, ResetMode, ResetOptions};
19use crate::porcelain::worktree_guard::{
20    ensure_safe_for_rebase_stop, ensure_worktree_clean_for_rebase_exec,
21};
22use crate::progress::{
23    poll_cancel, Cancel, NeverCancel, NullProgress, ProgressSink, UncancellableProgress,
24};
25use crate::refs;
26use crate::repo::Repository;
27use crate::state::{resolve_head_for_repository, write_orig_head, HeadState};
28use crate::write_tree::{write_tree_update_index, WriteTreeFlags};
29
30use super::apply::{apply_rewrite, log_head_move};
31use super::error::HistoryError;
32use super::last::{write_history_last, HistoryLastRecord};
33use super::plan::{
34    commit_parents, plan_rewrite_with_order_resumable_seeded, write_rebase_staged_resolution,
35    ResumablePlanOutcome,
36};
37use super::rebase::{
38    commits_in_rebase_range, plan_rebase_resumable, PreRebase, RebaseOntoOptions, RebasePlan,
39    RebaseRequest,
40};
41use super::types::RefScope;
42use super::types::{
43    AppliedRewrite, ApplyOptions, CommitEdit, HistoryEdits, PlannedRefUpdate, RewriteOptions,
44    RewritePlan, RewrittenCommit,
45};
46use super::{commit_tree, load_commit};
47use crate::operation::Conflict;
48
49/// Options for [`start_rebase`].
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct StartRebaseOptions {
52    /// Upstream, optional onto, and branch tip to rebase.
53    pub request: RebaseRequest,
54    /// Replay and cherry-detection options.
55    pub rebase: RebaseOntoOptions,
56    /// Reflog identity and message used when the rebase finishes with [`apply_rewrite`].
57    pub apply: ApplyOptions,
58    /// Whether [`start_rebase`] runs the `pre-rebase` hook. [`continue_rebase`] and
59    /// [`skip_rebase`] never run it.
60    pub pre_rebase: PreRebase,
61}
62
63/// Snapshot when a resumable rebase stops on conflicts.
64#[derive(Debug, Clone, PartialEq, Eq)]
65#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
66pub struct RebaseStop {
67    /// Original commit that could not be replayed cleanly.
68    pub commit: ObjectId,
69    /// Conflicted paths and kinds for the current step.
70    pub conflicts: Vec<Conflict>,
71    /// Source commits fully replayed before the stop (oldest first).
72    pub done: Vec<ObjectId>,
73    /// Source commits not yet replayed (including `commit`).
74    pub remaining: Vec<ObjectId>,
75    /// Rerere replay or preimage events when `rerere.enabled` is set.
76    pub rerere: Vec<crate::rerere::RerereEvent>,
77}
78
79/// Result of starting or advancing a rebase session.
80#[derive(Debug, Clone, PartialEq, Eq)]
81#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
82#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
83pub enum RebaseOutcome {
84    /// Every commit was replayed and ref updates were applied.
85    Done {
86        /// Ref movements performed on completion.
87        applied: AppliedRewrite,
88        /// Final rewrite plan (commits and ref updates).
89        plan: Box<RewritePlan>,
90        /// `HEAD` commit OIDs before this invocation and after completion, as `(old, new)`.
91        ///
92        /// For [`continue_rebase`] and [`skip_rebase`], `old` is the stopped, detached
93        /// `HEAD` at the start of that call and `new` is `applied.head` after branch
94        /// reattachment. `None` means the OID is unchanged, including when only reattaching
95        /// `HEAD`. Unlike [`RewritePlan::head_change`], this covers the entire session call.
96        head_change: Option<(ObjectId, ObjectId)>,
97        /// Rerere postimage events from concluding a conflicted step in this call.
98        rerere: Vec<crate::rerere::RerereEvent>,
99    },
100    /// `--exec` commands ran at every replayed commit (from [`start_rebase_with_exec`], or a
101    /// resumed exec stop) and the atomic plan was applied.
102    ExecDone {
103        /// Ref movements performed on completion.
104        applied: AppliedRewrite,
105        /// Final rewrite plan; boxed to keep the outcome small.
106        plan: Box<RewritePlan>,
107        /// Invocations captured during this call ([`start_rebase_with_exec`], or a
108        /// continue/skip that resumed an exec stop).
109        exec: super::ExecReport,
110    },
111    /// Replay stopped on conflicts; `.git/rebase-merge/` and grit state were written.
112    Stopped(RebaseStop),
113}
114
115/// Result of [`abort_rebase`].
116#[derive(Debug, Clone, PartialEq, Eq, Default)]
117#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
118pub struct RebaseAbort {
119    /// Branches that something else moved while the rebase was stopped and that the abort
120    /// reset to their pre-rebase tip: `old` is the tip that was overwritten (still reachable
121    /// from the branch reflog), `new` the restored pre-rebase tip. Empty when the branch was
122    /// where the rebase left it.
123    pub overwritten: Vec<PlannedRefUpdate>,
124}
125
126/// Failures while driving a rebase session.
127#[derive(Debug, thiserror::Error)]
128#[non_exhaustive]
129pub enum RebaseSessionError {
130    /// No rebase session is in progress.
131    #[error("no rebase in progress")]
132    NoRebaseInProgress,
133
134    /// A rebase is already in progress.
135    #[error("rebase is already in progress")]
136    RebaseAlreadyInProgress,
137
138    /// A rebase that another tool (normally `git`) started is in progress: its state directory
139    /// exists without grit's plan, so only that tool can continue or skip it.
140    ///
141    /// [`abort_rebase`] also returns this when it cannot restore the rebase safely (an
142    /// autostash is recorded, or `orig-head` is unreadable).
143    #[error("a rebase started by git is in progress")]
144    ForeignRebaseInProgress,
145
146    /// The index still has unmerged entries.
147    #[error("cannot continue: unmerged paths remain in the index")]
148    UnmergedPaths,
149
150    /// Nothing to commit after resolving conflicts.
151    #[error("nothing to commit after conflict resolution")]
152    NothingToCommit,
153
154    /// `HEAD` is not the recorded conflict parent and is not a descendant of it, so grit
155    /// cannot tell which commit finished the stopped pick.
156    #[error(
157        "HEAD moved away from the conflict resolution parent (expected {expected}); save commits on a branch or reset before `grit rebase --continue`"
158    )]
159    ResolutionHeadChanged {
160        /// Detached replay parent written when the rebase stopped.
161        expected: ObjectId,
162    },
163
164    /// Branch rebase requires a symbolic `HEAD`.
165    #[error("rebase session requires HEAD on a branch")]
166    NotOnBranch,
167
168    /// The caller moved HEAD away from the recorded exec stop. Save new commits on
169    /// a branch, abort, and restart instead of silently discarding them on continue.
170    #[error("exec replay HEAD changed; expected {expected}; save new commits on a branch, then abort and restart the rebase")]
171    ExecHeadChanged {
172        /// Detached replay commit at the exec stop.
173        expected: ObjectId,
174    },
175
176    /// A ref the stopped exec rebase will move (the rebased branch or one of its planned
177    /// updates) was moved by something else, so the saved plan can no longer be applied.
178    /// Nothing was changed; [`abort_rebase`] restores the pre-rebase state.
179    #[error(
180        "ref {name} moved from {expected} to {} while the rebase was stopped; abort the rebase to restore it",
181        .actual.map_or_else(|| "(deleted)".to_owned(), |oid| oid.to_hex())
182    )]
183    StaleExecPlan {
184        /// Full ref name.
185        name: String,
186        /// Tip the saved plan expects.
187        expected: ObjectId,
188        /// Current tip, or `None` when the ref was deleted.
189        actual: Option<ObjectId>,
190    },
191
192    /// The rebased branch moved again while [`abort_rebase`] was resetting it; the rebase is
193    /// still in progress and the abort can be retried.
194    #[error("ref {name} moved while aborting the rebase; retry the abort")]
195    AbortRefMoved {
196        /// Full ref name.
197        name: String,
198    },
199
200    /// Grit's saved rebase plan records a value that is not a valid object id, so the session
201    /// cannot be continued, skipped, or aborted.
202    #[error("rebase state records an invalid object id: {value:?}")]
203    InvalidStateOid {
204        /// The recorded text that failed to parse.
205        value: String,
206    },
207
208    /// Underlying history rewrite failure.
209    #[error(transparent)]
210    History(#[from] HistoryError),
211
212    /// Repository I/O or ref failure.
213    #[error(transparent)]
214    Library(#[from] crate::error::Error),
215}
216
217impl RebaseSessionError {
218    /// Whether the rebase session was cancelled ([`crate::error::Error::Cancelled`]).
219    #[must_use]
220    pub fn is_cancelled(&self) -> bool {
221        match self {
222            Self::Library(e) => e.is_cancelled(),
223            Self::History(e) => e.is_cancelled(),
224            Self::NoRebaseInProgress
225            | Self::RebaseAlreadyInProgress
226            | Self::ForeignRebaseInProgress
227            | Self::UnmergedPaths
228            | Self::NothingToCommit
229            | Self::ResolutionHeadChanged { .. }
230            | Self::NotOnBranch
231            | Self::ExecHeadChanged { .. }
232            | Self::StaleExecPlan { .. }
233            | Self::AbortRefMoved { .. }
234            | Self::InvalidStateOid { .. } => false,
235        }
236    }
237}
238
239/// Start a resumable rebase of `opts.request.branch_tip` onto the effective onto commit.
240///
241/// Unless the branch is already up to date, runs `pre-rebase` before any commit is replayed
242/// (see [`StartRebaseOptions::pre_rebase`]); a refusal returns [`RebaseSessionError::History`]
243/// wrapping [`HistoryError::Library`] with [`Error::Hook`](crate::error::Error::Hook), and
244/// nothing is changed or written.
245///
246/// On a clean replay, applies the plan atomically and returns [`RebaseOutcome::Done`].
247/// On conflict, writes Git-compatible rebase state and returns [`RebaseOutcome::Stopped`]
248/// without moving the branch ref away from its pre-rebase tip. Both record the pre-rebase tip
249/// as `ORIG_HEAD`, which finishing or aborting the rebase leaves in place; an up-to-date branch
250/// leaves `ORIG_HEAD` untouched.
251///
252/// # Errors
253///
254/// Returns [`RebaseSessionError::RebaseAlreadyInProgress`] when grit or `git` already has a
255/// rebase in progress, [`RebaseSessionError::NotOnBranch`] when `HEAD` is unborn,
256/// and planning/apply failures otherwise. When the replay would stop on conflicts but the index
257/// or worktree has local changes (or an untracked file sits where the stop checks out a path),
258/// returns [`RebaseSessionError::Library`] wrapping
259/// [`Error::DirtyWorktree`](crate::error::Error::DirtyWorktree) or the overwrite error and
260/// leaves the repository untouched, because stopping would discard that work.
261///
262/// Work-tree updates report [`crate::progress::ProgressPhase::CheckingOutFiles`] to `progress`.
263/// `cancel` (or [`ProgressSink::should_cancel`]) is checked after planning and during the apply
264/// and stop checkouts; a cancel returns [`RebaseSessionError::Library`] wrapping
265/// [`Error::Cancelled`](crate::error::Error::Cancelled) (see [`RebaseSessionError::is_cancelled`]).
266/// The last cancel point is before a conflict stop is recorded, so a cancel leaves nothing
267/// changed; once the stop state and `HEAD` are written, its checkout runs to completion and the
268/// rebase stops with its conflicts as without a cancel.
269pub fn start_rebase(
270    repo: &Repository,
271    opts: &StartRebaseOptions,
272    progress: &mut dyn ProgressSink,
273    cancel: &dyn Cancel,
274) -> Result<RebaseOutcome, RebaseSessionError> {
275    super::ensure_rewrite_apply_ready(repo)?;
276    if let Some(active) = active_rebase(repo.git_dir()) {
277        return Err(match active {
278            ActiveRebase::Foreign(_) => RebaseSessionError::ForeignRebaseInProgress,
279            ActiveRebase::Exec | ActiveRebase::Grit => RebaseSessionError::RebaseAlreadyInProgress,
280        });
281    }
282    let (head_ref, orig_head) = branch_head(repo)?;
283    let plan = plan_rebase_resumable(repo, &opts.request, &opts.rebase, &opts.pre_rebase)?;
284    poll_cancel(progress, cancel).map_err(map_lib)?;
285    match plan {
286        Err(RebasePlan::UpToDate) => Ok(up_to_date_outcome(orig_head)),
287        Err(RebasePlan::Plan(_) | RebasePlan::Conflict { .. }) => Err(HistoryError::Planning {
288            message: "unexpected non-resumable rebase plan".into(),
289        }
290        .into()),
291        Ok(ResumablePlanOutcome::Complete(plan)) => {
292            let applied =
293                apply_rewrite(repo, &plan, &opts.apply, progress, cancel).map_err(map_apply_err)?;
294            write_orig_head(repo, orig_head).map_err(map_lib)?;
295            Ok(RebaseOutcome::Done {
296                head_change: (orig_head != applied.head).then_some((orig_head, applied.head)),
297                applied,
298                plan,
299                rerere: Vec::new(),
300            })
301        }
302        Ok(ResumablePlanOutcome::Partial(partial)) => {
303            let orig_tree = commit_tree(repo, orig_head)?;
304            let stop_tree = commit_tree(repo, partial.conflict.replayed_parent)?;
305            ensure_safe_for_rebase_stop(repo, &orig_tree, &stop_tree)?;
306            let stop = install_stopped_rebase(
307                repo,
308                &head_ref,
309                orig_head,
310                &opts.request,
311                &opts.rebase,
312                &opts.apply,
313                *partial,
314                Vec::new(),
315                progress,
316                cancel,
317            )?;
318            write_orig_head(repo, orig_head).map_err(map_lib)?;
319            if opts.apply.hooks.enabled() {
320                let onto = opts.request.onto.unwrap_or(opts.request.upstream);
321                run_post_checkout(repo, Some(orig_head), onto, true);
322            }
323            Ok(RebaseOutcome::Stopped(stop))
324        }
325    }
326}
327
328/// Rebase like [`start_rebase`], running `spec.commands` at each replayed commit before any
329/// ref moves (`grit rebase --exec`).
330///
331/// [`StartRebaseOptions::pre_rebase`] runs before anything is replayed, as for [`start_rebase`].
332///
333/// The commands run through [`verify_rebase_with_exec`](super::verify_rebase_with_exec); when
334/// they all succeed, the (possibly amended) plan is applied atomically and the pre-rebase tip
335/// `opts.request.branch_tip` is recorded as `ORIG_HEAD`. When there is nothing to replay and
336/// no `--exec` commands, returns [`RebaseOutcome::Done`] with no `head_change` and leaves
337/// `ORIG_HEAD` untouched. With `--exec`, commits in the rebase range still run the commands
338/// even when the branch is already based on the upstream (matching `git rebase -x`). A replay
339/// conflict before any command runs stops like [`start_rebase`] with conflict markers and a
340/// resumable grit session; [`continue_rebase`] runs the exec commands at the resolved commit.
341///
342/// # Errors
343///
344/// Returns the `--exec` errors of
345/// [`verify_rebase_with_exec`](super::verify_rebase_with_exec)
346/// ([`HistoryError::ExecFailed`], [`HistoryError::ExecDirty`],
347/// [`HistoryError::ExecConflict`], …). The two that save a resumable stop
348/// ([`HistoryError::ExecDirty`] and [`HistoryError::ExecConflict`]) record `ORIG_HEAD` first;
349/// the others roll back and leave it alone. Planning, work-tree and apply failures propagate
350/// as for [`start_rebase`].
351pub fn start_rebase_with_exec(
352    repo: &Repository,
353    opts: &StartRebaseOptions,
354    spec: &super::ExecSpec,
355    runner: &dyn crate::command_runner::CommandRunner,
356    progress: &mut dyn ProgressSink,
357    cancel: &dyn Cancel,
358) -> Result<RebaseOutcome, RebaseSessionError> {
359    super::ensure_rewrite_apply_ready(repo)?;
360    if let Some(active) = active_rebase(repo.git_dir()) {
361        return Err(match active {
362            ActiveRebase::Foreign(_) => RebaseSessionError::ForeignRebaseInProgress,
363            ActiveRebase::Exec | ActiveRebase::Grit => RebaseSessionError::RebaseAlreadyInProgress,
364        });
365    }
366    let (head_ref, orig_head) = branch_head(repo)?;
367    let plan = plan_rebase_resumable(repo, &opts.request, &opts.rebase, &opts.pre_rebase)?;
368    poll_cancel(progress, cancel).map_err(map_lib)?;
369    match plan {
370        Err(RebasePlan::UpToDate) => {
371            if spec.commands.is_empty() {
372                return Ok(up_to_date_outcome(orig_head));
373            }
374            let fork =
375                super::rebase::rebase_fork(repo, opts.request.upstream, opts.request.branch_tip)?;
376            let effective_onto = opts.request.onto.unwrap_or(opts.request.upstream);
377            if opts.request.branch_tip == effective_onto {
378                return Ok(up_to_date_outcome(orig_head));
379            }
380            let range =
381                super::rebase::commits_in_rebase_range(repo, fork, &[opts.request.branch_tip])?;
382            if range.is_empty() {
383                return Ok(up_to_date_outcome(orig_head));
384            }
385            opts.pre_rebase.run(repo)?;
386            let mut plan = super::rebase::plan_rebase_identity_exec(
387                repo,
388                fork,
389                effective_onto,
390                &range,
391                &opts.rebase,
392            )?;
393            super::apply::verify_rewrite_plan(repo, &plan)?;
394            let exec = match super::verify_rebase_with_exec(
395                repo,
396                &mut plan,
397                spec,
398                &opts.rebase.rewrite,
399                runner,
400            ) {
401                Ok(exec) => exec,
402                Err(
403                    stop @ (HistoryError::ExecDirty { .. } | HistoryError::ExecConflict { .. }),
404                ) => {
405                    write_orig_head(repo, orig_head).map_err(map_lib)?;
406                    return Err(stop.into());
407                }
408                Err(err) => return Err(err.into()),
409            };
410            let applied =
411                apply_rewrite(repo, &plan, &opts.apply, progress, cancel).map_err(map_apply_err)?;
412            write_orig_head(repo, orig_head).map_err(map_lib)?;
413            Ok(RebaseOutcome::ExecDone {
414                applied,
415                plan: Box::new(plan),
416                exec,
417            })
418        }
419        Err(RebasePlan::Plan(_) | RebasePlan::Conflict { .. }) => Err(HistoryError::Planning {
420            message: "unexpected non-resumable rebase plan".into(),
421        }
422        .into()),
423        Ok(ResumablePlanOutcome::Partial(partial)) => {
424            let orig_tree = commit_tree(repo, orig_head)?;
425            let stop_tree = commit_tree(repo, partial.conflict.replayed_parent)?;
426            ensure_safe_for_rebase_stop(repo, &orig_tree, &stop_tree)?;
427            let stop = install_stopped_rebase(
428                repo,
429                &head_ref,
430                orig_head,
431                &opts.request,
432                &opts.rebase,
433                &opts.apply,
434                *partial,
435                spec.commands.clone(),
436                progress,
437                cancel,
438            )?;
439            write_orig_head(repo, orig_head).map_err(map_lib)?;
440            if opts.apply.hooks.enabled() {
441                let onto = opts.request.onto.unwrap_or(opts.request.upstream);
442                run_post_checkout(repo, Some(orig_head), onto, true);
443            }
444            Ok(RebaseOutcome::Stopped(stop))
445        }
446        Ok(ResumablePlanOutcome::Complete(plan)) => {
447            let mut plan = *plan;
448            super::apply::verify_rewrite_plan(repo, &plan)?;
449            let exec = match super::verify_rebase_with_exec(
450                repo,
451                &mut plan,
452                spec,
453                &opts.rebase.rewrite,
454                runner,
455            ) {
456                Ok(exec) => exec,
457                Err(
458                    stop @ (HistoryError::ExecDirty { .. } | HistoryError::ExecConflict { .. }),
459                ) => {
460                    write_orig_head(repo, orig_head).map_err(map_lib)?;
461                    return Err(stop.into());
462                }
463                Err(err) => return Err(err.into()),
464            };
465            let applied =
466                apply_rewrite(repo, &plan, &opts.apply, progress, cancel).map_err(map_apply_err)?;
467            write_orig_head(repo, orig_head).map_err(map_lib)?;
468            Ok(RebaseOutcome::ExecDone {
469                applied,
470                plan: Box::new(plan),
471                exec,
472            })
473        }
474    }
475}
476
477fn up_to_date_outcome(head: ObjectId) -> RebaseOutcome {
478    RebaseOutcome::Done {
479        head_change: None,
480        applied: AppliedRewrite {
481            ref_updates: Vec::new(),
482            head,
483            hooks_run: Vec::new(),
484        },
485        plan: Box::new(RewritePlan {
486            commits: Vec::new(),
487            ref_updates: Vec::new(),
488            head: super::types::HeadEffect::default(),
489        }),
490        rerere: Vec::new(),
491    }
492}
493
494/// When `grit-plan` predates `stopped_head`, record the detached replay parent before
495/// `grit commit` moves `HEAD` to the resolution commit.
496pub(crate) fn persist_rebase_stopped_head_if_missing(
497    repo: &Repository,
498) -> Result<(), RebaseSessionError> {
499    if !crate::state::grit_rebase_conflict_stop_active(repo.git_dir()) {
500        return Ok(());
501    }
502    let path = repo.git_dir().join("rebase-merge/grit-plan");
503    let raw = fs::read_to_string(&path).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
504    let mut parsed: PersistedRebaseState =
505        serde_json::from_str(&raw).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
506    if parsed.stopped_head.is_some() {
507        return Ok(());
508    }
509    let head = resolve_head_oid(repo)?;
510    parsed.stopped_head = Some(oid_hex(&head));
511    write_persisted_state(repo.git_dir(), &parsed)
512}
513
514fn replay_base_parent(state: &LoadedState, oid: ObjectId) -> bool {
515    oid == state.onto || state.mapping.values().any(|mapped| *mapped == oid)
516}
517
518fn stopped_head_for_continue(
519    state: &LoadedState,
520    repo: &Repository,
521) -> Result<ObjectId, RebaseSessionError> {
522    if let Some(oid) = state.stopped_head {
523        return Ok(oid);
524    }
525    let head = resolve_head_oid(repo)?;
526    if replay_base_parent(state, head) {
527        return Ok(head);
528    }
529    let head_commit = load_commit(repo, head)?;
530    if head_commit.parents.len() == 1 {
531        if let Some(&parent) = head_commit.parents.first() {
532            if replay_base_parent(state, parent) {
533                return Ok(parent);
534            }
535        }
536    } else {
537        for &parent in &head_commit.parents {
538            if replay_base_parent(state, parent) {
539                return Ok(parent);
540            }
541        }
542    }
543    Err(RebaseSessionError::NoRebaseInProgress)
544}
545
546fn is_first_parent_descendant_of(
547    repo: &Repository,
548    ancestor: ObjectId,
549    tip: ObjectId,
550) -> Result<bool, RebaseSessionError> {
551    if tip == ancestor {
552        return Ok(false);
553    }
554    let mut current = Some(tip);
555    while let Some(oid) = current {
556        let parents = commit_parents(repo, oid).map_err(RebaseSessionError::from)?;
557        let Some(first) = parents.first().copied() else {
558            break;
559        };
560        if first == ancestor {
561            return Ok(true);
562        }
563        current = Some(first);
564    }
565    Ok(false)
566}
567
568fn run_exec_after_resolution(
569    repo: &Repository,
570    state: &LoadedState,
571    head_oid: ObjectId,
572) -> Result<ObjectId, RebaseSessionError> {
573    if state.exec_commands.is_empty() {
574        return Ok(head_oid);
575    }
576    let spec = super::ExecSpec {
577        commands: state.exec_commands.clone(),
578    };
579    let runner = repo.command_runner();
580    let (tip, _) = super::exec::run_exec_at_tip(repo, &spec, runner.as_ref(), head_oid)?;
581    Ok(tip)
582}
583
584#[allow(clippy::too_many_arguments)]
585fn advance_after_user_resolution_commit(
586    repo: &Repository,
587    state: &LoadedState,
588    opts: &StartRebaseOptions,
589    current: ObjectId,
590    head_oid: ObjectId,
591    rerere: Vec<crate::rerere::RerereEvent>,
592    progress: &mut dyn ProgressSink,
593    cancel: &dyn Cancel,
594) -> Result<RebaseOutcome, RebaseSessionError> {
595    let head_oid = run_exec_after_resolution(repo, state, head_oid)?;
596    let current_data = load_commit(repo, current)?;
597    let mut mapping = state.mapping.clone();
598    mapping.insert(current, head_oid);
599    let mut rewritten = state.rewritten.clone();
600    rewritten.push(RewrittenCommit {
601        old: current,
602        new: Some(head_oid),
603        change_id: logical_change_id_for_commit(repo, head_oid).map_err(map_lib)?,
604        subject: super::subject::subject_for_rewrite_row(repo, &current_data, Some(head_oid))
605            .map_err(RebaseSessionError::from)?,
606        reused: false,
607    });
608    let resume_order: Vec<ObjectId> = replay_order_for_resume(state, current)
609        .into_iter()
610        .filter(|commit| *commit != current)
611        .collect();
612    let mut edits = state.edits.clone();
613    edits.retain(|commit, _| resume_order.contains(commit));
614    let rewrite = replan_rewrite_options(state);
615    let outcome = plan_rewrite_with_order_resumable_seeded(
616        repo,
617        &edits,
618        &rewrite,
619        Some(&resume_order),
620        state.flatten_leaf_merges.then_some(state.onto),
621        state.rebase.favor,
622        Some((mapping, rewritten)),
623        Some(state.scoped_refs.clone()),
624    )?;
625    advance_after_plan(
626        repo,
627        state,
628        &opts.apply,
629        outcome,
630        head_oid,
631        rerere,
632        progress,
633        cancel,
634    )
635}
636
637/// Finish the current conflicted commit and replay remaining commits.
638///
639/// For an exec stop, requires a clean index/worktree at the recorded replay HEAD,
640/// then resumes at the next command (the successful dirty command is not rerun).
641/// Save exec edits with a stash before continuing. A moved HEAD is rejected;
642/// save new commits on a branch, abort, and restart in that case. For an exec replay
643/// conflict ([`HistoryError::ExecConflict`]), commits the staged resolution and runs the
644/// commands at it. Later replays and new commits use the rewrite options saved when the
645/// exec stop was recorded, not `opts.rebase.rewrite`.
646///
647/// # Errors
648///
649/// Returns [`RebaseSessionError::UnmergedPaths`] when the index still records conflicts,
650/// [`RebaseSessionError::ForeignRebaseInProgress`] when `git` started the rebase,
651/// [`RebaseSessionError::NoRebaseInProgress`] when no rebase is in progress, and, for an
652/// exec stop, [`RebaseSessionError::StaleExecPlan`] (changing nothing) when the rebased branch
653/// or a ref the plan updates was moved during the stop.
654/// Cancellation follows [`start_rebase`]: a cancel before the replayed commits are applied
655/// keeps the rebase stopped where it was.
656pub fn continue_rebase(
657    repo: &Repository,
658    opts: &StartRebaseOptions,
659    progress: &mut dyn ProgressSink,
660    cancel: &dyn Cancel,
661) -> Result<RebaseOutcome, RebaseSessionError> {
662    if matches!(active_rebase(repo.git_dir()), Some(ActiveRebase::Exec)) {
663        return super::exec::resume_exec_rebase(repo, &opts.apply, false, progress, cancel);
664    }
665    super::ensure_rewrite_apply_ready(repo)?;
666    let state = load_persisted_state(repo.git_dir())?;
667    if repo.load_index()?.has_unmerged_entries() {
668        return Err(RebaseSessionError::UnmergedPaths);
669    }
670    let rerere = crate::rerere::finish_merge_rerere(repo).map_err(map_lib)?;
671    let current = state
672        .stopped_commit
673        .or_else(|| state.remaining.first().copied())
674        .ok_or(RebaseSessionError::NoRebaseInProgress)?;
675    let new_tree = {
676        let mut index = repo.load_index()?;
677        write_tree_update_index(repo.odb(), &mut index, "", WriteTreeFlags::silent())
678            .map_err(map_lib)?
679    };
680    let head_oid = resolve_head_oid(repo)?;
681    let head_tree = commit_tree(repo, head_oid)?;
682    if new_tree == head_tree {
683        let stopped_head = stopped_head_for_continue(&state, repo)?;
684        if head_oid == stopped_head {
685            return Err(RebaseSessionError::NothingToCommit);
686        }
687        if is_first_parent_descendant_of(repo, stopped_head, head_oid)? {
688            ensure_worktree_clean_for_rebase_exec(repo).map_err(map_lib)?;
689            return advance_after_user_resolution_commit(
690                repo, &state, opts, current, head_oid, rerere, progress, cancel,
691            );
692        }
693        return Err(RebaseSessionError::ResolutionHeadChanged {
694            expected: stopped_head,
695        });
696    }
697
698    let original_parents = commit_parents(repo, current).map_err(RebaseSessionError::from)?;
699    let parents = if original_parents.len() <= 1 {
700        vec![head_oid]
701    } else {
702        let mut mapped = vec![head_oid];
703        mapped.extend(
704            original_parents[1..]
705                .iter()
706                .map(|parent| state.mapping.get(parent).copied().unwrap_or(*parent)),
707        );
708        mapped
709    };
710
711    // Merge-preserving replay with side branches before mainline needs the resolution
712    // recorded as a real commit before resuming; a single-commit stop can plan the staged
713    // tree directly (matches the linear `git rebase --continue` path).
714    if state.remaining.len() > 1 {
715        let stopped_head = stopped_head_for_continue(&state, repo)?;
716        if head_oid != stopped_head {
717            return Err(RebaseSessionError::ResolutionHeadChanged {
718                expected: stopped_head,
719            });
720        }
721        let resolved =
722            write_rebase_staged_resolution(repo, &state.rewrite, current, new_tree, &parents)
723                .map_err(RebaseSessionError::from)?;
724        repo.odb().flush_for_publish().map_err(map_lib)?;
725        refs::update_direct_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", &resolved)
726            .map_err(map_lib)?;
727        ensure_worktree_clean_for_rebase_exec(repo).map_err(map_lib)?;
728        return advance_after_user_resolution_commit(
729            repo, &state, opts, current, resolved, rerere, progress, cancel,
730        );
731    }
732
733    if !state.exec_commands.is_empty() && new_tree != head_tree {
734        let stopped_head = stopped_head_for_continue(&state, repo)?;
735        if head_oid != stopped_head {
736            return Err(RebaseSessionError::ResolutionHeadChanged {
737                expected: stopped_head,
738            });
739        }
740        let resolved =
741            write_rebase_staged_resolution(repo, &state.rewrite, current, new_tree, &parents)
742                .map_err(RebaseSessionError::from)?;
743        repo.odb().flush_for_publish().map_err(map_lib)?;
744        refs::update_direct_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", &resolved)
745            .map_err(map_lib)?;
746        ensure_worktree_clean_for_rebase_exec(repo).map_err(map_lib)?;
747        return advance_after_user_resolution_commit(
748            repo, &state, opts, current, resolved, rerere, progress, cancel,
749        );
750    }
751
752    let resume_order = replay_order_for_resume(&state, current);
753    let mut edits = state.edits.clone();
754    edits.insert(
755        current,
756        CommitEdit::Rewrite {
757            tree: Some(new_tree),
758            message: None,
759            author: None,
760            parents: Some(parents),
761        },
762    );
763    edits.retain(|commit, _| resume_order.contains(commit));
764
765    let seed = (state.mapping.clone(), state.rewritten.clone());
766    let rewrite = replan_rewrite_options(&state);
767    let outcome = plan_rewrite_with_order_resumable_seeded(
768        repo,
769        &edits,
770        &rewrite,
771        Some(&resume_order),
772        state.flatten_leaf_merges.then_some(state.onto),
773        state.rebase.favor,
774        Some(seed),
775        Some(state.scoped_refs.clone()),
776    )?;
777    advance_after_plan(
778        repo,
779        &state,
780        &opts.apply,
781        outcome,
782        head_oid,
783        rerere,
784        progress,
785        cancel,
786    )
787}
788
789/// Skip the current todo commit and replay the rest.
790///
791/// For an exec stop, requires the edits to be saved or removed first, then skips
792/// remaining commands at that replay commit; the already replayed commit is kept.
793/// For an exec replay conflict, drops the conflicted commit and runs that commit's
794/// commands at the current `HEAD`, as Git's todo list would.
795///
796/// # Errors
797///
798/// Returns [`RebaseSessionError::ForeignRebaseInProgress`] when `git` started the rebase,
799/// [`RebaseSessionError::NoRebaseInProgress`] when no rebase is in progress,
800/// [`RebaseSessionError::StaleExecPlan`] as for [`continue_rebase`], and
801/// planning/apply failures otherwise. Cancellation follows [`continue_rebase`].
802pub fn skip_rebase(
803    repo: &Repository,
804    opts: &StartRebaseOptions,
805    progress: &mut dyn ProgressSink,
806    cancel: &dyn Cancel,
807) -> Result<RebaseOutcome, RebaseSessionError> {
808    if matches!(active_rebase(repo.git_dir()), Some(ActiveRebase::Exec)) {
809        return super::exec::resume_exec_rebase(repo, &opts.apply, true, progress, cancel);
810    }
811    let state = load_persisted_state(repo.git_dir())?;
812    let current = state
813        .remaining
814        .first()
815        .copied()
816        .ok_or(RebaseSessionError::NoRebaseInProgress)?;
817    let head_oid = resolve_head_oid(repo)?;
818    reset_index_worktree_to_head(repo, head_oid, progress, cancel)?;
819
820    let replayed_parent = run_exec_after_resolution(repo, &state, head_oid)?;
821    let mut mapping = state.mapping.clone();
822    mapping.insert(current, replayed_parent);
823    let mut rewritten = state.rewritten.clone();
824    rewritten.push(RewrittenCommit {
825        old: current,
826        new: None,
827        change_id: None,
828        subject: None,
829        reused: false,
830    });
831
832    let resume_order: Vec<ObjectId> = state
833        .remaining
834        .iter()
835        .copied()
836        .filter(|c| *c != current)
837        .collect();
838    let mut edits = state.edits.clone();
839    edits.retain(|commit, _| resume_order.contains(commit));
840    let rewrite = replan_rewrite_options(&state);
841    let outcome = plan_rewrite_with_order_resumable_seeded(
842        repo,
843        &edits,
844        &rewrite,
845        Some(&resume_order),
846        state.flatten_leaf_merges.then_some(state.onto),
847        state.rebase.favor,
848        Some((mapping, rewritten)),
849        Some(state.scoped_refs.clone()),
850    )?;
851    advance_after_plan(
852        repo,
853        &state,
854        &opts.apply,
855        outcome,
856        head_oid,
857        Vec::new(),
858        progress,
859        cancel,
860    )
861}
862
863/// Cancel the in-progress rebase and restore the branch to `orig-head`.
864///
865/// Also cancels a rebase that `git` started (`rebase-merge/` or `rebase-apply/` without grit's
866/// plan): `HEAD` is re-attached to the rebased branch (or detached, when the rebase started
867/// detached), the index and worktree are reset to `orig-head`, and the state directory is
868/// removed. Either way, unresolved rerere records for the abandoned conflict are cleared
869/// (`MERGE_RR`), as `git rebase --abort` does.
870///
871/// The branch is restored to `orig-head` even when something else (another worktree, a
872/// `git branch -f`, an amend on the checked-out branch) moved it while the rebase was stopped;
873/// such movements are listed in [`RebaseAbort::overwritten`] and logged as `rebase (abort)`
874/// in the branch reflog with `identity`. `identity` also signs the reflog entries of the reset.
875///
876/// # Errors
877///
878/// Returns [`RebaseSessionError::NoRebaseInProgress`] when no rebase is in progress,
879/// [`RebaseSessionError::ForeignRebaseInProgress`] when a git-started rebase recorded an
880/// autostash or has no readable `orig-head` (finish those with `git rebase --abort`), and
881/// [`RebaseSessionError::AbortRefMoved`] when, for an exec stop, the branch moves again between
882/// reading and resetting it (nothing is restored and the abort can be retried).
883pub fn abort_rebase(
884    repo: &Repository,
885    identity: &crate::ident::Signature,
886) -> Result<RebaseAbort, RebaseSessionError> {
887    identity
888        .validate()
889        .map_err(|err| map_lib(crate::error::Error::InvalidSignature(err)))?;
890    let had_journal = super::rewrite_apply_journal_active(repo.git_dir());
891    if had_journal {
892        super::abort_interrupted_rewrite_apply(repo)?;
893    }
894    match active_rebase(repo.git_dir()) {
895        None => {
896            if had_journal {
897                Ok(RebaseAbort::default())
898            } else {
899                Err(RebaseSessionError::NoRebaseInProgress)
900            }
901        }
902        Some(ActiveRebase::Foreign(dir)) => abort_foreign_rebase(repo, &dir, identity),
903        Some(ActiveRebase::Exec) => super::exec::abort_exec_rebase(repo, identity),
904        Some(ActiveRebase::Grit) => {
905            let state = load_persisted_state(repo.git_dir())?;
906            crate::rerere::rerere_clear(repo.git_dir()).map_err(map_lib)?;
907            let abort = if rebase_head_is_branch(&state.head_ref) {
908                hard_reset_branch(repo, &state.head_ref, state.orig_head, identity)?
909            } else {
910                hard_reset_head(repo, state.orig_head, identity)?;
911                RebaseAbort::default()
912            };
913            clear_rebase_state(repo.git_dir())?;
914            Ok(abort)
915        }
916    }
917}
918
919fn abort_foreign_rebase(
920    repo: &Repository,
921    state_dir: &Path,
922    identity: &crate::ident::Signature,
923) -> Result<RebaseAbort, RebaseSessionError> {
924    if state_dir.join("autostash").exists() {
925        return Err(RebaseSessionError::ForeignRebaseInProgress);
926    }
927    let orig_head = first_line(&state_dir.join("orig-head"))
928        .and_then(|hex| ObjectId::from_hex(&hex).ok())
929        .ok_or(RebaseSessionError::ForeignRebaseInProgress)?;
930    crate::rerere::rerere_clear(repo.git_dir()).map_err(map_lib)?;
931    let abort =
932        match first_line(&state_dir.join("head-name")).filter(|name| name.starts_with("refs/")) {
933            Some(head_ref) => hard_reset_branch(repo, &head_ref, orig_head, identity)?,
934            None => {
935                hard_reset_head(repo, orig_head, identity)?;
936                RebaseAbort::default()
937            }
938        };
939    for name in ["REBASE_HEAD", "AUTO_MERGE", "MERGE_MSG"] {
940        remove_if_exists(&repo.git_dir().join(name))?;
941    }
942    fs::remove_dir_all(state_dir).map_err(map_lib)?;
943    Ok(abort)
944}
945
946/// Which tool owns the rebase state under the git directory.
947enum ActiveRebase {
948    /// Successful exec left tracked edits at a replay commit.
949    Exec,
950    /// `rebase-merge/grit-plan` exists.
951    Grit,
952    /// `rebase-merge/` or `rebase-apply/` (not an `am`) without grit's plan.
953    Foreign(std::path::PathBuf),
954}
955
956fn active_rebase(git_dir: &Path) -> Option<ActiveRebase> {
957    let merge = git_dir.join("rebase-merge");
958    if merge.join("grit-exec").is_file() {
959        return Some(ActiveRebase::Exec);
960    }
961    if merge.join("grit-plan").is_file() {
962        return Some(ActiveRebase::Grit);
963    }
964    if merge.is_dir() {
965        return Some(ActiveRebase::Foreign(merge));
966    }
967    let apply = git_dir.join("rebase-apply");
968    (apply.is_dir() && !apply.join("applying").exists()).then_some(ActiveRebase::Foreign(apply))
969}
970
971fn first_line(path: &Path) -> Option<String> {
972    let raw = fs::read_to_string(path).ok()?;
973    let line = raw.lines().next()?.trim();
974    (!line.is_empty()).then(|| line.to_owned())
975}
976
977fn remove_if_exists(path: &Path) -> Result<(), RebaseSessionError> {
978    match fs::remove_file(path) {
979        Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(map_lib(e)),
980        _ => Ok(()),
981    }
982}
983
984// --- persisted session ---
985
986#[derive(Debug, Clone, Serialize, Deserialize)]
987struct PersistedRebaseState {
988    mapping: HashMap<String, String>,
989    rewritten: Vec<PersistedRewrittenRow>,
990    edits: Vec<PersistedEdit>,
991    rewrite: RewriteOptions,
992    rebase: RebaseOntoOptions,
993    apply: ApplyOptions,
994    replay_order: Vec<String>,
995    remaining: Vec<String>,
996    done: Vec<String>,
997    fork: String,
998    onto: String,
999    upstream: String,
1000    head_ref: String,
1001    orig_head: String,
1002    branch_tip: String,
1003    flatten_leaf_merges: bool,
1004    scoped_refs: Vec<(String, String)>,
1005    /// Detached `HEAD` at the conflict stop (`conflict.replayed_parent`).
1006    #[serde(default)]
1007    stopped_head: Option<String>,
1008    /// Original commit being replayed when the rebase stopped (`partial.conflict.commit`).
1009    #[serde(default)]
1010    stopped_commit: Option<String>,
1011    /// `--exec` commands to run at each replayed commit when resuming (empty when absent).
1012    #[serde(default)]
1013    exec_commands: Vec<String>,
1014}
1015
1016#[derive(Debug, Clone, Serialize, Deserialize)]
1017struct PersistedRewrittenRow {
1018    old: String,
1019    new: Option<String>,
1020    #[serde(default)]
1021    reused: bool,
1022}
1023
1024#[derive(Debug, Clone, Serialize, Deserialize)]
1025enum PersistedEdit {
1026    Reparent {
1027        commit: String,
1028        parent: String,
1029    },
1030    Reuse {
1031        commit: String,
1032        existing: String,
1033        /// Absent in sessions saved before skipped commits carried their continuation parent.
1034        #[serde(default)]
1035        parent: Option<String>,
1036    },
1037    Drop {
1038        commit: String,
1039    },
1040    Rewrite {
1041        commit: String,
1042        tree: Option<String>,
1043        message: Option<String>,
1044        author: Option<String>,
1045    },
1046}
1047
1048fn oid_hex(oid: &ObjectId) -> String {
1049    oid.to_hex()
1050}
1051
1052fn parse_oid(hex: &str) -> Result<ObjectId, RebaseSessionError> {
1053    ObjectId::from_hex(hex).map_err(|_| RebaseSessionError::InvalidStateOid {
1054        value: hex.to_owned(),
1055    })
1056}
1057
1058fn edits_to_persist(edits: &HistoryEdits) -> Vec<PersistedEdit> {
1059    edits
1060        .iter()
1061        .map(|(commit, edit)| match edit {
1062            CommitEdit::Reparent { parent } => PersistedEdit::Reparent {
1063                commit: oid_hex(commit),
1064                parent: oid_hex(parent),
1065            },
1066            CommitEdit::Reuse { existing, parent } => PersistedEdit::Reuse {
1067                commit: oid_hex(commit),
1068                existing: oid_hex(existing),
1069                parent: Some(oid_hex(parent)),
1070            },
1071            CommitEdit::Drop => PersistedEdit::Drop {
1072                commit: oid_hex(commit),
1073            },
1074            CommitEdit::Rewrite {
1075                tree,
1076                message,
1077                author,
1078                parents: _,
1079            } => PersistedEdit::Rewrite {
1080                commit: oid_hex(commit),
1081                tree: tree.as_ref().map(oid_hex),
1082                message: message.clone(),
1083                author: author.clone(),
1084            },
1085            CommitEdit::InsertBefore(_) | CommitEdit::SplitBefore { .. } => PersistedEdit::Drop {
1086                commit: oid_hex(commit),
1087            },
1088        })
1089        .collect()
1090}
1091
1092fn edits_from_persist(rows: &[PersistedEdit]) -> Result<HistoryEdits, RebaseSessionError> {
1093    let mut edits = HistoryEdits::new();
1094    for row in rows {
1095        match row {
1096            PersistedEdit::Reparent { commit, parent } => {
1097                edits.insert(
1098                    parse_oid(commit)?,
1099                    CommitEdit::Reparent {
1100                        parent: parse_oid(parent)?,
1101                    },
1102                );
1103            }
1104            PersistedEdit::Reuse {
1105                commit,
1106                existing,
1107                parent,
1108            } => {
1109                let existing = parse_oid(existing)?;
1110                let parent = parent.as_deref().map(parse_oid).transpose()?;
1111                edits.insert(
1112                    parse_oid(commit)?,
1113                    CommitEdit::Reuse {
1114                        existing,
1115                        parent: parent.unwrap_or(existing),
1116                    },
1117                );
1118            }
1119            PersistedEdit::Drop { commit } => {
1120                edits.insert(parse_oid(commit)?, CommitEdit::Drop);
1121            }
1122            PersistedEdit::Rewrite {
1123                commit,
1124                tree,
1125                message,
1126                author,
1127            } => {
1128                edits.insert(
1129                    parse_oid(commit)?,
1130                    CommitEdit::Rewrite {
1131                        tree: tree.as_deref().map(parse_oid).transpose()?,
1132                        message: message.clone(),
1133                        author: author.clone(),
1134                        parents: None,
1135                    },
1136                );
1137            }
1138        }
1139    }
1140    Ok(edits)
1141}
1142
1143fn mapping_to_persist(map: &HashMap<ObjectId, ObjectId>) -> HashMap<String, String> {
1144    map.iter().map(|(k, v)| (oid_hex(k), oid_hex(v))).collect()
1145}
1146
1147fn mapping_from_persist(
1148    map: &HashMap<String, String>,
1149) -> Result<HashMap<ObjectId, ObjectId>, RebaseSessionError> {
1150    map.iter()
1151        .map(|(k, v)| Ok((parse_oid(k)?, parse_oid(v)?)))
1152        .collect()
1153}
1154
1155/// Commits still to replay when resuming after a stop (not the full initial replay order).
1156fn replay_order_for_resume(state: &LoadedState, current: ObjectId) -> Vec<ObjectId> {
1157    if let Some(pos) = state.replay_order.iter().position(|c| *c == current) {
1158        return state.replay_order[pos..].to_vec();
1159    }
1160    let mut order = if state.remaining.is_empty() {
1161        state.replay_order.clone()
1162    } else {
1163        state.remaining.clone()
1164    };
1165    if !order.contains(&current) {
1166        order.insert(0, current);
1167    } else if order.first() != Some(&current) {
1168        order.retain(|c| *c != current);
1169        order.insert(0, current);
1170    }
1171    order
1172}
1173
1174#[allow(clippy::too_many_arguments)]
1175fn state_from_partial(
1176    partial: &super::plan::PartialRewritePlan,
1177    head_ref: &str,
1178    orig_head: ObjectId,
1179    request: &RebaseRequest,
1180    rebase: &RebaseOntoOptions,
1181    apply: &ApplyOptions,
1182    effective_onto: ObjectId,
1183    fork: ObjectId,
1184) -> PersistedRebaseState {
1185    let conflict = partial.conflict.commit;
1186    let stop_idx = partial
1187        .commits_to_replay
1188        .iter()
1189        .position(|c| *c == conflict)
1190        .unwrap_or(0);
1191    let done: Vec<ObjectId> = partial.commits_to_replay[..stop_idx]
1192        .iter()
1193        .copied()
1194        .filter(|c| partial.mapping.contains_key(c))
1195        .collect();
1196    let remaining: Vec<ObjectId> = partial.commits_to_replay[stop_idx..].to_vec();
1197    PersistedRebaseState {
1198        mapping: mapping_to_persist(&partial.mapping),
1199        rewritten: partial
1200            .rewritten
1201            .iter()
1202            .map(|r| PersistedRewrittenRow {
1203                old: oid_hex(&r.old),
1204                new: r.new.as_ref().map(oid_hex),
1205                reused: r.reused,
1206            })
1207            .collect(),
1208        edits: edits_to_persist(&partial.edits),
1209        rewrite: partial.opts.clone(),
1210        rebase: rebase.clone(),
1211        apply: apply.clone(),
1212        replay_order: partial.commits_to_replay.iter().map(oid_hex).collect(),
1213        remaining: remaining.iter().map(oid_hex).collect(),
1214        done: done.iter().map(oid_hex).collect(),
1215        fork: oid_hex(&fork),
1216        onto: oid_hex(&effective_onto),
1217        upstream: oid_hex(&request.upstream),
1218        head_ref: head_ref.to_owned(),
1219        orig_head: oid_hex(&orig_head),
1220        branch_tip: oid_hex(&request.branch_tip),
1221        flatten_leaf_merges: partial.linearize_onto.is_some(),
1222        scoped_refs: partial
1223            .scoped_refs
1224            .iter()
1225            .map(|(n, o)| (n.clone(), oid_hex(o)))
1226            .collect(),
1227        stopped_head: None,
1228        stopped_commit: Some(oid_hex(&conflict)),
1229        exec_commands: Vec::new(),
1230    }
1231}
1232
1233struct LoadedState {
1234    mapping: HashMap<ObjectId, ObjectId>,
1235    rewritten: Vec<RewrittenCommit>,
1236    edits: HistoryEdits,
1237    rewrite: RewriteOptions,
1238    rebase: RebaseOntoOptions,
1239    replay_order: Vec<ObjectId>,
1240    remaining: Vec<ObjectId>,
1241    head_ref: String,
1242    orig_head: ObjectId,
1243    onto: ObjectId,
1244    upstream: ObjectId,
1245    branch_tip: ObjectId,
1246    flatten_leaf_merges: bool,
1247    scoped_refs: Vec<(String, ObjectId)>,
1248    stopped_head: Option<ObjectId>,
1249    stopped_commit: Option<ObjectId>,
1250    exec_commands: Vec<String>,
1251}
1252
1253fn load_persisted_state(git_dir: &Path) -> Result<LoadedState, RebaseSessionError> {
1254    match active_rebase(git_dir) {
1255        None => return Err(RebaseSessionError::NoRebaseInProgress),
1256        Some(ActiveRebase::Foreign(_)) => return Err(RebaseSessionError::ForeignRebaseInProgress),
1257        Some(ActiveRebase::Exec) => return Err(RebaseSessionError::NoRebaseInProgress),
1258        Some(ActiveRebase::Grit) => {}
1259    }
1260    let path = git_dir.join("rebase-merge").join("grit-plan");
1261    let raw = fs::read_to_string(&path).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
1262    let parsed: PersistedRebaseState =
1263        serde_json::from_str(&raw).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
1264    Ok(LoadedState {
1265        mapping: mapping_from_persist(&parsed.mapping)?,
1266        rewritten: parsed
1267            .rewritten
1268            .iter()
1269            .map(|r| {
1270                Ok(RewrittenCommit {
1271                    old: parse_oid(&r.old)?,
1272                    new: r.new.as_deref().map(parse_oid).transpose()?,
1273                    change_id: None,
1274                    subject: None,
1275                    reused: r.reused,
1276                })
1277            })
1278            .collect::<Result<_, RebaseSessionError>>()?,
1279        edits: edits_from_persist(&parsed.edits)?,
1280        rewrite: parsed.rewrite,
1281        rebase: parsed.rebase,
1282        replay_order: parsed
1283            .replay_order
1284            .iter()
1285            .map(|h| parse_oid(h))
1286            .collect::<Result<_, _>>()?,
1287        remaining: parsed
1288            .remaining
1289            .iter()
1290            .map(|h| parse_oid(h))
1291            .collect::<Result<_, _>>()?,
1292        head_ref: parsed.head_ref,
1293        orig_head: parse_oid(&parsed.orig_head)?,
1294        onto: parse_oid(&parsed.onto)?,
1295        upstream: parse_oid(&parsed.upstream)?,
1296        branch_tip: parse_oid(&parsed.branch_tip)?,
1297        flatten_leaf_merges: parsed.flatten_leaf_merges,
1298        scoped_refs: parsed
1299            .scoped_refs
1300            .iter()
1301            .map(|(name, hex)| parse_oid(hex).map(|oid| (name.clone(), oid)))
1302            .collect::<Result<_, _>>()?,
1303        stopped_head: parsed.stopped_head.as_deref().map(parse_oid).transpose()?,
1304        stopped_commit: parsed
1305            .stopped_commit
1306            .as_deref()
1307            .map(parse_oid)
1308            .transpose()?,
1309        exec_commands: parsed.exec_commands.clone(),
1310    })
1311}
1312
1313fn write_persisted_state(
1314    git_dir: &Path,
1315    state: &PersistedRebaseState,
1316) -> Result<(), RebaseSessionError> {
1317    let dir = git_dir.join("rebase-merge");
1318    fs::create_dir_all(&dir).map_err(map_lib)?;
1319    let lock_path = dir.join("grit-plan.lock");
1320    let mut lock = LockFile::acquire(&lock_path, LockWait::Immediately)
1321        .map_err(|e| RebaseSessionError::Library(crate::error::Error::Message(e.to_string())))?;
1322    let body = serde_json::to_vec_pretty(state).map_err(map_json)?;
1323    lock.write_all(&body).map_err(map_lib)?;
1324    lock.commit_to(&dir.join("grit-plan"))
1325        .map_err(|e| RebaseSessionError::Library(crate::error::Error::Message(e.to_string())))?;
1326    Ok(())
1327}
1328
1329#[allow(clippy::too_many_arguments)]
1330fn install_stopped_rebase(
1331    repo: &Repository,
1332    head_ref: &str,
1333    orig_head: ObjectId,
1334    request: &RebaseRequest,
1335    rebase_opts: &RebaseOntoOptions,
1336    apply: &ApplyOptions,
1337    partial: super::plan::PartialRewritePlan,
1338    exec_commands: Vec<String>,
1339    progress: &mut dyn ProgressSink,
1340    cancel: &dyn Cancel,
1341) -> Result<RebaseStop, RebaseSessionError> {
1342    poll_cancel(progress, cancel).map_err(map_lib)?;
1343    let effective_onto = request.onto.unwrap_or(request.upstream);
1344    let fork = super::rebase::rebase_fork(repo, request.upstream, request.branch_tip)?;
1345    let mut persisted = state_from_partial(
1346        &partial,
1347        head_ref,
1348        orig_head,
1349        request,
1350        rebase_opts,
1351        apply,
1352        effective_onto,
1353        fork,
1354    );
1355    let conflict = partial.conflict;
1356    persisted.stopped_head = Some(oid_hex(&conflict.replayed_parent));
1357    persisted.exec_commands = exec_commands;
1358    let done = persisted
1359        .done
1360        .iter()
1361        .map(|h| parse_oid(h))
1362        .collect::<Result<Vec<_>, _>>()?;
1363    let remaining = persisted
1364        .remaining
1365        .iter()
1366        .map(|h| parse_oid(h))
1367        .collect::<Result<Vec<_>, _>>()?;
1368    let git_progress = git_visible_rebase_progress(
1369        repo,
1370        fork,
1371        request.branch_tip,
1372        &rebase_opts.rewrite,
1373        &done,
1374        &remaining,
1375        conflict.commit,
1376    )?;
1377    write_git_rebase_merge_files(
1378        repo,
1379        head_ref,
1380        orig_head,
1381        effective_onto,
1382        &git_progress.done,
1383        &git_progress.remaining,
1384        git_progress.stop.as_ref(),
1385    )?;
1386    write_persisted_state(repo.git_dir(), &persisted)?;
1387
1388    repo.odb().flush_for_publish().map_err(map_lib)?;
1389    refs::update_direct_ref_on_store(
1390        repo.refs(),
1391        repo.git_dir(),
1392        "HEAD",
1393        &conflict.replayed_parent,
1394    )
1395    .map_err(map_lib)?;
1396    // With the stop state and HEAD published, a half-installed stop would let continue commit
1397    // without the conflict stages, so the checkouts below run to completion.
1398    let mut progress = UncancellableProgress::new(progress);
1399    reset_index_worktree_to_head(repo, conflict.replayed_parent, &mut progress, &NeverCancel)?;
1400    let ours_tree = commit_tree(repo, conflict.replayed_parent)?;
1401    apply_conflict_merge_worktree(
1402        repo,
1403        &ours_tree,
1404        &conflict.merge,
1405        &mut progress,
1406        &NeverCancel,
1407    )
1408    .map_err(map_lib)?;
1409
1410    let message = load_commit(repo, conflict.commit)?.message;
1411    fs::write(repo.git_dir().join("MERGE_MSG"), &message).map_err(map_lib)?;
1412
1413    let rerere = crate::rerere::repo_rerere(repo, crate::rerere::RerereAutoupdate::FromConfig)
1414        .map_err(map_lib)?;
1415
1416    Ok(RebaseStop {
1417        commit: conflict.commit,
1418        conflicts: conflicts_from_tree_merge(&conflict.merge),
1419        done,
1420        remaining,
1421        rerere,
1422    })
1423}
1424
1425#[allow(clippy::too_many_arguments)]
1426fn advance_after_plan(
1427    repo: &Repository,
1428    prev: &LoadedState,
1429    apply: &ApplyOptions,
1430    outcome: ResumablePlanOutcome,
1431    head_before: ObjectId,
1432    rerere: Vec<crate::rerere::RerereEvent>,
1433    progress: &mut dyn ProgressSink,
1434    cancel: &dyn Cancel,
1435) -> Result<RebaseOutcome, RebaseSessionError> {
1436    poll_cancel(progress, cancel).map_err(map_lib)?;
1437    match outcome {
1438        ResumablePlanOutcome::Complete(mut plan) => {
1439            if !prev.exec_commands.is_empty() {
1440                let spec = super::ExecSpec {
1441                    commands: prev.exec_commands.clone(),
1442                };
1443                let runner = repo.command_runner();
1444                super::exec::verify_rebase_with_exec_resume(
1445                    repo,
1446                    &mut plan,
1447                    &spec,
1448                    &prev.rewrite,
1449                    runner.as_ref(),
1450                )?;
1451            }
1452            let movable_head = rebase_movable_ref_name(&prev.head_ref);
1453            let head_ref_old = if rebase_head_is_branch(&prev.head_ref) {
1454                prev.orig_head
1455            } else {
1456                head_before
1457            };
1458            if !rebase_head_is_branch(&prev.head_ref) {
1459                for update in &mut plan.ref_updates {
1460                    if update.name == "HEAD" {
1461                        update.old = head_before;
1462                    }
1463                }
1464            }
1465            if let Some(new_tip) = plan.mapped_tip(prev.orig_head) {
1466                if !plan.ref_updates.iter().any(|u| u.name == movable_head) {
1467                    plan.ref_updates.push(PlannedRefUpdate {
1468                        name: movable_head.to_owned(),
1469                        old: head_ref_old,
1470                        new: new_tip,
1471                    });
1472                }
1473            }
1474            let apply_without_hooks = ApplyOptions {
1475                hooks: PostOperationHooks::Skip,
1476                ..apply.clone()
1477            };
1478            let mut applied = apply_rewrite(repo, &plan, &apply_without_hooks, progress, cancel)
1479                .map_err(map_apply_err)?;
1480            // The branch moved while HEAD was detached for the replay, so apply_rewrite only
1481            // logged it on the branch; HEAD's log still ends at the pre-rebase tip. Log before
1482            // reattaching so a failure leaves a detached, abortable rebase.
1483            let final_head = if rebase_head_is_branch(&prev.head_ref) {
1484                if let Some(branch) = plan
1485                    .ref_updates
1486                    .iter()
1487                    .find(|u| u.name == prev.head_ref && u.old != u.new)
1488                {
1489                    log_head_move(repo, branch, apply)?;
1490                }
1491                refs::write_symbolic_ref_on_store(
1492                    repo.refs(),
1493                    repo.git_dir(),
1494                    "HEAD",
1495                    &prev.head_ref,
1496                )
1497                .map_err(map_lib)?;
1498                repo.resolve_ref_oid(&prev.head_ref).map_err(map_lib)?
1499            } else {
1500                applied.head
1501            };
1502            // The refs already moved: finish the rebase even if a cancel arrives now.
1503            reset_index_worktree_to_head(repo, final_head, &mut NullProgress, &NeverCancel)?;
1504            applied.head = final_head;
1505            if let Some(operation) = apply.record_operation.as_deref() {
1506                let head_old_tree = commit_tree(repo, prev.orig_head)?;
1507                let head_new_tree = commit_tree(repo, final_head)?;
1508                let record = HistoryLastRecord {
1509                    operation: operation.into(),
1510                    head_ref: Some(prev.head_ref.clone()),
1511                    head_old_commit: Some(prev.orig_head),
1512                    head_new_commit: Some(final_head),
1513                    head_old_tree: Some(head_old_tree),
1514                    head_new_tree: Some(head_new_tree),
1515                    ref_updates: plan.ref_updates.clone(),
1516                };
1517                write_history_last(repo, &record, None)?;
1518            }
1519            clear_rebase_state(repo.git_dir())?;
1520            if apply.hooks.enabled()
1521                && run_post_rewrite(repo, RewriteKind::Rebase, &plan.rewritten_pairs())
1522            {
1523                applied.hooks_run.push(HookName::PostRewrite);
1524            }
1525            Ok(RebaseOutcome::Done {
1526                head_change: (head_before != applied.head).then_some((head_before, applied.head)),
1527                applied,
1528                plan,
1529                rerere,
1530            })
1531        }
1532        ResumablePlanOutcome::Partial(partial) => {
1533            let request = RebaseRequest {
1534                upstream: prev.upstream,
1535                onto: Some(prev.onto),
1536                branch_tip: prev.branch_tip,
1537            };
1538            let stop = install_stopped_rebase(
1539                repo,
1540                &prev.head_ref,
1541                prev.orig_head,
1542                &request,
1543                &prev.rebase,
1544                apply,
1545                *partial,
1546                prev.exec_commands.clone(),
1547                progress,
1548                cancel,
1549            )?;
1550            Ok(RebaseOutcome::Stopped(stop))
1551        }
1552    }
1553}
1554
1555struct GitVisibleRebaseProgress {
1556    done: Vec<ObjectId>,
1557    remaining: Vec<ObjectId>,
1558    stop: Option<ObjectId>,
1559}
1560
1561/// Commits written to Git's `rebase-merge/` files for cross-tool `--continue`.
1562///
1563/// Grit's full replay order can include stacked-branch commits when
1564/// [`RefScope::Branches`](super::types::RefScope::Branches) is active; Git must only see the
1565/// rebased branch's own range so `git rebase --continue` finishes on that branch's tip.
1566fn git_visible_rebase_progress(
1567    repo: &Repository,
1568    fork: ObjectId,
1569    branch_tip: ObjectId,
1570    rewrite: &RewriteOptions,
1571    done: &[ObjectId],
1572    remaining: &[ObjectId],
1573    conflict: ObjectId,
1574) -> Result<GitVisibleRebaseProgress, RebaseSessionError> {
1575    if matches!(rewrite.scope, RefScope::Head) {
1576        return Ok(GitVisibleRebaseProgress {
1577            done: done.to_vec(),
1578            remaining: remaining.to_vec(),
1579            stop: Some(conflict),
1580        });
1581    }
1582    let primary = commits_in_rebase_range(repo, fork, &[branch_tip])?;
1583    let primary_set: HashSet<ObjectId> = primary.iter().copied().collect();
1584    let git_done: Vec<ObjectId> = done
1585        .iter()
1586        .copied()
1587        .filter(|oid| primary_set.contains(oid))
1588        .collect();
1589    let git_remaining: Vec<ObjectId> = remaining
1590        .iter()
1591        .copied()
1592        .filter(|oid| primary_set.contains(oid))
1593        .collect();
1594    let git_stop = primary_set.contains(&conflict).then_some(conflict);
1595    Ok(GitVisibleRebaseProgress {
1596        done: git_done,
1597        remaining: git_remaining,
1598        stop: git_stop,
1599    })
1600}
1601
1602fn write_git_rebase_merge_files(
1603    repo: &Repository,
1604    head_ref: &str,
1605    orig_head: ObjectId,
1606    onto: ObjectId,
1607    done: &[ObjectId],
1608    remaining: &[ObjectId],
1609    current: Option<&ObjectId>,
1610) -> Result<(), RebaseSessionError> {
1611    let dir = repo.git_dir().join("rebase-merge");
1612    fs::create_dir_all(&dir).map_err(map_lib)?;
1613    fs::write(dir.join("head-name"), format!("{head_ref}\n")).map_err(map_lib)?;
1614    fs::write(dir.join("onto"), format!("{}\n", onto.to_hex())).map_err(map_lib)?;
1615    fs::write(dir.join("orig-head"), format!("{}\n", orig_head.to_hex())).map_err(map_lib)?;
1616    let todo_remaining: Vec<ObjectId> = match current {
1617        Some(current) if remaining.first() == Some(current) => {
1618            remaining.iter().skip(1).copied().collect()
1619        }
1620        _ => remaining.to_vec(),
1621    };
1622    fs::write(
1623        dir.join("git-rebase-todo"),
1624        todo_lines(repo, &todo_remaining)?,
1625    )
1626    .map_err(map_lib)?;
1627    // Git lists the stopped pick as the last `done` line and resumes from `stopped-sha`,
1628    // committing the staged resolution with `message` and `author-script`.
1629    let done_with_current: Vec<ObjectId> = match current {
1630        Some(current) => done.iter().chain([current]).copied().collect(),
1631        None => done.to_vec(),
1632    };
1633    fs::write(dir.join("done"), todo_lines(repo, &done_with_current)?).map_err(map_lib)?;
1634    let msgnum = done_with_current.len();
1635    let end = done_with_current.len() + todo_remaining.len();
1636    fs::write(dir.join("msgnum"), format!("{msgnum}\n")).map_err(map_lib)?;
1637    fs::write(dir.join("end"), format!("{end}\n")).map_err(map_lib)?;
1638    if let Some(current) = current {
1639        fs::write(dir.join("stopped-sha"), format!("{}\n", current.to_hex())).map_err(map_lib)?;
1640        let commit = load_commit(repo, *current)?;
1641        match author_script(&commit.author) {
1642            Some(script) => fs::write(dir.join("author-script"), script).map_err(map_lib)?,
1643            None => remove_if_exists(&dir.join("author-script"))?,
1644        }
1645        fs::write(dir.join("message"), commit.message).map_err(map_lib)?;
1646    } else {
1647        remove_if_exists(&dir.join("stopped-sha"))?;
1648        remove_if_exists(&dir.join("author-script"))?;
1649        remove_if_exists(&dir.join("message"))?;
1650    }
1651    Ok(())
1652}
1653
1654fn todo_lines(repo: &Repository, commits: &[ObjectId]) -> Result<String, RebaseSessionError> {
1655    let mut out = String::new();
1656    for &oid in commits {
1657        let subject = super::subject::commit_message_subject_line(&load_commit(repo, oid)?.message);
1658        let abbrev = oid.to_short_hex();
1659        out.push_str(&format!("pick {abbrev} {subject}\n"));
1660    }
1661    Ok(out)
1662}
1663
1664/// `rebase-merge/author-script` body for a commit's `author` header: shell assignments of
1665/// `GIT_AUTHOR_NAME`, `GIT_AUTHOR_EMAIL` and `GIT_AUTHOR_DATE` (`@<seconds> <offset>`), each
1666/// value single-quoted. `None` when the header has no `<email>`.
1667fn author_script(author: &str) -> Option<String> {
1668    let lt = author.find('<')?;
1669    let gt = lt + author[lt..].find('>')?;
1670    let name = author[..lt].trim_end();
1671    let email = &author[lt + 1..gt];
1672    let date = format!("@{}", author[gt + 1..].trim());
1673    let quote = crate::transport::sq_quote_shell_arg;
1674    Some(format!(
1675        "GIT_AUTHOR_NAME={}\nGIT_AUTHOR_EMAIL={}\nGIT_AUTHOR_DATE={}\n",
1676        quote(name),
1677        quote(email),
1678        quote(&date)
1679    ))
1680}
1681
1682pub(super) fn clear_rebase_state(git_dir: &Path) -> Result<(), RebaseSessionError> {
1683    remove_if_exists(&git_dir.join("MERGE_MSG"))?;
1684    let dir = git_dir.join("rebase-merge");
1685    if dir.is_dir() {
1686        fs::remove_dir_all(&dir).map_err(map_lib)?;
1687    }
1688    Ok(())
1689}
1690
1691const REBASE_DETACHED_HEAD_NAME: &str = "detached HEAD";
1692
1693fn rebase_head_is_branch(head_ref: &str) -> bool {
1694    head_ref.starts_with("refs/")
1695}
1696
1697/// Ref name used in [`PlannedRefUpdate`] and live ref moves during a rebase session.
1698///
1699/// Persisted `head-name` stays `detached HEAD` for Git compatibility; only `HEAD` is storable.
1700fn rebase_movable_ref_name(head_ref: &str) -> &str {
1701    if rebase_head_is_branch(head_ref) {
1702        head_ref
1703    } else {
1704        "HEAD"
1705    }
1706}
1707
1708fn branch_head(repo: &Repository) -> Result<(String, ObjectId), RebaseSessionError> {
1709    match resolve_head_for_repository(repo).map_err(map_lib)? {
1710        HeadState::Branch {
1711            refname,
1712            oid: Some(oid),
1713            ..
1714        } => Ok((refname, oid)),
1715        HeadState::Detached { oid } => Ok((REBASE_DETACHED_HEAD_NAME.to_owned(), oid)),
1716        HeadState::Branch { oid: None, .. } | HeadState::Invalid => {
1717            Err(RebaseSessionError::NotOnBranch)
1718        }
1719    }
1720}
1721
1722fn resolve_head_oid(repo: &Repository) -> Result<ObjectId, RebaseSessionError> {
1723    match resolve_head_for_repository(repo).map_err(map_lib)? {
1724        HeadState::Branch { oid: Some(oid), .. } | HeadState::Detached { oid } => Ok(oid),
1725        HeadState::Branch { oid: None, .. } | HeadState::Invalid => {
1726            Err(RebaseSessionError::NotOnBranch)
1727        }
1728    }
1729}
1730
1731fn reset_index_worktree_to_head(
1732    repo: &Repository,
1733    head_oid: ObjectId,
1734    progress: &mut dyn ProgressSink,
1735    cancel: &dyn Cancel,
1736) -> Result<(), RebaseSessionError> {
1737    let head_tree = commit_tree(repo, head_oid)?;
1738    // Paths tracked before the move but absent from `head_oid` must go too; leaving them as
1739    // untracked files makes `git rebase --skip/--abort` refuse to overwrite them later.
1740    checkout_hard_to_tree_from_head_tree(repo, &head_tree, &head_tree, progress, cancel)
1741        .map_err(map_lib)?;
1742    Ok(())
1743}
1744
1745/// Re-attach `HEAD` to `refname` and hard-reset it to `target`, reporting the tip it had
1746/// when that was not `target`.
1747fn hard_reset_branch(
1748    repo: &Repository,
1749    refname: &str,
1750    target: ObjectId,
1751    identity: &crate::ident::Signature,
1752) -> Result<RebaseAbort, RebaseSessionError> {
1753    let before = repo
1754        .references()
1755        .resolve_if_exists(refname)
1756        .map_err(map_lib)?;
1757    refs::write_symbolic_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", refname)
1758        .map_err(map_lib)?;
1759    hard_reset_head(repo, target, identity)?;
1760    let overwritten = (before != Some(target)).then(|| PlannedRefUpdate {
1761        name: refname.to_owned(),
1762        old: before.unwrap_or_else(crate::diff::zero_oid),
1763        new: target,
1764    });
1765    Ok(RebaseAbort {
1766        overwritten: overwritten.into_iter().collect(),
1767    })
1768}
1769
1770fn hard_reset_head(
1771    repo: &Repository,
1772    target: ObjectId,
1773    identity: &crate::ident::Signature,
1774) -> Result<(), RebaseSessionError> {
1775    reset_to_commit(
1776        repo,
1777        &ResetOptions::new(target, ResetMode::Hard, identity.clone(), "rebase (abort)"),
1778        &mut crate::progress::NullProgress,
1779        &crate::progress::NeverCancel,
1780    )
1781    .map_err(map_lib)?;
1782    Ok(())
1783}
1784
1785use std::io::Write;
1786
1787fn map_lib<E: Into<crate::error::Error>>(err: E) -> RebaseSessionError {
1788    RebaseSessionError::Library(err.into())
1789}
1790
1791fn map_apply_err(err: HistoryError) -> RebaseSessionError {
1792    if err.is_cancelled() {
1793        RebaseSessionError::Library(crate::error::Error::Cancelled)
1794    } else {
1795        err.into()
1796    }
1797}
1798
1799fn replan_rewrite_options(state: &LoadedState) -> RewriteOptions {
1800    state.rewrite.clone()
1801}
1802
1803fn map_json(err: serde_json::Error) -> RebaseSessionError {
1804    RebaseSessionError::Library(crate::error::Error::Message(err.to_string()))
1805}
1806
1807#[cfg(test)]
1808mod tests {
1809    use super::{
1810        author_script, edits_from_persist, edits_to_persist, parse_oid, PersistedEdit,
1811        RebaseSessionError,
1812    };
1813    use crate::history::types::{CommitEdit, HistoryEdits};
1814    use crate::objects::ObjectId;
1815
1816    fn oid(byte: u8) -> ObjectId {
1817        ObjectId::from_bytes(&[byte; 20]).unwrap_or_else(|_| ObjectId::zero())
1818    }
1819
1820    #[test]
1821    fn reuse_edit_round_trips_its_continuation_parent() {
1822        let mut edits = HistoryEdits::new();
1823        edits.insert(
1824            oid(1),
1825            CommitEdit::Reuse {
1826                existing: oid(2),
1827                parent: oid(3),
1828            },
1829        );
1830        let rows = edits_to_persist(&edits);
1831        let json = serde_json::to_string(&rows).unwrap_or_default();
1832        let parsed: Vec<PersistedEdit> = serde_json::from_str(&json).unwrap_or_default();
1833        assert_eq!(edits_from_persist(&parsed).ok(), Some(edits));
1834    }
1835
1836    #[test]
1837    fn reuse_edit_saved_without_parent_continues_from_the_reused_commit() {
1838        let json = format!(
1839            r#"[{{"Reuse":{{"commit":"{}","existing":"{}"}}}}]"#,
1840            oid(1).to_hex(),
1841            oid(2).to_hex()
1842        );
1843        let parsed: Vec<PersistedEdit> = serde_json::from_str(&json).unwrap_or_default();
1844        let edits = edits_from_persist(&parsed).ok();
1845        assert_eq!(
1846            edits.and_then(|edits| edits.get(&oid(1)).cloned()),
1847            Some(CommitEdit::Reuse {
1848                existing: oid(2),
1849                parent: oid(2),
1850            })
1851        );
1852    }
1853
1854    #[test]
1855    fn parse_oid_reports_the_bad_value() {
1856        assert_eq!(parse_oid(&oid(7).to_hex()).ok(), Some(oid(7)));
1857        let err = parse_oid("abc").err();
1858        assert!(
1859            matches!(&err, Some(RebaseSessionError::InvalidStateOid { value }) if value == "abc"),
1860            "{err:?}"
1861        );
1862    }
1863
1864    #[test]
1865    fn persisted_edit_with_bad_oid_is_rejected() {
1866        let rows = vec![PersistedEdit::Drop {
1867            commit: "zz".into(),
1868        }];
1869        assert!(matches!(
1870            edits_from_persist(&rows),
1871            Err(RebaseSessionError::InvalidStateOid { value }) if value == "zz"
1872        ));
1873    }
1874
1875    #[test]
1876    fn author_script_quotes_values_and_prefixes_date() {
1877        assert_eq!(
1878            author_script("A U Thor <a@example.com> 1700000000 -0130").as_deref(),
1879            Some(
1880                "GIT_AUTHOR_NAME='A U Thor'\nGIT_AUTHOR_EMAIL='a@example.com'\nGIT_AUTHOR_DATE='@1700000000 -0130'\n"
1881            )
1882        );
1883        assert_eq!(
1884            author_script("O'Brien <o@example.com> 1 +0000").as_deref(),
1885            Some(
1886                "GIT_AUTHOR_NAME='O'\\''Brien'\nGIT_AUTHOR_EMAIL='o@example.com'\nGIT_AUTHOR_DATE='@1 +0000'\n"
1887            )
1888        );
1889        assert_eq!(author_script("no email here 1 +0000"), None);
1890    }
1891}