1use std::collections::{HashMap, HashSet};
4use std::fs;
5use std::path::Path;
6
7use serde::{Deserialize, Serialize};
8
9use crate::change_id::logical_change_id_for_commit;
10use crate::hooks::{
11 run_post_checkout, run_post_rewrite, HookName, PostOperationHooks, RewriteKind,
12};
13use crate::lockfile::{LockFile, LockWait};
14use crate::objects::ObjectId;
15use crate::operation::conflicts_from_tree_merge;
16use crate::porcelain::checkout::checkout_hard_to_tree_from_head_tree;
17use crate::porcelain::merge::apply_conflict_merge_worktree;
18use crate::porcelain::reset::{reset_to_commit, ResetMode, ResetOptions};
19use crate::porcelain::worktree_guard::{
20 ensure_safe_for_rebase_stop, ensure_worktree_clean_for_rebase_exec,
21};
22use crate::progress::{
23 poll_cancel, Cancel, NeverCancel, NullProgress, ProgressSink, UncancellableProgress,
24};
25use crate::refs;
26use crate::repo::Repository;
27use crate::state::{resolve_head_for_repository, write_orig_head, HeadState};
28use crate::write_tree::{write_tree_update_index, WriteTreeFlags};
29
30use super::apply::{apply_rewrite, log_head_move};
31use super::error::HistoryError;
32use super::last::{write_history_last, HistoryLastRecord};
33use super::plan::{
34 commit_parents, plan_rewrite_with_order_resumable_seeded, write_rebase_staged_resolution,
35 ResumablePlanOutcome,
36};
37use super::rebase::{
38 commits_in_rebase_range, plan_rebase_resumable, PreRebase, RebaseOntoOptions, RebasePlan,
39 RebaseRequest,
40};
41use super::types::RefScope;
42use super::types::{
43 AppliedRewrite, ApplyOptions, CommitEdit, HistoryEdits, PlannedRefUpdate, RewriteOptions,
44 RewritePlan, RewrittenCommit,
45};
46use super::{commit_tree, load_commit};
47use crate::operation::Conflict;
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct StartRebaseOptions {
52 pub request: RebaseRequest,
54 pub rebase: RebaseOntoOptions,
56 pub apply: ApplyOptions,
58 pub pre_rebase: PreRebase,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
66pub struct RebaseStop {
67 pub commit: ObjectId,
69 pub conflicts: Vec<Conflict>,
71 pub done: Vec<ObjectId>,
73 pub remaining: Vec<ObjectId>,
75}
76
77#[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 Done {
84 applied: AppliedRewrite,
86 plan: Box<RewritePlan>,
88 head_change: Option<(ObjectId, ObjectId)>,
95 },
96 ExecDone {
99 applied: AppliedRewrite,
101 plan: Box<RewritePlan>,
103 exec: super::ExecReport,
106 },
107 Stopped(RebaseStop),
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Default)]
113#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
114pub struct RebaseAbort {
115 pub overwritten: Vec<PlannedRefUpdate>,
120}
121
122#[derive(Debug, thiserror::Error)]
124#[non_exhaustive]
125pub enum RebaseSessionError {
126 #[error("no rebase in progress")]
128 NoRebaseInProgress,
129
130 #[error("rebase is already in progress")]
132 RebaseAlreadyInProgress,
133
134 #[error("a rebase started by git is in progress")]
140 ForeignRebaseInProgress,
141
142 #[error("cannot continue: unmerged paths remain in the index")]
144 UnmergedPaths,
145
146 #[error("nothing to commit after conflict resolution")]
148 NothingToCommit,
149
150 #[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 expected: ObjectId,
158 },
159
160 #[error("rebase session requires HEAD on a branch")]
162 NotOnBranch,
163
164 #[error("exec replay HEAD changed; expected {expected}; save new commits on a branch, then abort and restart the rebase")]
167 ExecHeadChanged {
168 expected: ObjectId,
170 },
171
172 #[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 name: String,
182 expected: ObjectId,
184 actual: Option<ObjectId>,
186 },
187
188 #[error("ref {name} moved while aborting the rebase; retry the abort")]
191 AbortRefMoved {
192 name: String,
194 },
195
196 #[error("rebase state records an invalid object id: {value:?}")]
199 InvalidStateOid {
200 value: String,
202 },
203
204 #[error(transparent)]
206 History(#[from] HistoryError),
207
208 #[error(transparent)]
210 Library(#[from] crate::error::Error),
211}
212
213impl RebaseSessionError {
214 #[must_use]
216 pub fn is_cancelled(&self) -> bool {
217 match self {
218 Self::Library(e) => e.is_cancelled(),
219 Self::History(e) => e.is_cancelled(),
220 Self::NoRebaseInProgress
221 | Self::RebaseAlreadyInProgress
222 | Self::ForeignRebaseInProgress
223 | Self::UnmergedPaths
224 | Self::NothingToCommit
225 | Self::ResolutionHeadChanged { .. }
226 | Self::NotOnBranch
227 | Self::ExecHeadChanged { .. }
228 | Self::StaleExecPlan { .. }
229 | Self::AbortRefMoved { .. }
230 | Self::InvalidStateOid { .. } => false,
231 }
232 }
233}
234
235pub fn start_rebase(
266 repo: &Repository,
267 opts: &StartRebaseOptions,
268 progress: &mut dyn ProgressSink,
269 cancel: &dyn Cancel,
270) -> Result<RebaseOutcome, RebaseSessionError> {
271 super::ensure_rewrite_apply_ready(repo)?;
272 if let Some(active) = active_rebase(repo.git_dir()) {
273 return Err(match active {
274 ActiveRebase::Foreign(_) => RebaseSessionError::ForeignRebaseInProgress,
275 ActiveRebase::Exec | ActiveRebase::Grit => RebaseSessionError::RebaseAlreadyInProgress,
276 });
277 }
278 let (head_ref, orig_head) = branch_head(repo)?;
279 let plan = plan_rebase_resumable(repo, &opts.request, &opts.rebase, &opts.pre_rebase)?;
280 poll_cancel(progress, cancel).map_err(map_lib)?;
281 match plan {
282 Err(RebasePlan::UpToDate) => Ok(up_to_date_outcome(orig_head)),
283 Err(RebasePlan::Plan(_) | RebasePlan::Conflict { .. }) => Err(HistoryError::Planning {
284 message: "unexpected non-resumable rebase plan".into(),
285 }
286 .into()),
287 Ok(ResumablePlanOutcome::Complete(plan)) => {
288 let applied = apply_rewrite(repo, &plan, &opts.apply, progress, cancel)?;
289 write_orig_head(repo, orig_head).map_err(map_lib)?;
290 Ok(RebaseOutcome::Done {
291 head_change: (orig_head != applied.head).then_some((orig_head, applied.head)),
292 applied,
293 plan,
294 })
295 }
296 Ok(ResumablePlanOutcome::Partial(partial)) => {
297 let orig_tree = commit_tree(repo, orig_head)?;
298 let stop_tree = commit_tree(repo, partial.conflict.replayed_parent)?;
299 ensure_safe_for_rebase_stop(repo, &orig_tree, &stop_tree)?;
300 let stop = install_stopped_rebase(
301 repo,
302 &head_ref,
303 orig_head,
304 &opts.request,
305 &opts.rebase,
306 &opts.apply,
307 *partial,
308 Vec::new(),
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
322pub fn start_rebase_with_exec(
346 repo: &Repository,
347 opts: &StartRebaseOptions,
348 spec: &super::ExecSpec,
349 runner: &dyn crate::command_runner::CommandRunner,
350 progress: &mut dyn ProgressSink,
351 cancel: &dyn Cancel,
352) -> Result<RebaseOutcome, RebaseSessionError> {
353 super::ensure_rewrite_apply_ready(repo)?;
354 if let Some(active) = active_rebase(repo.git_dir()) {
355 return Err(match active {
356 ActiveRebase::Foreign(_) => RebaseSessionError::ForeignRebaseInProgress,
357 ActiveRebase::Exec | ActiveRebase::Grit => RebaseSessionError::RebaseAlreadyInProgress,
358 });
359 }
360 let (head_ref, orig_head) = branch_head(repo)?;
361 let plan = plan_rebase_resumable(repo, &opts.request, &opts.rebase, &opts.pre_rebase)?;
362 poll_cancel(progress, cancel).map_err(map_lib)?;
363 match plan {
364 Err(RebasePlan::UpToDate) => {
365 if spec.commands.is_empty() {
366 return Ok(up_to_date_outcome(orig_head));
367 }
368 let fork =
369 super::rebase::rebase_fork(repo, opts.request.upstream, opts.request.branch_tip)?;
370 let effective_onto = opts.request.onto.unwrap_or(opts.request.upstream);
371 if opts.request.branch_tip == effective_onto {
372 return Ok(up_to_date_outcome(orig_head));
373 }
374 let range =
375 super::rebase::commits_in_rebase_range(repo, fork, &[opts.request.branch_tip])?;
376 if range.is_empty() {
377 return Ok(up_to_date_outcome(orig_head));
378 }
379 opts.pre_rebase.run(repo)?;
380 let mut plan = super::rebase::plan_rebase_identity_exec(
381 repo,
382 fork,
383 effective_onto,
384 &range,
385 &opts.rebase,
386 )?;
387 super::apply::verify_rewrite_plan(repo, &plan)?;
388 let exec = match super::verify_rebase_with_exec(
389 repo,
390 &mut plan,
391 spec,
392 &opts.rebase.rewrite,
393 runner,
394 ) {
395 Ok(exec) => exec,
396 Err(
397 stop @ (HistoryError::ExecDirty { .. } | HistoryError::ExecConflict { .. }),
398 ) => {
399 write_orig_head(repo, orig_head).map_err(map_lib)?;
400 return Err(stop.into());
401 }
402 Err(err) => return Err(err.into()),
403 };
404 let applied = apply_rewrite(repo, &plan, &opts.apply, progress, cancel)?;
405 write_orig_head(repo, orig_head).map_err(map_lib)?;
406 Ok(RebaseOutcome::ExecDone {
407 applied,
408 plan: Box::new(plan),
409 exec,
410 })
411 }
412 Err(RebasePlan::Plan(_) | RebasePlan::Conflict { .. }) => Err(HistoryError::Planning {
413 message: "unexpected non-resumable rebase plan".into(),
414 }
415 .into()),
416 Ok(ResumablePlanOutcome::Partial(partial)) => {
417 let orig_tree = commit_tree(repo, orig_head)?;
418 let stop_tree = commit_tree(repo, partial.conflict.replayed_parent)?;
419 ensure_safe_for_rebase_stop(repo, &orig_tree, &stop_tree)?;
420 let stop = install_stopped_rebase(
421 repo,
422 &head_ref,
423 orig_head,
424 &opts.request,
425 &opts.rebase,
426 &opts.apply,
427 *partial,
428 spec.commands.clone(),
429 progress,
430 cancel,
431 )?;
432 write_orig_head(repo, orig_head).map_err(map_lib)?;
433 if opts.apply.hooks.enabled() {
434 let onto = opts.request.onto.unwrap_or(opts.request.upstream);
435 run_post_checkout(repo, Some(orig_head), onto, true);
436 }
437 Ok(RebaseOutcome::Stopped(stop))
438 }
439 Ok(ResumablePlanOutcome::Complete(plan)) => {
440 let mut plan = *plan;
441 super::apply::verify_rewrite_plan(repo, &plan)?;
442 let exec = match super::verify_rebase_with_exec(
443 repo,
444 &mut plan,
445 spec,
446 &opts.rebase.rewrite,
447 runner,
448 ) {
449 Ok(exec) => exec,
450 Err(
451 stop @ (HistoryError::ExecDirty { .. } | HistoryError::ExecConflict { .. }),
452 ) => {
453 write_orig_head(repo, orig_head).map_err(map_lib)?;
454 return Err(stop.into());
455 }
456 Err(err) => return Err(err.into()),
457 };
458 let applied = apply_rewrite(repo, &plan, &opts.apply, progress, cancel)?;
459 write_orig_head(repo, orig_head).map_err(map_lib)?;
460 Ok(RebaseOutcome::ExecDone {
461 applied,
462 plan: Box::new(plan),
463 exec,
464 })
465 }
466 }
467}
468
469fn up_to_date_outcome(head: ObjectId) -> RebaseOutcome {
470 RebaseOutcome::Done {
471 head_change: None,
472 applied: AppliedRewrite {
473 ref_updates: Vec::new(),
474 head,
475 hooks_run: Vec::new(),
476 },
477 plan: Box::new(RewritePlan {
478 commits: Vec::new(),
479 ref_updates: Vec::new(),
480 head: super::types::HeadEffect::default(),
481 }),
482 }
483}
484
485pub(crate) fn persist_rebase_stopped_head_if_missing(
488 repo: &Repository,
489) -> Result<(), RebaseSessionError> {
490 if !crate::state::grit_rebase_conflict_stop_active(repo.git_dir()) {
491 return Ok(());
492 }
493 let path = repo.git_dir().join("rebase-merge/grit-plan");
494 let raw = fs::read_to_string(&path).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
495 let mut parsed: PersistedRebaseState =
496 serde_json::from_str(&raw).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
497 if parsed.stopped_head.is_some() {
498 return Ok(());
499 }
500 let head = resolve_head_oid(repo)?;
501 parsed.stopped_head = Some(oid_hex(&head));
502 write_persisted_state(repo.git_dir(), &parsed)
503}
504
505fn replay_base_parent(state: &LoadedState, oid: ObjectId) -> bool {
506 oid == state.onto || state.mapping.values().any(|mapped| *mapped == oid)
507}
508
509fn stopped_head_for_continue(
510 state: &LoadedState,
511 repo: &Repository,
512) -> Result<ObjectId, RebaseSessionError> {
513 if let Some(oid) = state.stopped_head {
514 return Ok(oid);
515 }
516 let head = resolve_head_oid(repo)?;
517 if replay_base_parent(state, head) {
518 return Ok(head);
519 }
520 let head_commit = load_commit(repo, head)?;
521 if head_commit.parents.len() == 1 {
522 if let Some(&parent) = head_commit.parents.first() {
523 if replay_base_parent(state, parent) {
524 return Ok(parent);
525 }
526 }
527 } else {
528 for &parent in &head_commit.parents {
529 if replay_base_parent(state, parent) {
530 return Ok(parent);
531 }
532 }
533 }
534 Err(RebaseSessionError::NoRebaseInProgress)
535}
536
537fn is_first_parent_descendant_of(
538 repo: &Repository,
539 ancestor: ObjectId,
540 tip: ObjectId,
541) -> Result<bool, RebaseSessionError> {
542 if tip == ancestor {
543 return Ok(false);
544 }
545 let mut current = Some(tip);
546 while let Some(oid) = current {
547 let parents = commit_parents(repo, oid).map_err(RebaseSessionError::from)?;
548 let Some(first) = parents.first().copied() else {
549 break;
550 };
551 if first == ancestor {
552 return Ok(true);
553 }
554 current = Some(first);
555 }
556 Ok(false)
557}
558
559fn run_exec_after_resolution(
560 repo: &Repository,
561 state: &LoadedState,
562 head_oid: ObjectId,
563) -> Result<ObjectId, RebaseSessionError> {
564 if state.exec_commands.is_empty() {
565 return Ok(head_oid);
566 }
567 let spec = super::ExecSpec {
568 commands: state.exec_commands.clone(),
569 };
570 let runner = repo.command_runner();
571 let (tip, _) = super::exec::run_exec_at_tip(repo, &spec, runner.as_ref(), head_oid)?;
572 Ok(tip)
573}
574
575fn advance_after_user_resolution_commit(
576 repo: &Repository,
577 state: &LoadedState,
578 opts: &StartRebaseOptions,
579 current: ObjectId,
580 head_oid: ObjectId,
581 progress: &mut dyn ProgressSink,
582 cancel: &dyn Cancel,
583) -> Result<RebaseOutcome, RebaseSessionError> {
584 let head_oid = run_exec_after_resolution(repo, state, head_oid)?;
585 let current_data = load_commit(repo, current)?;
586 let mut mapping = state.mapping.clone();
587 mapping.insert(current, head_oid);
588 let mut rewritten = state.rewritten.clone();
589 rewritten.push(RewrittenCommit {
590 old: current,
591 new: Some(head_oid),
592 change_id: logical_change_id_for_commit(repo, head_oid).map_err(map_lib)?,
593 subject: super::subject::subject_for_rewrite_row(repo, ¤t_data, Some(head_oid))
594 .map_err(RebaseSessionError::from)?,
595 reused: false,
596 });
597 let resume_order: Vec<ObjectId> = replay_order_for_resume(state, current)
598 .into_iter()
599 .filter(|commit| *commit != current)
600 .collect();
601 let mut edits = state.edits.clone();
602 edits.retain(|commit, _| resume_order.contains(commit));
603 let rewrite = replan_rewrite_options(state);
604 let outcome = plan_rewrite_with_order_resumable_seeded(
605 repo,
606 &edits,
607 &rewrite,
608 Some(&resume_order),
609 state.flatten_leaf_merges.then_some(state.onto),
610 state.rebase.favor,
611 Some((mapping, rewritten)),
612 Some(state.scoped_refs.clone()),
613 )?;
614 advance_after_plan(
615 repo,
616 state,
617 &opts.apply,
618 outcome,
619 head_oid,
620 progress,
621 cancel,
622 )
623}
624
625pub fn continue_rebase(
645 repo: &Repository,
646 opts: &StartRebaseOptions,
647 progress: &mut dyn ProgressSink,
648 cancel: &dyn Cancel,
649) -> Result<RebaseOutcome, RebaseSessionError> {
650 if matches!(active_rebase(repo.git_dir()), Some(ActiveRebase::Exec)) {
651 return super::exec::resume_exec_rebase(repo, &opts.apply, false, progress, cancel);
652 }
653 super::ensure_rewrite_apply_ready(repo)?;
654 let state = load_persisted_state(repo.git_dir())?;
655 if repo.load_index()?.has_unmerged_entries() {
656 return Err(RebaseSessionError::UnmergedPaths);
657 }
658 let current = state
659 .stopped_commit
660 .or_else(|| state.remaining.first().copied())
661 .ok_or(RebaseSessionError::NoRebaseInProgress)?;
662 let new_tree = {
663 let mut index = repo.load_index()?;
664 write_tree_update_index(repo.odb(), &mut index, "", WriteTreeFlags::silent())
665 .map_err(map_lib)?
666 };
667 let head_oid = resolve_head_oid(repo)?;
668 let head_tree = commit_tree(repo, head_oid)?;
669 if new_tree == head_tree {
670 let stopped_head = stopped_head_for_continue(&state, repo)?;
671 if head_oid == stopped_head {
672 return Err(RebaseSessionError::NothingToCommit);
673 }
674 if is_first_parent_descendant_of(repo, stopped_head, head_oid)? {
675 ensure_worktree_clean_for_rebase_exec(repo).map_err(map_lib)?;
676 return advance_after_user_resolution_commit(
677 repo, &state, opts, current, head_oid, progress, cancel,
678 );
679 }
680 return Err(RebaseSessionError::ResolutionHeadChanged {
681 expected: stopped_head,
682 });
683 }
684
685 let original_parents = commit_parents(repo, current).map_err(RebaseSessionError::from)?;
686 let parents = if original_parents.len() <= 1 {
687 vec![head_oid]
688 } else {
689 let mut mapped = vec![head_oid];
690 mapped.extend(
691 original_parents[1..]
692 .iter()
693 .map(|parent| state.mapping.get(parent).copied().unwrap_or(*parent)),
694 );
695 mapped
696 };
697
698 if state.remaining.len() > 1 {
702 let stopped_head = stopped_head_for_continue(&state, repo)?;
703 if head_oid != stopped_head {
704 return Err(RebaseSessionError::ResolutionHeadChanged {
705 expected: stopped_head,
706 });
707 }
708 let resolved =
709 write_rebase_staged_resolution(repo, &state.rewrite, current, new_tree, &parents)
710 .map_err(RebaseSessionError::from)?;
711 repo.odb().flush_for_publish().map_err(map_lib)?;
712 refs::update_direct_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", &resolved)
713 .map_err(map_lib)?;
714 ensure_worktree_clean_for_rebase_exec(repo).map_err(map_lib)?;
715 return advance_after_user_resolution_commit(
716 repo, &state, opts, current, resolved, progress, cancel,
717 );
718 }
719
720 if !state.exec_commands.is_empty() && new_tree != head_tree {
721 let stopped_head = stopped_head_for_continue(&state, repo)?;
722 if head_oid != stopped_head {
723 return Err(RebaseSessionError::ResolutionHeadChanged {
724 expected: stopped_head,
725 });
726 }
727 let resolved =
728 write_rebase_staged_resolution(repo, &state.rewrite, current, new_tree, &parents)
729 .map_err(RebaseSessionError::from)?;
730 repo.odb().flush_for_publish().map_err(map_lib)?;
731 refs::update_direct_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", &resolved)
732 .map_err(map_lib)?;
733 ensure_worktree_clean_for_rebase_exec(repo).map_err(map_lib)?;
734 return advance_after_user_resolution_commit(
735 repo, &state, opts, current, resolved, progress, cancel,
736 );
737 }
738
739 let resume_order = replay_order_for_resume(&state, current);
740 let mut edits = state.edits.clone();
741 edits.insert(
742 current,
743 CommitEdit::Rewrite {
744 tree: Some(new_tree),
745 message: None,
746 author: None,
747 parents: Some(parents),
748 },
749 );
750 edits.retain(|commit, _| resume_order.contains(commit));
751
752 let seed = (state.mapping.clone(), state.rewritten.clone());
753 let rewrite = replan_rewrite_options(&state);
754 let outcome = plan_rewrite_with_order_resumable_seeded(
755 repo,
756 &edits,
757 &rewrite,
758 Some(&resume_order),
759 state.flatten_leaf_merges.then_some(state.onto),
760 state.rebase.favor,
761 Some(seed),
762 Some(state.scoped_refs.clone()),
763 )?;
764 advance_after_plan(
765 repo,
766 &state,
767 &opts.apply,
768 outcome,
769 head_oid,
770 progress,
771 cancel,
772 )
773}
774
775pub fn skip_rebase(
789 repo: &Repository,
790 opts: &StartRebaseOptions,
791 progress: &mut dyn ProgressSink,
792 cancel: &dyn Cancel,
793) -> Result<RebaseOutcome, RebaseSessionError> {
794 if matches!(active_rebase(repo.git_dir()), Some(ActiveRebase::Exec)) {
795 return super::exec::resume_exec_rebase(repo, &opts.apply, true, progress, cancel);
796 }
797 let state = load_persisted_state(repo.git_dir())?;
798 let current = state
799 .remaining
800 .first()
801 .copied()
802 .ok_or(RebaseSessionError::NoRebaseInProgress)?;
803 let head_oid = resolve_head_oid(repo)?;
804 reset_index_worktree_to_head(repo, head_oid, progress, cancel)?;
805
806 let replayed_parent = run_exec_after_resolution(repo, &state, head_oid)?;
807 let mut mapping = state.mapping.clone();
808 mapping.insert(current, replayed_parent);
809 let mut rewritten = state.rewritten.clone();
810 rewritten.push(RewrittenCommit {
811 old: current,
812 new: None,
813 change_id: None,
814 subject: None,
815 reused: false,
816 });
817
818 let resume_order: Vec<ObjectId> = state
819 .remaining
820 .iter()
821 .copied()
822 .filter(|c| *c != current)
823 .collect();
824 let mut edits = state.edits.clone();
825 edits.retain(|commit, _| resume_order.contains(commit));
826 let rewrite = replan_rewrite_options(&state);
827 let outcome = plan_rewrite_with_order_resumable_seeded(
828 repo,
829 &edits,
830 &rewrite,
831 Some(&resume_order),
832 state.flatten_leaf_merges.then_some(state.onto),
833 state.rebase.favor,
834 Some((mapping, rewritten)),
835 Some(state.scoped_refs.clone()),
836 )?;
837 advance_after_plan(
838 repo,
839 &state,
840 &opts.apply,
841 outcome,
842 head_oid,
843 progress,
844 cancel,
845 )
846}
847
848pub fn abort_rebase(
869 repo: &Repository,
870 identity: &crate::ident::Signature,
871) -> Result<RebaseAbort, RebaseSessionError> {
872 identity
873 .validate()
874 .map_err(|err| map_lib(crate::error::Error::InvalidSignature(err)))?;
875 let had_journal = super::rewrite_apply_journal_active(repo.git_dir());
876 if had_journal {
877 super::abort_interrupted_rewrite_apply(repo)?;
878 }
879 match active_rebase(repo.git_dir()) {
880 None => {
881 if had_journal {
882 Ok(RebaseAbort::default())
883 } else {
884 Err(RebaseSessionError::NoRebaseInProgress)
885 }
886 }
887 Some(ActiveRebase::Foreign(dir)) => abort_foreign_rebase(repo, &dir, identity),
888 Some(ActiveRebase::Exec) => super::exec::abort_exec_rebase(repo, identity),
889 Some(ActiveRebase::Grit) => {
890 let state = load_persisted_state(repo.git_dir())?;
891 crate::rerere::rerere_clear(repo.git_dir()).map_err(map_lib)?;
892 let abort = hard_reset_branch(repo, &state.head_ref, state.orig_head, identity)?;
893 clear_rebase_state(repo.git_dir())?;
894 Ok(abort)
895 }
896 }
897}
898
899fn abort_foreign_rebase(
900 repo: &Repository,
901 state_dir: &Path,
902 identity: &crate::ident::Signature,
903) -> Result<RebaseAbort, RebaseSessionError> {
904 if state_dir.join("autostash").exists() {
905 return Err(RebaseSessionError::ForeignRebaseInProgress);
906 }
907 let orig_head = first_line(&state_dir.join("orig-head"))
908 .and_then(|hex| ObjectId::from_hex(&hex).ok())
909 .ok_or(RebaseSessionError::ForeignRebaseInProgress)?;
910 crate::rerere::rerere_clear(repo.git_dir()).map_err(map_lib)?;
911 let abort =
912 match first_line(&state_dir.join("head-name")).filter(|name| name.starts_with("refs/")) {
913 Some(head_ref) => hard_reset_branch(repo, &head_ref, orig_head, identity)?,
914 None => {
915 hard_reset_head(repo, orig_head, identity)?;
916 RebaseAbort::default()
917 }
918 };
919 for name in ["REBASE_HEAD", "AUTO_MERGE", "MERGE_MSG"] {
920 remove_if_exists(&repo.git_dir().join(name))?;
921 }
922 fs::remove_dir_all(state_dir).map_err(map_lib)?;
923 Ok(abort)
924}
925
926enum ActiveRebase {
928 Exec,
930 Grit,
932 Foreign(std::path::PathBuf),
934}
935
936fn active_rebase(git_dir: &Path) -> Option<ActiveRebase> {
937 let merge = git_dir.join("rebase-merge");
938 if merge.join("grit-exec").is_file() {
939 return Some(ActiveRebase::Exec);
940 }
941 if merge.join("grit-plan").is_file() {
942 return Some(ActiveRebase::Grit);
943 }
944 if merge.is_dir() {
945 return Some(ActiveRebase::Foreign(merge));
946 }
947 let apply = git_dir.join("rebase-apply");
948 (apply.is_dir() && !apply.join("applying").exists()).then_some(ActiveRebase::Foreign(apply))
949}
950
951fn first_line(path: &Path) -> Option<String> {
952 let raw = fs::read_to_string(path).ok()?;
953 let line = raw.lines().next()?.trim();
954 (!line.is_empty()).then(|| line.to_owned())
955}
956
957fn remove_if_exists(path: &Path) -> Result<(), RebaseSessionError> {
958 match fs::remove_file(path) {
959 Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(map_lib(e)),
960 _ => Ok(()),
961 }
962}
963
964#[derive(Debug, Clone, Serialize, Deserialize)]
967struct PersistedRebaseState {
968 mapping: HashMap<String, String>,
969 rewritten: Vec<PersistedRewrittenRow>,
970 edits: Vec<PersistedEdit>,
971 rewrite: RewriteOptions,
972 rebase: RebaseOntoOptions,
973 apply: ApplyOptions,
974 replay_order: Vec<String>,
975 remaining: Vec<String>,
976 done: Vec<String>,
977 fork: String,
978 onto: String,
979 upstream: String,
980 head_ref: String,
981 orig_head: String,
982 branch_tip: String,
983 flatten_leaf_merges: bool,
984 scoped_refs: Vec<(String, String)>,
985 #[serde(default)]
987 stopped_head: Option<String>,
988 #[serde(default)]
990 stopped_commit: Option<String>,
991 #[serde(default)]
993 exec_commands: Vec<String>,
994}
995
996#[derive(Debug, Clone, Serialize, Deserialize)]
997struct PersistedRewrittenRow {
998 old: String,
999 new: Option<String>,
1000 #[serde(default)]
1001 reused: bool,
1002}
1003
1004#[derive(Debug, Clone, Serialize, Deserialize)]
1005enum PersistedEdit {
1006 Reparent {
1007 commit: String,
1008 parent: String,
1009 },
1010 Reuse {
1011 commit: String,
1012 existing: String,
1013 #[serde(default)]
1015 parent: Option<String>,
1016 },
1017 Drop {
1018 commit: String,
1019 },
1020 Rewrite {
1021 commit: String,
1022 tree: Option<String>,
1023 message: Option<String>,
1024 author: Option<String>,
1025 },
1026}
1027
1028fn oid_hex(oid: &ObjectId) -> String {
1029 oid.to_hex()
1030}
1031
1032fn parse_oid(hex: &str) -> Result<ObjectId, RebaseSessionError> {
1033 ObjectId::from_hex(hex).map_err(|_| RebaseSessionError::InvalidStateOid {
1034 value: hex.to_owned(),
1035 })
1036}
1037
1038fn edits_to_persist(edits: &HistoryEdits) -> Vec<PersistedEdit> {
1039 edits
1040 .iter()
1041 .map(|(commit, edit)| match edit {
1042 CommitEdit::Reparent { parent } => PersistedEdit::Reparent {
1043 commit: oid_hex(commit),
1044 parent: oid_hex(parent),
1045 },
1046 CommitEdit::Reuse { existing, parent } => PersistedEdit::Reuse {
1047 commit: oid_hex(commit),
1048 existing: oid_hex(existing),
1049 parent: Some(oid_hex(parent)),
1050 },
1051 CommitEdit::Drop => PersistedEdit::Drop {
1052 commit: oid_hex(commit),
1053 },
1054 CommitEdit::Rewrite {
1055 tree,
1056 message,
1057 author,
1058 parents: _,
1059 } => PersistedEdit::Rewrite {
1060 commit: oid_hex(commit),
1061 tree: tree.as_ref().map(oid_hex),
1062 message: message.clone(),
1063 author: author.clone(),
1064 },
1065 CommitEdit::InsertBefore(_) | CommitEdit::SplitBefore { .. } => PersistedEdit::Drop {
1066 commit: oid_hex(commit),
1067 },
1068 })
1069 .collect()
1070}
1071
1072fn edits_from_persist(rows: &[PersistedEdit]) -> Result<HistoryEdits, RebaseSessionError> {
1073 let mut edits = HistoryEdits::new();
1074 for row in rows {
1075 match row {
1076 PersistedEdit::Reparent { commit, parent } => {
1077 edits.insert(
1078 parse_oid(commit)?,
1079 CommitEdit::Reparent {
1080 parent: parse_oid(parent)?,
1081 },
1082 );
1083 }
1084 PersistedEdit::Reuse {
1085 commit,
1086 existing,
1087 parent,
1088 } => {
1089 let existing = parse_oid(existing)?;
1090 let parent = parent.as_deref().map(parse_oid).transpose()?;
1091 edits.insert(
1092 parse_oid(commit)?,
1093 CommitEdit::Reuse {
1094 existing,
1095 parent: parent.unwrap_or(existing),
1096 },
1097 );
1098 }
1099 PersistedEdit::Drop { commit } => {
1100 edits.insert(parse_oid(commit)?, CommitEdit::Drop);
1101 }
1102 PersistedEdit::Rewrite {
1103 commit,
1104 tree,
1105 message,
1106 author,
1107 } => {
1108 edits.insert(
1109 parse_oid(commit)?,
1110 CommitEdit::Rewrite {
1111 tree: tree.as_deref().map(parse_oid).transpose()?,
1112 message: message.clone(),
1113 author: author.clone(),
1114 parents: None,
1115 },
1116 );
1117 }
1118 }
1119 }
1120 Ok(edits)
1121}
1122
1123fn mapping_to_persist(map: &HashMap<ObjectId, ObjectId>) -> HashMap<String, String> {
1124 map.iter().map(|(k, v)| (oid_hex(k), oid_hex(v))).collect()
1125}
1126
1127fn mapping_from_persist(
1128 map: &HashMap<String, String>,
1129) -> Result<HashMap<ObjectId, ObjectId>, RebaseSessionError> {
1130 map.iter()
1131 .map(|(k, v)| Ok((parse_oid(k)?, parse_oid(v)?)))
1132 .collect()
1133}
1134
1135fn replay_order_for_resume(state: &LoadedState, current: ObjectId) -> Vec<ObjectId> {
1137 if let Some(pos) = state.replay_order.iter().position(|c| *c == current) {
1138 return state.replay_order[pos..].to_vec();
1139 }
1140 let mut order = if state.remaining.is_empty() {
1141 state.replay_order.clone()
1142 } else {
1143 state.remaining.clone()
1144 };
1145 if !order.contains(¤t) {
1146 order.insert(0, current);
1147 } else if order.first() != Some(¤t) {
1148 order.retain(|c| *c != current);
1149 order.insert(0, current);
1150 }
1151 order
1152}
1153
1154#[allow(clippy::too_many_arguments)]
1155fn state_from_partial(
1156 partial: &super::plan::PartialRewritePlan,
1157 head_ref: &str,
1158 orig_head: ObjectId,
1159 request: &RebaseRequest,
1160 rebase: &RebaseOntoOptions,
1161 apply: &ApplyOptions,
1162 effective_onto: ObjectId,
1163 fork: ObjectId,
1164) -> PersistedRebaseState {
1165 let conflict = partial.conflict.commit;
1166 let stop_idx = partial
1167 .commits_to_replay
1168 .iter()
1169 .position(|c| *c == conflict)
1170 .unwrap_or(0);
1171 let done: Vec<ObjectId> = partial.commits_to_replay[..stop_idx]
1172 .iter()
1173 .copied()
1174 .filter(|c| partial.mapping.contains_key(c))
1175 .collect();
1176 let remaining: Vec<ObjectId> = partial.commits_to_replay[stop_idx..].to_vec();
1177 PersistedRebaseState {
1178 mapping: mapping_to_persist(&partial.mapping),
1179 rewritten: partial
1180 .rewritten
1181 .iter()
1182 .map(|r| PersistedRewrittenRow {
1183 old: oid_hex(&r.old),
1184 new: r.new.as_ref().map(oid_hex),
1185 reused: r.reused,
1186 })
1187 .collect(),
1188 edits: edits_to_persist(&partial.edits),
1189 rewrite: partial.opts.clone(),
1190 rebase: rebase.clone(),
1191 apply: apply.clone(),
1192 replay_order: partial.commits_to_replay.iter().map(oid_hex).collect(),
1193 remaining: remaining.iter().map(oid_hex).collect(),
1194 done: done.iter().map(oid_hex).collect(),
1195 fork: oid_hex(&fork),
1196 onto: oid_hex(&effective_onto),
1197 upstream: oid_hex(&request.upstream),
1198 head_ref: head_ref.to_owned(),
1199 orig_head: oid_hex(&orig_head),
1200 branch_tip: oid_hex(&request.branch_tip),
1201 flatten_leaf_merges: partial.linearize_onto.is_some(),
1202 scoped_refs: partial
1203 .scoped_refs
1204 .iter()
1205 .map(|(n, o)| (n.clone(), oid_hex(o)))
1206 .collect(),
1207 stopped_head: None,
1208 stopped_commit: Some(oid_hex(&conflict)),
1209 exec_commands: Vec::new(),
1210 }
1211}
1212
1213struct LoadedState {
1214 mapping: HashMap<ObjectId, ObjectId>,
1215 rewritten: Vec<RewrittenCommit>,
1216 edits: HistoryEdits,
1217 rewrite: RewriteOptions,
1218 rebase: RebaseOntoOptions,
1219 replay_order: Vec<ObjectId>,
1220 remaining: Vec<ObjectId>,
1221 head_ref: String,
1222 orig_head: ObjectId,
1223 onto: ObjectId,
1224 upstream: ObjectId,
1225 branch_tip: ObjectId,
1226 flatten_leaf_merges: bool,
1227 scoped_refs: Vec<(String, ObjectId)>,
1228 stopped_head: Option<ObjectId>,
1229 stopped_commit: Option<ObjectId>,
1230 exec_commands: Vec<String>,
1231}
1232
1233fn load_persisted_state(git_dir: &Path) -> Result<LoadedState, RebaseSessionError> {
1234 match active_rebase(git_dir) {
1235 None => return Err(RebaseSessionError::NoRebaseInProgress),
1236 Some(ActiveRebase::Foreign(_)) => return Err(RebaseSessionError::ForeignRebaseInProgress),
1237 Some(ActiveRebase::Exec) => return Err(RebaseSessionError::NoRebaseInProgress),
1238 Some(ActiveRebase::Grit) => {}
1239 }
1240 let path = git_dir.join("rebase-merge").join("grit-plan");
1241 let raw = fs::read_to_string(&path).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
1242 let parsed: PersistedRebaseState =
1243 serde_json::from_str(&raw).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
1244 Ok(LoadedState {
1245 mapping: mapping_from_persist(&parsed.mapping)?,
1246 rewritten: parsed
1247 .rewritten
1248 .iter()
1249 .map(|r| {
1250 Ok(RewrittenCommit {
1251 old: parse_oid(&r.old)?,
1252 new: r.new.as_deref().map(parse_oid).transpose()?,
1253 change_id: None,
1254 subject: None,
1255 reused: r.reused,
1256 })
1257 })
1258 .collect::<Result<_, RebaseSessionError>>()?,
1259 edits: edits_from_persist(&parsed.edits)?,
1260 rewrite: parsed.rewrite,
1261 rebase: parsed.rebase,
1262 replay_order: parsed
1263 .replay_order
1264 .iter()
1265 .map(|h| parse_oid(h))
1266 .collect::<Result<_, _>>()?,
1267 remaining: parsed
1268 .remaining
1269 .iter()
1270 .map(|h| parse_oid(h))
1271 .collect::<Result<_, _>>()?,
1272 head_ref: parsed.head_ref,
1273 orig_head: parse_oid(&parsed.orig_head)?,
1274 onto: parse_oid(&parsed.onto)?,
1275 upstream: parse_oid(&parsed.upstream)?,
1276 branch_tip: parse_oid(&parsed.branch_tip)?,
1277 flatten_leaf_merges: parsed.flatten_leaf_merges,
1278 scoped_refs: parsed
1279 .scoped_refs
1280 .iter()
1281 .map(|(name, hex)| parse_oid(hex).map(|oid| (name.clone(), oid)))
1282 .collect::<Result<_, _>>()?,
1283 stopped_head: parsed.stopped_head.as_deref().map(parse_oid).transpose()?,
1284 stopped_commit: parsed
1285 .stopped_commit
1286 .as_deref()
1287 .map(parse_oid)
1288 .transpose()?,
1289 exec_commands: parsed.exec_commands.clone(),
1290 })
1291}
1292
1293fn write_persisted_state(
1294 git_dir: &Path,
1295 state: &PersistedRebaseState,
1296) -> Result<(), RebaseSessionError> {
1297 let dir = git_dir.join("rebase-merge");
1298 fs::create_dir_all(&dir).map_err(map_lib)?;
1299 let lock_path = dir.join("grit-plan.lock");
1300 let mut lock = LockFile::acquire(&lock_path, LockWait::Immediately)
1301 .map_err(|e| RebaseSessionError::Library(crate::error::Error::Message(e.to_string())))?;
1302 let body = serde_json::to_vec_pretty(state).map_err(map_json)?;
1303 lock.write_all(&body).map_err(map_lib)?;
1304 lock.commit_to(&dir.join("grit-plan"))
1305 .map_err(|e| RebaseSessionError::Library(crate::error::Error::Message(e.to_string())))?;
1306 Ok(())
1307}
1308
1309#[allow(clippy::too_many_arguments)]
1310fn install_stopped_rebase(
1311 repo: &Repository,
1312 head_ref: &str,
1313 orig_head: ObjectId,
1314 request: &RebaseRequest,
1315 rebase_opts: &RebaseOntoOptions,
1316 apply: &ApplyOptions,
1317 partial: super::plan::PartialRewritePlan,
1318 exec_commands: Vec<String>,
1319 progress: &mut dyn ProgressSink,
1320 cancel: &dyn Cancel,
1321) -> Result<RebaseStop, RebaseSessionError> {
1322 poll_cancel(progress, cancel).map_err(map_lib)?;
1323 let effective_onto = request.onto.unwrap_or(request.upstream);
1324 let fork = super::rebase::rebase_fork(repo, request.upstream, request.branch_tip)?;
1325 let mut persisted = state_from_partial(
1326 &partial,
1327 head_ref,
1328 orig_head,
1329 request,
1330 rebase_opts,
1331 apply,
1332 effective_onto,
1333 fork,
1334 );
1335 let conflict = partial.conflict;
1336 persisted.stopped_head = Some(oid_hex(&conflict.replayed_parent));
1337 persisted.exec_commands = exec_commands;
1338 let done = persisted
1339 .done
1340 .iter()
1341 .map(|h| parse_oid(h))
1342 .collect::<Result<Vec<_>, _>>()?;
1343 let remaining = persisted
1344 .remaining
1345 .iter()
1346 .map(|h| parse_oid(h))
1347 .collect::<Result<Vec<_>, _>>()?;
1348 let git_progress = git_visible_rebase_progress(
1349 repo,
1350 fork,
1351 request.branch_tip,
1352 &rebase_opts.rewrite,
1353 &done,
1354 &remaining,
1355 conflict.commit,
1356 )?;
1357 write_git_rebase_merge_files(
1358 repo,
1359 head_ref,
1360 orig_head,
1361 effective_onto,
1362 &git_progress.done,
1363 &git_progress.remaining,
1364 git_progress.stop.as_ref(),
1365 )?;
1366 write_persisted_state(repo.git_dir(), &persisted)?;
1367
1368 repo.odb().flush_for_publish().map_err(map_lib)?;
1369 refs::update_direct_ref_on_store(
1370 repo.refs(),
1371 repo.git_dir(),
1372 "HEAD",
1373 &conflict.replayed_parent,
1374 )
1375 .map_err(map_lib)?;
1376 let mut progress = UncancellableProgress::new(progress);
1379 reset_index_worktree_to_head(repo, conflict.replayed_parent, &mut progress, &NeverCancel)?;
1380 let ours_tree = commit_tree(repo, conflict.replayed_parent)?;
1381 apply_conflict_merge_worktree(
1382 repo,
1383 &ours_tree,
1384 &conflict.merge,
1385 &mut progress,
1386 &NeverCancel,
1387 )
1388 .map_err(map_lib)?;
1389
1390 let message = load_commit(repo, conflict.commit)?.message;
1391 fs::write(repo.git_dir().join("MERGE_MSG"), &message).map_err(map_lib)?;
1392
1393 Ok(RebaseStop {
1394 commit: conflict.commit,
1395 conflicts: conflicts_from_tree_merge(&conflict.merge),
1396 done,
1397 remaining,
1398 })
1399}
1400
1401#[allow(clippy::too_many_arguments)]
1402fn advance_after_plan(
1403 repo: &Repository,
1404 prev: &LoadedState,
1405 apply: &ApplyOptions,
1406 outcome: ResumablePlanOutcome,
1407 head_before: ObjectId,
1408 progress: &mut dyn ProgressSink,
1409 cancel: &dyn Cancel,
1410) -> Result<RebaseOutcome, RebaseSessionError> {
1411 poll_cancel(progress, cancel).map_err(map_lib)?;
1412 match outcome {
1413 ResumablePlanOutcome::Complete(mut plan) => {
1414 if !prev.exec_commands.is_empty() {
1415 let spec = super::ExecSpec {
1416 commands: prev.exec_commands.clone(),
1417 };
1418 let runner = repo.command_runner();
1419 super::exec::verify_rebase_with_exec_resume(
1420 repo,
1421 &mut plan,
1422 &spec,
1423 &prev.rewrite,
1424 runner.as_ref(),
1425 )?;
1426 }
1427 if let Some(new_tip) = plan.mapped_tip(prev.orig_head) {
1428 if !plan.ref_updates.iter().any(|u| u.name == prev.head_ref) {
1429 plan.ref_updates.push(PlannedRefUpdate {
1430 name: prev.head_ref.clone(),
1431 old: prev.orig_head,
1432 new: new_tip,
1433 });
1434 }
1435 }
1436 let apply_without_hooks = ApplyOptions {
1437 hooks: PostOperationHooks::Skip,
1438 ..apply.clone()
1439 };
1440 let mut applied = apply_rewrite(repo, &plan, &apply_without_hooks, progress, cancel)?;
1441 if let Some(branch) = plan
1445 .ref_updates
1446 .iter()
1447 .find(|u| u.name == prev.head_ref && u.old != u.new)
1448 {
1449 log_head_move(repo, branch, apply)?;
1450 }
1451 refs::write_symbolic_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", &prev.head_ref)
1452 .map_err(map_lib)?;
1453 let branch_tip = repo.resolve_ref_oid(&prev.head_ref).map_err(map_lib)?;
1454 reset_index_worktree_to_head(repo, branch_tip, &mut NullProgress, &NeverCancel)?;
1456 applied.head = branch_tip;
1457 if let Some(operation) = apply.record_operation.as_deref() {
1458 let head_old_tree = commit_tree(repo, prev.orig_head)?;
1459 let head_new_tree = commit_tree(repo, branch_tip)?;
1460 let record = HistoryLastRecord {
1461 operation: operation.into(),
1462 head_ref: Some(prev.head_ref.clone()),
1463 head_old_commit: Some(prev.orig_head),
1464 head_new_commit: Some(branch_tip),
1465 head_old_tree: Some(head_old_tree),
1466 head_new_tree: Some(head_new_tree),
1467 ref_updates: plan.ref_updates.clone(),
1468 };
1469 write_history_last(repo, &record, None)?;
1470 }
1471 clear_rebase_state(repo.git_dir())?;
1472 if apply.hooks.enabled()
1473 && run_post_rewrite(repo, RewriteKind::Rebase, &plan.rewritten_pairs())
1474 {
1475 applied.hooks_run.push(HookName::PostRewrite);
1476 }
1477 Ok(RebaseOutcome::Done {
1478 head_change: (head_before != applied.head).then_some((head_before, applied.head)),
1479 applied,
1480 plan,
1481 })
1482 }
1483 ResumablePlanOutcome::Partial(partial) => {
1484 let request = RebaseRequest {
1485 upstream: prev.upstream,
1486 onto: Some(prev.onto),
1487 branch_tip: prev.branch_tip,
1488 };
1489 let stop = install_stopped_rebase(
1490 repo,
1491 &prev.head_ref,
1492 prev.orig_head,
1493 &request,
1494 &prev.rebase,
1495 apply,
1496 *partial,
1497 prev.exec_commands.clone(),
1498 progress,
1499 cancel,
1500 )?;
1501 Ok(RebaseOutcome::Stopped(stop))
1502 }
1503 }
1504}
1505
1506struct GitVisibleRebaseProgress {
1507 done: Vec<ObjectId>,
1508 remaining: Vec<ObjectId>,
1509 stop: Option<ObjectId>,
1510}
1511
1512fn git_visible_rebase_progress(
1518 repo: &Repository,
1519 fork: ObjectId,
1520 branch_tip: ObjectId,
1521 rewrite: &RewriteOptions,
1522 done: &[ObjectId],
1523 remaining: &[ObjectId],
1524 conflict: ObjectId,
1525) -> Result<GitVisibleRebaseProgress, RebaseSessionError> {
1526 if matches!(rewrite.scope, RefScope::Head) {
1527 return Ok(GitVisibleRebaseProgress {
1528 done: done.to_vec(),
1529 remaining: remaining.to_vec(),
1530 stop: Some(conflict),
1531 });
1532 }
1533 let primary = commits_in_rebase_range(repo, fork, &[branch_tip])?;
1534 let primary_set: HashSet<ObjectId> = primary.iter().copied().collect();
1535 let git_done: Vec<ObjectId> = done
1536 .iter()
1537 .copied()
1538 .filter(|oid| primary_set.contains(oid))
1539 .collect();
1540 let git_remaining: Vec<ObjectId> = remaining
1541 .iter()
1542 .copied()
1543 .filter(|oid| primary_set.contains(oid))
1544 .collect();
1545 let git_stop = primary_set.contains(&conflict).then_some(conflict);
1546 Ok(GitVisibleRebaseProgress {
1547 done: git_done,
1548 remaining: git_remaining,
1549 stop: git_stop,
1550 })
1551}
1552
1553fn write_git_rebase_merge_files(
1554 repo: &Repository,
1555 head_ref: &str,
1556 orig_head: ObjectId,
1557 onto: ObjectId,
1558 done: &[ObjectId],
1559 remaining: &[ObjectId],
1560 current: Option<&ObjectId>,
1561) -> Result<(), RebaseSessionError> {
1562 let dir = repo.git_dir().join("rebase-merge");
1563 fs::create_dir_all(&dir).map_err(map_lib)?;
1564 fs::write(dir.join("head-name"), format!("{head_ref}\n")).map_err(map_lib)?;
1565 fs::write(dir.join("onto"), format!("{}\n", onto.to_hex())).map_err(map_lib)?;
1566 fs::write(dir.join("orig-head"), format!("{}\n", orig_head.to_hex())).map_err(map_lib)?;
1567 let todo_remaining: Vec<ObjectId> = match current {
1568 Some(current) if remaining.first() == Some(current) => {
1569 remaining.iter().skip(1).copied().collect()
1570 }
1571 _ => remaining.to_vec(),
1572 };
1573 fs::write(
1574 dir.join("git-rebase-todo"),
1575 todo_lines(repo, &todo_remaining)?,
1576 )
1577 .map_err(map_lib)?;
1578 let done_with_current: Vec<ObjectId> = match current {
1581 Some(current) => done.iter().chain([current]).copied().collect(),
1582 None => done.to_vec(),
1583 };
1584 fs::write(dir.join("done"), todo_lines(repo, &done_with_current)?).map_err(map_lib)?;
1585 let msgnum = done_with_current.len();
1586 let end = done_with_current.len() + todo_remaining.len();
1587 fs::write(dir.join("msgnum"), format!("{msgnum}\n")).map_err(map_lib)?;
1588 fs::write(dir.join("end"), format!("{end}\n")).map_err(map_lib)?;
1589 if let Some(current) = current {
1590 fs::write(dir.join("stopped-sha"), format!("{}\n", current.to_hex())).map_err(map_lib)?;
1591 let commit = load_commit(repo, *current)?;
1592 match author_script(&commit.author) {
1593 Some(script) => fs::write(dir.join("author-script"), script).map_err(map_lib)?,
1594 None => remove_if_exists(&dir.join("author-script"))?,
1595 }
1596 fs::write(dir.join("message"), commit.message).map_err(map_lib)?;
1597 } else {
1598 remove_if_exists(&dir.join("stopped-sha"))?;
1599 remove_if_exists(&dir.join("author-script"))?;
1600 remove_if_exists(&dir.join("message"))?;
1601 }
1602 Ok(())
1603}
1604
1605fn todo_lines(repo: &Repository, commits: &[ObjectId]) -> Result<String, RebaseSessionError> {
1606 let mut out = String::new();
1607 for &oid in commits {
1608 let subject = super::subject::commit_message_subject_line(&load_commit(repo, oid)?.message);
1609 let abbrev = oid.to_short_hex();
1610 out.push_str(&format!("pick {abbrev} {subject}\n"));
1611 }
1612 Ok(out)
1613}
1614
1615fn author_script(author: &str) -> Option<String> {
1619 let lt = author.find('<')?;
1620 let gt = lt + author[lt..].find('>')?;
1621 let name = author[..lt].trim_end();
1622 let email = &author[lt + 1..gt];
1623 let date = format!("@{}", author[gt + 1..].trim());
1624 let quote = crate::transport::sq_quote_shell_arg;
1625 Some(format!(
1626 "GIT_AUTHOR_NAME={}\nGIT_AUTHOR_EMAIL={}\nGIT_AUTHOR_DATE={}\n",
1627 quote(name),
1628 quote(email),
1629 quote(&date)
1630 ))
1631}
1632
1633pub(super) fn clear_rebase_state(git_dir: &Path) -> Result<(), RebaseSessionError> {
1634 remove_if_exists(&git_dir.join("MERGE_MSG"))?;
1635 let dir = git_dir.join("rebase-merge");
1636 if dir.is_dir() {
1637 fs::remove_dir_all(&dir).map_err(map_lib)?;
1638 }
1639 Ok(())
1640}
1641
1642fn branch_head(repo: &Repository) -> Result<(String, ObjectId), RebaseSessionError> {
1643 match resolve_head_for_repository(repo).map_err(map_lib)? {
1644 HeadState::Branch {
1645 refname,
1646 oid: Some(oid),
1647 ..
1648 } => Ok((refname, oid)),
1649 HeadState::Branch { oid: None, .. } | HeadState::Detached { .. } | HeadState::Invalid => {
1650 Err(RebaseSessionError::NotOnBranch)
1651 }
1652 }
1653}
1654
1655fn resolve_head_oid(repo: &Repository) -> Result<ObjectId, RebaseSessionError> {
1656 resolve_head_for_repository(repo)
1657 .map_err(map_lib)?
1658 .oid()
1659 .copied()
1660 .ok_or(RebaseSessionError::NotOnBranch)
1661}
1662
1663fn reset_index_worktree_to_head(
1664 repo: &Repository,
1665 head_oid: ObjectId,
1666 progress: &mut dyn ProgressSink,
1667 cancel: &dyn Cancel,
1668) -> Result<(), RebaseSessionError> {
1669 let head_tree = commit_tree(repo, head_oid)?;
1670 checkout_hard_to_tree_from_head_tree(repo, &head_tree, &head_tree, progress, cancel)
1673 .map_err(map_lib)?;
1674 Ok(())
1675}
1676
1677fn hard_reset_branch(
1680 repo: &Repository,
1681 refname: &str,
1682 target: ObjectId,
1683 identity: &crate::ident::Signature,
1684) -> Result<RebaseAbort, RebaseSessionError> {
1685 let before = repo
1686 .references()
1687 .resolve_if_exists(refname)
1688 .map_err(map_lib)?;
1689 refs::write_symbolic_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", refname)
1690 .map_err(map_lib)?;
1691 hard_reset_head(repo, target, identity)?;
1692 let overwritten = (before != Some(target)).then(|| PlannedRefUpdate {
1693 name: refname.to_owned(),
1694 old: before.unwrap_or_else(crate::diff::zero_oid),
1695 new: target,
1696 });
1697 Ok(RebaseAbort {
1698 overwritten: overwritten.into_iter().collect(),
1699 })
1700}
1701
1702fn hard_reset_head(
1703 repo: &Repository,
1704 target: ObjectId,
1705 identity: &crate::ident::Signature,
1706) -> Result<(), RebaseSessionError> {
1707 reset_to_commit(
1708 repo,
1709 &ResetOptions::new(target, ResetMode::Hard, identity.clone(), "rebase (abort)"),
1710 &mut crate::progress::NullProgress,
1711 &crate::progress::NeverCancel,
1712 )
1713 .map_err(map_lib)?;
1714 Ok(())
1715}
1716
1717use std::io::Write;
1718
1719fn map_lib<E: Into<crate::error::Error>>(err: E) -> RebaseSessionError {
1720 RebaseSessionError::Library(err.into())
1721}
1722
1723fn replan_rewrite_options(state: &LoadedState) -> RewriteOptions {
1724 state.rewrite.clone()
1725}
1726
1727fn map_json(err: serde_json::Error) -> RebaseSessionError {
1728 RebaseSessionError::Library(crate::error::Error::Message(err.to_string()))
1729}
1730
1731#[cfg(test)]
1732mod tests {
1733 use super::{
1734 author_script, edits_from_persist, edits_to_persist, parse_oid, PersistedEdit,
1735 RebaseSessionError,
1736 };
1737 use crate::history::types::{CommitEdit, HistoryEdits};
1738 use crate::objects::ObjectId;
1739
1740 fn oid(byte: u8) -> ObjectId {
1741 ObjectId::from_bytes(&[byte; 20]).unwrap_or_else(|_| ObjectId::zero())
1742 }
1743
1744 #[test]
1745 fn reuse_edit_round_trips_its_continuation_parent() {
1746 let mut edits = HistoryEdits::new();
1747 edits.insert(
1748 oid(1),
1749 CommitEdit::Reuse {
1750 existing: oid(2),
1751 parent: oid(3),
1752 },
1753 );
1754 let rows = edits_to_persist(&edits);
1755 let json = serde_json::to_string(&rows).unwrap_or_default();
1756 let parsed: Vec<PersistedEdit> = serde_json::from_str(&json).unwrap_or_default();
1757 assert_eq!(edits_from_persist(&parsed).ok(), Some(edits));
1758 }
1759
1760 #[test]
1761 fn reuse_edit_saved_without_parent_continues_from_the_reused_commit() {
1762 let json = format!(
1763 r#"[{{"Reuse":{{"commit":"{}","existing":"{}"}}}}]"#,
1764 oid(1).to_hex(),
1765 oid(2).to_hex()
1766 );
1767 let parsed: Vec<PersistedEdit> = serde_json::from_str(&json).unwrap_or_default();
1768 let edits = edits_from_persist(&parsed).ok();
1769 assert_eq!(
1770 edits.and_then(|edits| edits.get(&oid(1)).cloned()),
1771 Some(CommitEdit::Reuse {
1772 existing: oid(2),
1773 parent: oid(2),
1774 })
1775 );
1776 }
1777
1778 #[test]
1779 fn parse_oid_reports_the_bad_value() {
1780 assert_eq!(parse_oid(&oid(7).to_hex()).ok(), Some(oid(7)));
1781 let err = parse_oid("abc").err();
1782 assert!(
1783 matches!(&err, Some(RebaseSessionError::InvalidStateOid { value }) if value == "abc"),
1784 "{err:?}"
1785 );
1786 }
1787
1788 #[test]
1789 fn persisted_edit_with_bad_oid_is_rejected() {
1790 let rows = vec![PersistedEdit::Drop {
1791 commit: "zz".into(),
1792 }];
1793 assert!(matches!(
1794 edits_from_persist(&rows),
1795 Err(RebaseSessionError::InvalidStateOid { value }) if value == "zz"
1796 ));
1797 }
1798
1799 #[test]
1800 fn author_script_quotes_values_and_prefixes_date() {
1801 assert_eq!(
1802 author_script("A U Thor <a@example.com> 1700000000 -0130").as_deref(),
1803 Some(
1804 "GIT_AUTHOR_NAME='A U Thor'\nGIT_AUTHOR_EMAIL='a@example.com'\nGIT_AUTHOR_DATE='@1700000000 -0130'\n"
1805 )
1806 );
1807 assert_eq!(
1808 author_script("O'Brien <o@example.com> 1 +0000").as_deref(),
1809 Some(
1810 "GIT_AUTHOR_NAME='O'\\''Brien'\nGIT_AUTHOR_EMAIL='o@example.com'\nGIT_AUTHOR_DATE='@1 +0000'\n"
1811 )
1812 );
1813 assert_eq!(author_script("no email here 1 +0000"), None);
1814 }
1815}