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::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#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct StartRebaseOptions {
53 pub request: RebaseRequest,
55 pub rebase: RebaseOntoOptions,
57 pub apply: ApplyOptions,
59 pub pre_rebase: PreRebase,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
66#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
67pub struct RebaseStop {
68 pub commit: ObjectId,
70 pub conflicts: Vec<Conflict>,
72 pub done: Vec<ObjectId>,
74 pub remaining: Vec<ObjectId>,
76}
77
78#[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 Done {
85 applied: AppliedRewrite,
87 plan: Box<RewritePlan>,
89 head_change: Option<(ObjectId, ObjectId)>,
96 },
97 ExecDone {
100 applied: AppliedRewrite,
102 plan: Box<RewritePlan>,
104 exec: super::ExecReport,
107 },
108 Stopped(RebaseStop),
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Default)]
114#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
115pub struct RebaseAbort {
116 pub overwritten: Vec<PlannedRefUpdate>,
121}
122
123#[derive(Debug, thiserror::Error)]
125#[non_exhaustive]
126pub enum RebaseSessionError {
127 #[error("no rebase in progress")]
129 NoRebaseInProgress,
130
131 #[error("rebase is already in progress")]
133 RebaseAlreadyInProgress,
134
135 #[error("a rebase started by git is in progress")]
141 ForeignRebaseInProgress,
142
143 #[error("cannot continue: unmerged paths remain in the index")]
145 UnmergedPaths,
146
147 #[error("nothing to commit after conflict resolution")]
149 NothingToCommit,
150
151 #[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 expected: ObjectId,
159 },
160
161 #[error("rebase session requires HEAD on a branch")]
163 NotOnBranch,
164
165 #[error("exec replay HEAD changed; expected {expected}; save new commits on a branch, then abort and restart the rebase")]
168 ExecHeadChanged {
169 expected: ObjectId,
171 },
172
173 #[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 name: String,
183 expected: ObjectId,
185 actual: Option<ObjectId>,
187 },
188
189 #[error("ref {name} moved while aborting the rebase; retry the abort")]
192 AbortRefMoved {
193 name: String,
195 },
196
197 #[error("rebase state records an invalid object id: {value:?}")]
200 InvalidStateOid {
201 value: String,
203 },
204
205 #[error(transparent)]
207 History(#[from] HistoryError),
208
209 #[error(transparent)]
211 Library(#[from] crate::error::Error),
212}
213
214impl RebaseSessionError {
215 #[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
236pub 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
322pub 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
437pub(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, ¤t_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
560pub 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 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
690pub 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
763pub 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
841enum ActiveRebase {
843 Exec,
845 Grit,
847 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#[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 #[serde(default)]
902 stopped_head: Option<String>,
903 #[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 #[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
1047fn 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(¤t) {
1058 order.insert(0, current);
1059 } else if order.first() != Some(¤t) {
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 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 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 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
1407fn 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 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
1510fn 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 checkout_hard_to_tree_from_head_tree(repo, &head_tree, &head_tree, progress, cancel)
1568 .map_err(map_lib)?;
1569 Ok(())
1570}
1571
1572fn 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}