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