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