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 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(transparent)]
198 History(#[from] HistoryError),
199
200 #[error(transparent)]
202 Library(#[from] crate::error::Error),
203}
204
205impl RebaseSessionError {
206 #[must_use]
208 pub fn is_cancelled(&self) -> bool {
209 match self {
210 Self::Library(e) => e.is_cancelled(),
211 Self::History(e) => e.is_cancelled(),
212 Self::NoRebaseInProgress
213 | Self::RebaseAlreadyInProgress
214 | Self::ForeignRebaseInProgress
215 | Self::UnmergedPaths
216 | Self::NothingToCommit
217 | Self::ResolutionHeadChanged { .. }
218 | Self::NotOnBranch
219 | Self::ExecHeadChanged { .. }
220 | Self::StaleExecPlan { .. }
221 | Self::AbortRefMoved { .. } => false,
222 }
223 }
224}
225
226pub fn start_rebase(
257 repo: &Repository,
258 opts: &StartRebaseOptions,
259 progress: &mut dyn ProgressSink,
260 cancel: &dyn Cancel,
261) -> Result<RebaseOutcome, RebaseSessionError> {
262 super::ensure_rewrite_apply_ready(repo)?;
263 if let Some(active) = active_rebase(repo.git_dir()) {
264 return Err(match active {
265 ActiveRebase::Foreign(_) => RebaseSessionError::ForeignRebaseInProgress,
266 ActiveRebase::Exec | ActiveRebase::Grit => RebaseSessionError::RebaseAlreadyInProgress,
267 });
268 }
269 let (head_ref, orig_head) = branch_head(repo)?;
270 let plan = plan_rebase_resumable(repo, &opts.request, &opts.rebase, &opts.pre_rebase)?;
271 poll_cancel(progress, cancel).map_err(map_lib)?;
272 match plan {
273 Err(RebasePlan::UpToDate) => Ok(up_to_date_outcome(orig_head)),
274 Err(RebasePlan::Plan(_) | RebasePlan::Conflict { .. }) => Err(HistoryError::Planning {
275 message: "unexpected non-resumable rebase plan".into(),
276 }
277 .into()),
278 Ok(ResumablePlanOutcome::Complete(plan)) => {
279 let applied = apply_rewrite(repo, &plan, &opts.apply, progress, cancel)?;
280 write_orig_head(repo, orig_head).map_err(map_lib)?;
281 Ok(RebaseOutcome::Done {
282 head_change: (orig_head != applied.head).then_some((orig_head, applied.head)),
283 applied,
284 plan,
285 })
286 }
287 Ok(ResumablePlanOutcome::Partial(partial)) => {
288 let orig_tree = commit_tree(repo, orig_head)?;
289 let stop_tree = commit_tree(repo, partial.conflict.replayed_parent)?;
290 ensure_safe_for_rebase_stop(repo, &orig_tree, &stop_tree)?;
291 let stop = install_stopped_rebase(
292 repo,
293 &head_ref,
294 orig_head,
295 &opts.request,
296 &opts.rebase,
297 &opts.apply,
298 *partial,
299 progress,
300 cancel,
301 )?;
302 write_orig_head(repo, orig_head).map_err(map_lib)?;
303 if opts.apply.hooks.enabled() {
304 let onto = opts.request.onto.unwrap_or(opts.request.upstream);
305 run_post_checkout(repo, Some(orig_head), onto, true);
306 }
307 Ok(RebaseOutcome::Stopped(stop))
308 }
309 }
310}
311
312pub fn start_rebase_with_exec(
337 repo: &Repository,
338 opts: &StartRebaseOptions,
339 spec: &super::ExecSpec,
340 runner: &dyn crate::command_runner::CommandRunner,
341 progress: &mut dyn ProgressSink,
342 cancel: &dyn Cancel,
343) -> Result<RebaseOutcome, RebaseSessionError> {
344 super::ensure_rewrite_apply_ready(repo)?;
345 let orig_head = opts.request.branch_tip;
346 let mut plan = match super::rebase::plan_rebase_with_hook(
347 repo,
348 &opts.request,
349 &opts.rebase,
350 &opts.pre_rebase,
351 )? {
352 RebasePlan::UpToDate => {
353 if spec.commands.is_empty() {
354 return Ok(up_to_date_outcome(orig_head));
355 }
356 let fork =
357 super::rebase::rebase_fork(repo, opts.request.upstream, opts.request.branch_tip)?;
358 let effective_onto = opts.request.onto.unwrap_or(opts.request.upstream);
359 if opts.request.branch_tip == effective_onto {
360 return Ok(up_to_date_outcome(orig_head));
361 }
362 let range =
363 super::rebase::commits_in_rebase_range(repo, fork, &[opts.request.branch_tip])?;
364 if range.is_empty() {
365 return Ok(up_to_date_outcome(orig_head));
366 }
367 opts.pre_rebase.run(repo)?;
368 super::rebase::plan_rebase_identity_exec(
369 repo,
370 fork,
371 effective_onto,
372 &range,
373 &opts.rebase,
374 )?
375 }
376 RebasePlan::Conflict {
377 commit,
378 subject,
379 conflicts,
380 refs,
381 } => {
382 return Err(HistoryError::Conflict {
383 commit,
384 subject,
385 conflicts,
386 refs,
387 }
388 .into());
389 }
390 RebasePlan::Plan(plan) => plan,
391 };
392 super::apply::verify_rewrite_plan(repo, &plan)?;
393 let exec =
394 match super::verify_rebase_with_exec(repo, &mut plan, spec, &opts.rebase.rewrite, runner) {
395 Ok(exec) => exec,
396 Err(stop @ (HistoryError::ExecDirty { .. } | HistoryError::ExecConflict { .. })) => {
397 write_orig_head(repo, orig_head).map_err(map_lib)?;
398 return Err(stop.into());
399 }
400 Err(err) => return Err(err.into()),
401 };
402 let applied = apply_rewrite(repo, &plan, &opts.apply, progress, cancel)?;
403 write_orig_head(repo, orig_head).map_err(map_lib)?;
404 Ok(RebaseOutcome::ExecDone {
405 applied,
406 plan: Box::new(plan),
407 exec,
408 })
409}
410
411fn up_to_date_outcome(head: ObjectId) -> RebaseOutcome {
412 RebaseOutcome::Done {
413 head_change: None,
414 applied: AppliedRewrite {
415 ref_updates: Vec::new(),
416 head,
417 hooks_run: Vec::new(),
418 },
419 plan: Box::new(RewritePlan {
420 commits: Vec::new(),
421 ref_updates: Vec::new(),
422 head: super::types::HeadEffect::default(),
423 }),
424 }
425}
426
427pub(crate) fn persist_rebase_stopped_head_if_missing(
430 repo: &Repository,
431) -> Result<(), RebaseSessionError> {
432 if !crate::state::grit_rebase_conflict_stop_active(repo.git_dir()) {
433 return Ok(());
434 }
435 let path = repo.git_dir().join("rebase-merge/grit-plan");
436 let raw = fs::read_to_string(&path).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
437 let mut parsed: PersistedRebaseState =
438 serde_json::from_str(&raw).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
439 if parsed.stopped_head.is_some() {
440 return Ok(());
441 }
442 let head = resolve_head_oid(repo)?;
443 parsed.stopped_head = Some(oid_hex(&head));
444 write_persisted_state(repo.git_dir(), &parsed)
445}
446
447fn replay_base_parent(state: &LoadedState, oid: ObjectId) -> bool {
448 oid == state.onto || state.mapping.values().any(|mapped| *mapped == oid)
449}
450
451fn stopped_head_for_continue(
452 state: &LoadedState,
453 repo: &Repository,
454) -> Result<ObjectId, RebaseSessionError> {
455 if let Some(oid) = state.stopped_head {
456 return Ok(oid);
457 }
458 let head = resolve_head_oid(repo)?;
459 if replay_base_parent(state, head) {
460 return Ok(head);
461 }
462 let head_commit = load_commit(repo, head)?;
463 if head_commit.parents.len() == 1 {
464 if let Some(&parent) = head_commit.parents.first() {
465 if replay_base_parent(state, parent) {
466 return Ok(parent);
467 }
468 }
469 } else {
470 for &parent in &head_commit.parents {
471 if replay_base_parent(state, parent) {
472 return Ok(parent);
473 }
474 }
475 }
476 Err(RebaseSessionError::NoRebaseInProgress)
477}
478
479fn is_first_parent_descendant_of(
480 repo: &Repository,
481 ancestor: ObjectId,
482 tip: ObjectId,
483) -> Result<bool, RebaseSessionError> {
484 if tip == ancestor {
485 return Ok(false);
486 }
487 let mut current = Some(tip);
488 while let Some(oid) = current {
489 let parents = commit_parents(repo, oid).map_err(RebaseSessionError::from)?;
490 let Some(first) = parents.first().copied() else {
491 break;
492 };
493 if first == ancestor {
494 return Ok(true);
495 }
496 current = Some(first);
497 }
498 Ok(false)
499}
500
501fn advance_after_user_resolution_commit(
502 repo: &Repository,
503 state: &LoadedState,
504 opts: &StartRebaseOptions,
505 current: ObjectId,
506 head_oid: ObjectId,
507 progress: &mut dyn ProgressSink,
508 cancel: &dyn Cancel,
509) -> Result<RebaseOutcome, RebaseSessionError> {
510 let current_data = load_commit(repo, current)?;
511 let mut mapping = state.mapping.clone();
512 mapping.insert(current, head_oid);
513 let mut rewritten = state.rewritten.clone();
514 rewritten.push(RewrittenCommit {
515 old: current,
516 new: Some(head_oid),
517 change_id: logical_change_id_for_commit(repo, head_oid).map_err(map_lib)?,
518 subject: super::subject::subject_for_rewrite_row(repo, ¤t_data, Some(head_oid))
519 .map_err(RebaseSessionError::from)?,
520 reused: false,
521 });
522 let resume_order: Vec<ObjectId> = replay_order_for_resume(state, current)
523 .into_iter()
524 .filter(|commit| *commit != current)
525 .collect();
526 let mut edits = state.edits.clone();
527 edits.retain(|commit, _| resume_order.contains(commit));
528 let rewrite = replan_rewrite_options(state);
529 let outcome = plan_rewrite_with_order_resumable_seeded(
530 repo,
531 &edits,
532 &rewrite,
533 Some(&resume_order),
534 state.flatten_leaf_merges.then_some(state.onto),
535 state.rebase.favor,
536 Some((mapping, rewritten)),
537 Some(state.scoped_refs.clone()),
538 )?;
539 advance_after_plan(
540 repo,
541 state,
542 &opts.apply,
543 outcome,
544 head_oid,
545 progress,
546 cancel,
547 )
548}
549
550pub fn continue_rebase(
570 repo: &Repository,
571 opts: &StartRebaseOptions,
572 progress: &mut dyn ProgressSink,
573 cancel: &dyn Cancel,
574) -> Result<RebaseOutcome, RebaseSessionError> {
575 if matches!(active_rebase(repo.git_dir()), Some(ActiveRebase::Exec)) {
576 return super::exec::resume_exec_rebase(repo, &opts.apply, false, progress, cancel);
577 }
578 super::ensure_rewrite_apply_ready(repo)?;
579 let state = load_persisted_state(repo.git_dir())?;
580 if repo.load_index()?.has_unmerged_entries() {
581 return Err(RebaseSessionError::UnmergedPaths);
582 }
583 let current = state
584 .stopped_commit
585 .or_else(|| state.remaining.first().copied())
586 .ok_or(RebaseSessionError::NoRebaseInProgress)?;
587 let new_tree = {
588 let mut index = repo.load_index()?;
589 write_tree_update_index(repo.odb(), &mut index, "", WriteTreeFlags::silent())
590 .map_err(map_lib)?
591 };
592 let head_oid = resolve_head_oid(repo)?;
593 let head_tree = commit_tree(repo, head_oid)?;
594 if new_tree == head_tree {
595 let stopped_head = stopped_head_for_continue(&state, repo)?;
596 if head_oid == stopped_head {
597 return Err(RebaseSessionError::NothingToCommit);
598 }
599 if is_first_parent_descendant_of(repo, stopped_head, head_oid)? {
600 ensure_worktree_clean_for_rebase_exec(repo).map_err(map_lib)?;
601 return advance_after_user_resolution_commit(
602 repo, &state, opts, current, head_oid, progress, cancel,
603 );
604 }
605 return Err(RebaseSessionError::ResolutionHeadChanged {
606 expected: stopped_head,
607 });
608 }
609
610 let original_parents = commit_parents(repo, current).map_err(RebaseSessionError::from)?;
611 let parents = if original_parents.len() <= 1 {
612 vec![head_oid]
613 } else {
614 let mut mapped = vec![head_oid];
615 mapped.extend(
616 original_parents[1..]
617 .iter()
618 .map(|parent| state.mapping.get(parent).copied().unwrap_or(*parent)),
619 );
620 mapped
621 };
622
623 if state.remaining.len() > 1 {
627 let stopped_head = stopped_head_for_continue(&state, repo)?;
628 if head_oid != stopped_head {
629 return Err(RebaseSessionError::ResolutionHeadChanged {
630 expected: stopped_head,
631 });
632 }
633 let resolved =
634 write_rebase_staged_resolution(repo, &state.rewrite, current, new_tree, &parents)
635 .map_err(RebaseSessionError::from)?;
636 refs::update_direct_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", &resolved)
637 .map_err(map_lib)?;
638 ensure_worktree_clean_for_rebase_exec(repo).map_err(map_lib)?;
639 return advance_after_user_resolution_commit(
640 repo, &state, opts, current, resolved, progress, cancel,
641 );
642 }
643
644 let resume_order = replay_order_for_resume(&state, current);
645 let mut edits = state.edits.clone();
646 edits.insert(
647 current,
648 CommitEdit::Rewrite {
649 tree: Some(new_tree),
650 message: None,
651 author: None,
652 parents: Some(parents),
653 },
654 );
655 edits.retain(|commit, _| resume_order.contains(commit));
656
657 let seed = (state.mapping.clone(), state.rewritten.clone());
658 let rewrite = replan_rewrite_options(&state);
659 let outcome = plan_rewrite_with_order_resumable_seeded(
660 repo,
661 &edits,
662 &rewrite,
663 Some(&resume_order),
664 state.flatten_leaf_merges.then_some(state.onto),
665 state.rebase.favor,
666 Some(seed),
667 Some(state.scoped_refs.clone()),
668 )?;
669 advance_after_plan(
670 repo,
671 &state,
672 &opts.apply,
673 outcome,
674 head_oid,
675 progress,
676 cancel,
677 )
678}
679
680pub fn skip_rebase(
694 repo: &Repository,
695 opts: &StartRebaseOptions,
696 progress: &mut dyn ProgressSink,
697 cancel: &dyn Cancel,
698) -> Result<RebaseOutcome, RebaseSessionError> {
699 if matches!(active_rebase(repo.git_dir()), Some(ActiveRebase::Exec)) {
700 return super::exec::resume_exec_rebase(repo, &opts.apply, true, progress, cancel);
701 }
702 let state = load_persisted_state(repo.git_dir())?;
703 let current = state
704 .remaining
705 .first()
706 .copied()
707 .ok_or(RebaseSessionError::NoRebaseInProgress)?;
708 let head_oid = resolve_head_oid(repo)?;
709 reset_index_worktree_to_head(repo, head_oid, progress, cancel)?;
710
711 let replayed_parent = head_oid;
712 let mut mapping = state.mapping.clone();
713 mapping.insert(current, replayed_parent);
714 let mut rewritten = state.rewritten.clone();
715 rewritten.push(RewrittenCommit {
716 old: current,
717 new: None,
718 change_id: None,
719 subject: None,
720 reused: false,
721 });
722
723 let resume_order: Vec<ObjectId> = state
724 .remaining
725 .iter()
726 .copied()
727 .filter(|c| *c != current)
728 .collect();
729 let mut edits = state.edits.clone();
730 edits.retain(|commit, _| resume_order.contains(commit));
731 let rewrite = replan_rewrite_options(&state);
732 let outcome = plan_rewrite_with_order_resumable_seeded(
733 repo,
734 &edits,
735 &rewrite,
736 Some(&resume_order),
737 state.flatten_leaf_merges.then_some(state.onto),
738 state.rebase.favor,
739 Some((mapping, rewritten)),
740 Some(state.scoped_refs.clone()),
741 )?;
742 advance_after_plan(
743 repo,
744 &state,
745 &opts.apply,
746 outcome,
747 head_oid,
748 progress,
749 cancel,
750 )
751}
752
753pub fn abort_rebase(
774 repo: &Repository,
775 identity: &crate::ident::Signature,
776) -> Result<RebaseAbort, RebaseSessionError> {
777 identity
778 .validate()
779 .map_err(|err| map_lib(crate::error::Error::InvalidSignature(err)))?;
780 let had_journal = super::rewrite_apply_journal_active(repo.git_dir());
781 if had_journal {
782 super::abort_interrupted_rewrite_apply(repo)?;
783 }
784 match active_rebase(repo.git_dir()) {
785 None => {
786 if had_journal {
787 Ok(RebaseAbort::default())
788 } else {
789 Err(RebaseSessionError::NoRebaseInProgress)
790 }
791 }
792 Some(ActiveRebase::Foreign(dir)) => abort_foreign_rebase(repo, &dir, identity),
793 Some(ActiveRebase::Exec) => super::exec::abort_exec_rebase(repo, identity),
794 Some(ActiveRebase::Grit) => {
795 let state = load_persisted_state(repo.git_dir())?;
796 crate::rerere::rerere_clear(repo.git_dir()).map_err(map_lib)?;
797 let abort = hard_reset_branch(repo, &state.head_ref, state.orig_head, identity)?;
798 clear_rebase_state(repo.git_dir())?;
799 Ok(abort)
800 }
801 }
802}
803
804fn abort_foreign_rebase(
805 repo: &Repository,
806 state_dir: &Path,
807 identity: &crate::ident::Signature,
808) -> Result<RebaseAbort, RebaseSessionError> {
809 if state_dir.join("autostash").exists() {
810 return Err(RebaseSessionError::ForeignRebaseInProgress);
811 }
812 let orig_head = first_line(&state_dir.join("orig-head"))
813 .and_then(|hex| ObjectId::from_hex(&hex).ok())
814 .ok_or(RebaseSessionError::ForeignRebaseInProgress)?;
815 crate::rerere::rerere_clear(repo.git_dir()).map_err(map_lib)?;
816 let abort =
817 match first_line(&state_dir.join("head-name")).filter(|name| name.starts_with("refs/")) {
818 Some(head_ref) => hard_reset_branch(repo, &head_ref, orig_head, identity)?,
819 None => {
820 hard_reset_head(repo, orig_head, identity)?;
821 RebaseAbort::default()
822 }
823 };
824 for name in ["REBASE_HEAD", "AUTO_MERGE", "MERGE_MSG"] {
825 remove_if_exists(&repo.git_dir().join(name))?;
826 }
827 fs::remove_dir_all(state_dir).map_err(map_lib)?;
828 Ok(abort)
829}
830
831enum ActiveRebase {
833 Exec,
835 Grit,
837 Foreign(std::path::PathBuf),
839}
840
841fn active_rebase(git_dir: &Path) -> Option<ActiveRebase> {
842 let merge = git_dir.join("rebase-merge");
843 if merge.join("grit-exec").is_file() {
844 return Some(ActiveRebase::Exec);
845 }
846 if merge.join("grit-plan").is_file() {
847 return Some(ActiveRebase::Grit);
848 }
849 if merge.is_dir() {
850 return Some(ActiveRebase::Foreign(merge));
851 }
852 let apply = git_dir.join("rebase-apply");
853 (apply.is_dir() && !apply.join("applying").exists()).then_some(ActiveRebase::Foreign(apply))
854}
855
856fn first_line(path: &Path) -> Option<String> {
857 let raw = fs::read_to_string(path).ok()?;
858 let line = raw.lines().next()?.trim();
859 (!line.is_empty()).then(|| line.to_owned())
860}
861
862fn remove_if_exists(path: &Path) -> Result<(), RebaseSessionError> {
863 match fs::remove_file(path) {
864 Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(map_lib(e)),
865 _ => Ok(()),
866 }
867}
868
869#[derive(Debug, Clone, Serialize, Deserialize)]
872struct PersistedRebaseState {
873 mapping: HashMap<String, String>,
874 rewritten: Vec<PersistedRewrittenRow>,
875 edits: Vec<PersistedEdit>,
876 rewrite: RewriteOptions,
877 rebase: RebaseOntoOptions,
878 apply: ApplyOptions,
879 replay_order: Vec<String>,
880 remaining: Vec<String>,
881 done: Vec<String>,
882 fork: String,
883 onto: String,
884 upstream: String,
885 head_ref: String,
886 orig_head: String,
887 branch_tip: String,
888 flatten_leaf_merges: bool,
889 scoped_refs: Vec<(String, String)>,
890 #[serde(default)]
892 stopped_head: Option<String>,
893 #[serde(default)]
895 stopped_commit: Option<String>,
896}
897
898#[derive(Debug, Clone, Serialize, Deserialize)]
899struct PersistedRewrittenRow {
900 old: String,
901 new: Option<String>,
902 #[serde(default)]
903 reused: bool,
904}
905
906#[derive(Debug, Clone, Serialize, Deserialize)]
907enum PersistedEdit {
908 Reparent {
909 commit: String,
910 parent: String,
911 },
912 Reuse {
913 commit: String,
914 existing: String,
915 #[serde(default)]
917 parent: Option<String>,
918 },
919 Drop {
920 commit: String,
921 },
922 Rewrite {
923 commit: String,
924 tree: Option<String>,
925 message: Option<String>,
926 author: Option<String>,
927 },
928}
929
930fn oid_hex(oid: &ObjectId) -> String {
931 oid.to_hex()
932}
933
934fn parse_oid(hex: &str) -> Result<ObjectId, RebaseSessionError> {
935 ObjectId::from_hex(hex).map_err(|e| {
936 HistoryError::Planning {
937 message: e.to_string(),
938 }
939 .into()
940 })
941}
942
943fn edits_to_persist(edits: &HistoryEdits) -> Vec<PersistedEdit> {
944 edits
945 .iter()
946 .map(|(commit, edit)| match edit {
947 CommitEdit::Reparent { parent } => PersistedEdit::Reparent {
948 commit: oid_hex(commit),
949 parent: oid_hex(parent),
950 },
951 CommitEdit::Reuse { existing, parent } => PersistedEdit::Reuse {
952 commit: oid_hex(commit),
953 existing: oid_hex(existing),
954 parent: Some(oid_hex(parent)),
955 },
956 CommitEdit::Drop => PersistedEdit::Drop {
957 commit: oid_hex(commit),
958 },
959 CommitEdit::Rewrite {
960 tree,
961 message,
962 author,
963 parents: _,
964 } => PersistedEdit::Rewrite {
965 commit: oid_hex(commit),
966 tree: tree.as_ref().map(oid_hex),
967 message: message.clone(),
968 author: author.clone(),
969 },
970 CommitEdit::InsertBefore(_) | CommitEdit::SplitBefore { .. } => PersistedEdit::Drop {
971 commit: oid_hex(commit),
972 },
973 })
974 .collect()
975}
976
977fn edits_from_persist(rows: &[PersistedEdit]) -> Result<HistoryEdits, RebaseSessionError> {
978 let mut edits = HistoryEdits::new();
979 for row in rows {
980 match row {
981 PersistedEdit::Reparent { commit, parent } => {
982 edits.insert(
983 parse_oid(commit)?,
984 CommitEdit::Reparent {
985 parent: parse_oid(parent)?,
986 },
987 );
988 }
989 PersistedEdit::Reuse {
990 commit,
991 existing,
992 parent,
993 } => {
994 let existing = parse_oid(existing)?;
995 let parent = parent.as_deref().map(parse_oid).transpose()?;
996 edits.insert(
997 parse_oid(commit)?,
998 CommitEdit::Reuse {
999 existing,
1000 parent: parent.unwrap_or(existing),
1001 },
1002 );
1003 }
1004 PersistedEdit::Drop { commit } => {
1005 edits.insert(parse_oid(commit)?, CommitEdit::Drop);
1006 }
1007 PersistedEdit::Rewrite {
1008 commit,
1009 tree,
1010 message,
1011 author,
1012 } => {
1013 edits.insert(
1014 parse_oid(commit)?,
1015 CommitEdit::Rewrite {
1016 tree: tree.as_deref().map(parse_oid).transpose()?,
1017 message: message.clone(),
1018 author: author.clone(),
1019 parents: None,
1020 },
1021 );
1022 }
1023 }
1024 }
1025 Ok(edits)
1026}
1027
1028fn mapping_to_persist(map: &HashMap<ObjectId, ObjectId>) -> HashMap<String, String> {
1029 map.iter().map(|(k, v)| (oid_hex(k), oid_hex(v))).collect()
1030}
1031
1032fn mapping_from_persist(
1033 map: &HashMap<String, String>,
1034) -> Result<HashMap<ObjectId, ObjectId>, RebaseSessionError> {
1035 map.iter()
1036 .map(|(k, v)| Ok((parse_oid(k)?, parse_oid(v)?)))
1037 .collect()
1038}
1039
1040fn replay_order_for_resume(state: &LoadedState, current: ObjectId) -> Vec<ObjectId> {
1042 if let Some(pos) = state.replay_order.iter().position(|c| *c == current) {
1043 return state.replay_order[pos..].to_vec();
1044 }
1045 let mut order = if state.remaining.is_empty() {
1046 state.replay_order.clone()
1047 } else {
1048 state.remaining.clone()
1049 };
1050 if !order.contains(¤t) {
1051 order.insert(0, current);
1052 } else if order.first() != Some(¤t) {
1053 order.retain(|c| *c != current);
1054 order.insert(0, current);
1055 }
1056 order
1057}
1058
1059#[allow(clippy::too_many_arguments)]
1060fn state_from_partial(
1061 partial: &super::plan::PartialRewritePlan,
1062 head_ref: &str,
1063 orig_head: ObjectId,
1064 request: &RebaseRequest,
1065 rebase: &RebaseOntoOptions,
1066 apply: &ApplyOptions,
1067 effective_onto: ObjectId,
1068 fork: ObjectId,
1069) -> PersistedRebaseState {
1070 let conflict = partial.conflict.commit;
1071 let stop_idx = partial
1072 .commits_to_replay
1073 .iter()
1074 .position(|c| *c == conflict)
1075 .unwrap_or(0);
1076 let done: Vec<ObjectId> = partial.commits_to_replay[..stop_idx]
1077 .iter()
1078 .copied()
1079 .filter(|c| partial.mapping.contains_key(c))
1080 .collect();
1081 let remaining: Vec<ObjectId> = partial.commits_to_replay[stop_idx..].to_vec();
1082 PersistedRebaseState {
1083 mapping: mapping_to_persist(&partial.mapping),
1084 rewritten: partial
1085 .rewritten
1086 .iter()
1087 .map(|r| PersistedRewrittenRow {
1088 old: oid_hex(&r.old),
1089 new: r.new.as_ref().map(oid_hex),
1090 reused: r.reused,
1091 })
1092 .collect(),
1093 edits: edits_to_persist(&partial.edits),
1094 rewrite: partial.opts.clone(),
1095 rebase: rebase.clone(),
1096 apply: apply.clone(),
1097 replay_order: partial.commits_to_replay.iter().map(oid_hex).collect(),
1098 remaining: remaining.iter().map(oid_hex).collect(),
1099 done: done.iter().map(oid_hex).collect(),
1100 fork: oid_hex(&fork),
1101 onto: oid_hex(&effective_onto),
1102 upstream: oid_hex(&request.upstream),
1103 head_ref: head_ref.to_owned(),
1104 orig_head: oid_hex(&orig_head),
1105 branch_tip: oid_hex(&request.branch_tip),
1106 flatten_leaf_merges: partial.linearize_onto.is_some(),
1107 scoped_refs: partial
1108 .scoped_refs
1109 .iter()
1110 .map(|(n, o)| (n.clone(), oid_hex(o)))
1111 .collect(),
1112 stopped_head: None,
1113 stopped_commit: Some(oid_hex(&conflict)),
1114 }
1115}
1116
1117struct LoadedState {
1118 mapping: HashMap<ObjectId, ObjectId>,
1119 rewritten: Vec<RewrittenCommit>,
1120 edits: HistoryEdits,
1121 rewrite: RewriteOptions,
1122 rebase: RebaseOntoOptions,
1123 replay_order: Vec<ObjectId>,
1124 remaining: Vec<ObjectId>,
1125 head_ref: String,
1126 orig_head: ObjectId,
1127 onto: ObjectId,
1128 upstream: ObjectId,
1129 branch_tip: ObjectId,
1130 flatten_leaf_merges: bool,
1131 scoped_refs: Vec<(String, ObjectId)>,
1132 stopped_head: Option<ObjectId>,
1133 stopped_commit: Option<ObjectId>,
1134}
1135
1136fn load_persisted_state(git_dir: &Path) -> Result<LoadedState, RebaseSessionError> {
1137 match active_rebase(git_dir) {
1138 None => return Err(RebaseSessionError::NoRebaseInProgress),
1139 Some(ActiveRebase::Foreign(_)) => return Err(RebaseSessionError::ForeignRebaseInProgress),
1140 Some(ActiveRebase::Exec) => return Err(RebaseSessionError::NoRebaseInProgress),
1141 Some(ActiveRebase::Grit) => {}
1142 }
1143 let path = git_dir.join("rebase-merge").join("grit-plan");
1144 let raw = fs::read_to_string(&path).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
1145 let parsed: PersistedRebaseState =
1146 serde_json::from_str(&raw).map_err(|_| RebaseSessionError::NoRebaseInProgress)?;
1147 Ok(LoadedState {
1148 mapping: mapping_from_persist(&parsed.mapping)?,
1149 rewritten: parsed
1150 .rewritten
1151 .iter()
1152 .map(|r| {
1153 Ok(RewrittenCommit {
1154 old: parse_oid(&r.old)?,
1155 new: r.new.as_deref().map(parse_oid).transpose()?,
1156 change_id: None,
1157 subject: None,
1158 reused: r.reused,
1159 })
1160 })
1161 .collect::<Result<_, RebaseSessionError>>()?,
1162 edits: edits_from_persist(&parsed.edits)?,
1163 rewrite: parsed.rewrite,
1164 rebase: parsed.rebase,
1165 replay_order: parsed
1166 .replay_order
1167 .iter()
1168 .map(|h| parse_oid(h))
1169 .collect::<Result<_, _>>()?,
1170 remaining: parsed
1171 .remaining
1172 .iter()
1173 .map(|h| parse_oid(h))
1174 .collect::<Result<_, _>>()?,
1175 head_ref: parsed.head_ref,
1176 orig_head: parse_oid(&parsed.orig_head)?,
1177 onto: parse_oid(&parsed.onto)?,
1178 upstream: parse_oid(&parsed.upstream)?,
1179 branch_tip: parse_oid(&parsed.branch_tip)?,
1180 flatten_leaf_merges: parsed.flatten_leaf_merges,
1181 scoped_refs: parsed
1182 .scoped_refs
1183 .iter()
1184 .map(|(name, hex)| parse_oid(hex).map(|oid| (name.clone(), oid)))
1185 .collect::<Result<_, _>>()?,
1186 stopped_head: parsed.stopped_head.as_deref().map(parse_oid).transpose()?,
1187 stopped_commit: parsed
1188 .stopped_commit
1189 .as_deref()
1190 .map(parse_oid)
1191 .transpose()?,
1192 })
1193}
1194
1195fn write_persisted_state(
1196 git_dir: &Path,
1197 state: &PersistedRebaseState,
1198) -> Result<(), RebaseSessionError> {
1199 let dir = git_dir.join("rebase-merge");
1200 fs::create_dir_all(&dir).map_err(map_lib)?;
1201 let lock_path = dir.join("grit-plan.lock");
1202 let mut lock = LockFile::acquire(&lock_path, LockWait::Immediately)
1203 .map_err(|e| RebaseSessionError::Library(crate::error::Error::Message(e.to_string())))?;
1204 let body = serde_json::to_vec_pretty(state).map_err(map_json)?;
1205 lock.write_all(&body).map_err(map_lib)?;
1206 lock.commit_to(&dir.join("grit-plan"))
1207 .map_err(|e| RebaseSessionError::Library(crate::error::Error::Message(e.to_string())))?;
1208 Ok(())
1209}
1210
1211#[allow(clippy::too_many_arguments)]
1212fn install_stopped_rebase(
1213 repo: &Repository,
1214 head_ref: &str,
1215 orig_head: ObjectId,
1216 request: &RebaseRequest,
1217 rebase_opts: &RebaseOntoOptions,
1218 apply: &ApplyOptions,
1219 partial: super::plan::PartialRewritePlan,
1220 progress: &mut dyn ProgressSink,
1221 cancel: &dyn Cancel,
1222) -> Result<RebaseStop, RebaseSessionError> {
1223 poll_cancel(progress, cancel).map_err(map_lib)?;
1224 let effective_onto = request.onto.unwrap_or(request.upstream);
1225 let fork = super::rebase::rebase_fork(repo, request.upstream, request.branch_tip)?;
1226 let mut persisted = state_from_partial(
1227 &partial,
1228 head_ref,
1229 orig_head,
1230 request,
1231 rebase_opts,
1232 apply,
1233 effective_onto,
1234 fork,
1235 );
1236 let conflict = partial.conflict;
1237 persisted.stopped_head = Some(oid_hex(&conflict.replayed_parent));
1238 let done = persisted
1239 .done
1240 .iter()
1241 .map(|h| parse_oid(h))
1242 .collect::<Result<Vec<_>, _>>()?;
1243 let remaining = persisted
1244 .remaining
1245 .iter()
1246 .map(|h| parse_oid(h))
1247 .collect::<Result<Vec<_>, _>>()?;
1248 let git_progress = git_visible_rebase_progress(
1249 repo,
1250 fork,
1251 request.branch_tip,
1252 &rebase_opts.rewrite,
1253 &done,
1254 &remaining,
1255 conflict.commit,
1256 )?;
1257 write_git_rebase_merge_files(
1258 repo,
1259 head_ref,
1260 orig_head,
1261 effective_onto,
1262 &git_progress.done,
1263 &git_progress.remaining,
1264 git_progress.stop.as_ref(),
1265 )?;
1266 write_persisted_state(repo.git_dir(), &persisted)?;
1267
1268 refs::update_direct_ref_on_store(
1269 repo.refs(),
1270 repo.git_dir(),
1271 "HEAD",
1272 &conflict.replayed_parent,
1273 )
1274 .map_err(map_lib)?;
1275 let mut progress = UncancellableProgress::new(progress);
1278 reset_index_worktree_to_head(repo, conflict.replayed_parent, &mut progress, &NeverCancel)?;
1279 let ours_tree = commit_tree(repo, conflict.replayed_parent)?;
1280 let materialized =
1281 materialized_merge_result_tree(repo.odb(), &conflict.merge).map_err(map_lib)?;
1282 apply_conflict_merge_worktree(
1283 repo,
1284 &ours_tree,
1285 &conflict.merge,
1286 &materialized,
1287 &mut progress,
1288 &NeverCancel,
1289 )
1290 .map_err(map_lib)?;
1291
1292 let message = load_commit(repo, conflict.commit)?.message;
1293 fs::write(repo.git_dir().join("MERGE_MSG"), &message).map_err(map_lib)?;
1294
1295 Ok(RebaseStop {
1296 commit: conflict.commit,
1297 conflicts: conflicts_from_tree_merge(&conflict.merge),
1298 done,
1299 remaining,
1300 })
1301}
1302
1303#[allow(clippy::too_many_arguments)]
1304fn advance_after_plan(
1305 repo: &Repository,
1306 prev: &LoadedState,
1307 apply: &ApplyOptions,
1308 outcome: ResumablePlanOutcome,
1309 head_before: ObjectId,
1310 progress: &mut dyn ProgressSink,
1311 cancel: &dyn Cancel,
1312) -> Result<RebaseOutcome, RebaseSessionError> {
1313 poll_cancel(progress, cancel).map_err(map_lib)?;
1314 match outcome {
1315 ResumablePlanOutcome::Complete(mut plan) => {
1316 if let Some(new_tip) = plan.mapped_tip(prev.orig_head) {
1317 if !plan.ref_updates.iter().any(|u| u.name == prev.head_ref) {
1318 plan.ref_updates.push(PlannedRefUpdate {
1319 name: prev.head_ref.clone(),
1320 old: prev.orig_head,
1321 new: new_tip,
1322 });
1323 }
1324 }
1325 let apply_without_hooks = ApplyOptions {
1326 hooks: PostOperationHooks::Skip,
1327 ..apply.clone()
1328 };
1329 let mut applied = apply_rewrite(repo, &plan, &apply_without_hooks, progress, cancel)?;
1330 if let Some(branch) = plan
1334 .ref_updates
1335 .iter()
1336 .find(|u| u.name == prev.head_ref && u.old != u.new)
1337 {
1338 log_head_move(repo, branch, apply)?;
1339 }
1340 refs::write_symbolic_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", &prev.head_ref)
1341 .map_err(map_lib)?;
1342 let branch_tip = repo.resolve_ref_oid(&prev.head_ref).map_err(map_lib)?;
1343 reset_index_worktree_to_head(repo, branch_tip, &mut NullProgress, &NeverCancel)?;
1345 applied.head = branch_tip;
1346 if let Some(operation) = apply.record_operation.as_deref() {
1347 let head_old_tree = commit_tree(repo, prev.orig_head)?;
1348 let head_new_tree = commit_tree(repo, branch_tip)?;
1349 let record = HistoryLastRecord {
1350 operation: operation.into(),
1351 head_ref: Some(prev.head_ref.clone()),
1352 head_old_commit: Some(prev.orig_head),
1353 head_new_commit: Some(branch_tip),
1354 head_old_tree: Some(head_old_tree),
1355 head_new_tree: Some(head_new_tree),
1356 ref_updates: plan.ref_updates.clone(),
1357 };
1358 write_history_last(repo, &record, None)?;
1359 }
1360 clear_rebase_state(repo.git_dir())?;
1361 if apply.hooks.enabled()
1362 && run_post_rewrite(repo, RewriteKind::Rebase, &plan.rewritten_pairs())
1363 {
1364 applied.hooks_run.push(HookName::PostRewrite);
1365 }
1366 Ok(RebaseOutcome::Done {
1367 head_change: (head_before != applied.head).then_some((head_before, applied.head)),
1368 applied,
1369 plan,
1370 })
1371 }
1372 ResumablePlanOutcome::Partial(partial) => {
1373 let request = RebaseRequest {
1374 upstream: prev.upstream,
1375 onto: Some(prev.onto),
1376 branch_tip: prev.branch_tip,
1377 };
1378 let stop = install_stopped_rebase(
1379 repo,
1380 &prev.head_ref,
1381 prev.orig_head,
1382 &request,
1383 &prev.rebase,
1384 apply,
1385 *partial,
1386 progress,
1387 cancel,
1388 )?;
1389 Ok(RebaseOutcome::Stopped(stop))
1390 }
1391 }
1392}
1393
1394struct GitVisibleRebaseProgress {
1395 done: Vec<ObjectId>,
1396 remaining: Vec<ObjectId>,
1397 stop: Option<ObjectId>,
1398}
1399
1400fn git_visible_rebase_progress(
1406 repo: &Repository,
1407 fork: ObjectId,
1408 branch_tip: ObjectId,
1409 rewrite: &RewriteOptions,
1410 done: &[ObjectId],
1411 remaining: &[ObjectId],
1412 conflict: ObjectId,
1413) -> Result<GitVisibleRebaseProgress, RebaseSessionError> {
1414 if matches!(rewrite.scope, RefScope::Head) {
1415 return Ok(GitVisibleRebaseProgress {
1416 done: done.to_vec(),
1417 remaining: remaining.to_vec(),
1418 stop: Some(conflict),
1419 });
1420 }
1421 let primary = commits_in_rebase_range(repo, fork, &[branch_tip])?;
1422 let primary_set: HashSet<ObjectId> = primary.iter().copied().collect();
1423 let git_done: Vec<ObjectId> = done
1424 .iter()
1425 .copied()
1426 .filter(|oid| primary_set.contains(oid))
1427 .collect();
1428 let git_remaining: Vec<ObjectId> = remaining
1429 .iter()
1430 .copied()
1431 .filter(|oid| primary_set.contains(oid))
1432 .collect();
1433 let git_stop = primary_set.contains(&conflict).then_some(conflict);
1434 Ok(GitVisibleRebaseProgress {
1435 done: git_done,
1436 remaining: git_remaining,
1437 stop: git_stop,
1438 })
1439}
1440
1441fn write_git_rebase_merge_files(
1442 repo: &Repository,
1443 head_ref: &str,
1444 orig_head: ObjectId,
1445 onto: ObjectId,
1446 done: &[ObjectId],
1447 remaining: &[ObjectId],
1448 current: Option<&ObjectId>,
1449) -> Result<(), RebaseSessionError> {
1450 let dir = repo.git_dir().join("rebase-merge");
1451 fs::create_dir_all(&dir).map_err(map_lib)?;
1452 fs::write(dir.join("head-name"), format!("{head_ref}\n")).map_err(map_lib)?;
1453 fs::write(dir.join("onto"), format!("{}\n", onto.to_hex())).map_err(map_lib)?;
1454 fs::write(dir.join("orig-head"), format!("{}\n", orig_head.to_hex())).map_err(map_lib)?;
1455 let todo_remaining: Vec<ObjectId> = match current {
1456 Some(current) if remaining.first() == Some(current) => {
1457 remaining.iter().skip(1).copied().collect()
1458 }
1459 _ => remaining.to_vec(),
1460 };
1461 fs::write(
1462 dir.join("git-rebase-todo"),
1463 todo_lines(repo, &todo_remaining)?,
1464 )
1465 .map_err(map_lib)?;
1466 let done_with_current: Vec<ObjectId> = match current {
1469 Some(current) => done.iter().chain([current]).copied().collect(),
1470 None => done.to_vec(),
1471 };
1472 fs::write(dir.join("done"), todo_lines(repo, &done_with_current)?).map_err(map_lib)?;
1473 let msgnum = done_with_current.len();
1474 let end = done_with_current.len() + todo_remaining.len();
1475 fs::write(dir.join("msgnum"), format!("{msgnum}\n")).map_err(map_lib)?;
1476 fs::write(dir.join("end"), format!("{end}\n")).map_err(map_lib)?;
1477 if let Some(current) = current {
1478 fs::write(dir.join("stopped-sha"), format!("{}\n", current.to_hex())).map_err(map_lib)?;
1479 let commit = load_commit(repo, *current)?;
1480 match author_script(&commit.author) {
1481 Some(script) => fs::write(dir.join("author-script"), script).map_err(map_lib)?,
1482 None => remove_if_exists(&dir.join("author-script"))?,
1483 }
1484 fs::write(dir.join("message"), commit.message).map_err(map_lib)?;
1485 } else {
1486 remove_if_exists(&dir.join("stopped-sha"))?;
1487 remove_if_exists(&dir.join("author-script"))?;
1488 remove_if_exists(&dir.join("message"))?;
1489 }
1490 Ok(())
1491}
1492
1493fn todo_lines(repo: &Repository, commits: &[ObjectId]) -> Result<String, RebaseSessionError> {
1494 let mut out = String::new();
1495 for &oid in commits {
1496 let subject = super::subject::commit_message_subject_line(&load_commit(repo, oid)?.message);
1497 let abbrev = &oid.to_hex()[..7];
1498 out.push_str(&format!("pick {abbrev} {subject}\n"));
1499 }
1500 Ok(out)
1501}
1502
1503fn author_script(author: &str) -> Option<String> {
1507 let lt = author.find('<')?;
1508 let gt = lt + author[lt..].find('>')?;
1509 let name = author[..lt].trim_end();
1510 let email = &author[lt + 1..gt];
1511 let date = format!("@{}", author[gt + 1..].trim());
1512 let quote = crate::transport::sq_quote_shell_arg;
1513 Some(format!(
1514 "GIT_AUTHOR_NAME={}\nGIT_AUTHOR_EMAIL={}\nGIT_AUTHOR_DATE={}\n",
1515 quote(name),
1516 quote(email),
1517 quote(&date)
1518 ))
1519}
1520
1521pub(super) fn clear_rebase_state(git_dir: &Path) -> Result<(), RebaseSessionError> {
1522 remove_if_exists(&git_dir.join("MERGE_MSG"))?;
1523 let dir = git_dir.join("rebase-merge");
1524 if dir.is_dir() {
1525 fs::remove_dir_all(&dir).map_err(map_lib)?;
1526 }
1527 Ok(())
1528}
1529
1530fn branch_head(repo: &Repository) -> Result<(String, ObjectId), RebaseSessionError> {
1531 match resolve_head_for_repository(repo).map_err(map_lib)? {
1532 HeadState::Branch {
1533 refname,
1534 oid: Some(oid),
1535 ..
1536 } => Ok((refname, oid)),
1537 HeadState::Branch { oid: None, .. } | HeadState::Detached { .. } | HeadState::Invalid => {
1538 Err(RebaseSessionError::NotOnBranch)
1539 }
1540 }
1541}
1542
1543fn resolve_head_oid(repo: &Repository) -> Result<ObjectId, RebaseSessionError> {
1544 resolve_head_for_repository(repo)
1545 .map_err(map_lib)?
1546 .oid()
1547 .copied()
1548 .ok_or(RebaseSessionError::NotOnBranch)
1549}
1550
1551fn commit_tree(repo: &Repository, commit: ObjectId) -> Result<ObjectId, RebaseSessionError> {
1552 repo.odb().commit_tree(&commit).map_err(map_lib)
1553}
1554
1555fn load_commit(
1556 repo: &Repository,
1557 oid: ObjectId,
1558) -> Result<crate::objects::CommitData, RebaseSessionError> {
1559 repo.odb().read_commit(&oid).map_err(map_lib)
1560}
1561
1562fn reset_index_worktree_to_head(
1563 repo: &Repository,
1564 head_oid: ObjectId,
1565 progress: &mut dyn ProgressSink,
1566 cancel: &dyn Cancel,
1567) -> Result<(), RebaseSessionError> {
1568 let head_tree = commit_tree(repo, head_oid)?;
1569 checkout_hard_to_tree_from_head_tree(repo, &head_tree, &head_tree, progress, cancel)
1572 .map_err(map_lib)?;
1573 Ok(())
1574}
1575
1576fn hard_reset_branch(
1579 repo: &Repository,
1580 refname: &str,
1581 target: ObjectId,
1582 identity: &crate::ident::Signature,
1583) -> Result<RebaseAbort, RebaseSessionError> {
1584 let before = repo
1585 .references()
1586 .resolve_if_exists(refname)
1587 .map_err(map_lib)?;
1588 refs::write_symbolic_ref_on_store(repo.refs(), repo.git_dir(), "HEAD", refname)
1589 .map_err(map_lib)?;
1590 hard_reset_head(repo, target, identity)?;
1591 let overwritten = (before != Some(target)).then(|| PlannedRefUpdate {
1592 name: refname.to_owned(),
1593 old: before.unwrap_or_else(crate::diff::zero_oid),
1594 new: target,
1595 });
1596 Ok(RebaseAbort {
1597 overwritten: overwritten.into_iter().collect(),
1598 })
1599}
1600
1601fn hard_reset_head(
1602 repo: &Repository,
1603 target: ObjectId,
1604 identity: &crate::ident::Signature,
1605) -> Result<(), RebaseSessionError> {
1606 reset_to_commit(
1607 repo,
1608 &ResetOptions::new(target, ResetMode::Hard, identity.clone(), "rebase (abort)"),
1609 &mut crate::progress::NullProgress,
1610 &crate::progress::NeverCancel,
1611 )
1612 .map_err(map_lib)?;
1613 Ok(())
1614}
1615
1616use std::io::Write;
1617
1618fn map_lib<E: Into<crate::error::Error>>(err: E) -> RebaseSessionError {
1619 RebaseSessionError::Library(err.into())
1620}
1621
1622fn replan_rewrite_options(state: &LoadedState) -> RewriteOptions {
1623 state.rewrite.clone()
1624}
1625
1626fn map_json(err: serde_json::Error) -> RebaseSessionError {
1627 RebaseSessionError::Library(crate::error::Error::Message(err.to_string()))
1628}
1629
1630#[cfg(test)]
1631mod tests {
1632 use super::{author_script, edits_from_persist, edits_to_persist, PersistedEdit};
1633 use crate::history::types::{CommitEdit, HistoryEdits};
1634 use crate::objects::ObjectId;
1635
1636 fn oid(byte: u8) -> ObjectId {
1637 ObjectId::from_bytes(&[byte; 20]).unwrap_or_else(|_| ObjectId::zero())
1638 }
1639
1640 #[test]
1641 fn reuse_edit_round_trips_its_continuation_parent() {
1642 let mut edits = HistoryEdits::new();
1643 edits.insert(
1644 oid(1),
1645 CommitEdit::Reuse {
1646 existing: oid(2),
1647 parent: oid(3),
1648 },
1649 );
1650 let rows = edits_to_persist(&edits);
1651 let json = serde_json::to_string(&rows).unwrap_or_default();
1652 let parsed: Vec<PersistedEdit> = serde_json::from_str(&json).unwrap_or_default();
1653 assert_eq!(edits_from_persist(&parsed).ok(), Some(edits));
1654 }
1655
1656 #[test]
1657 fn reuse_edit_saved_without_parent_continues_from_the_reused_commit() {
1658 let json = format!(
1659 r#"[{{"Reuse":{{"commit":"{}","existing":"{}"}}}}]"#,
1660 oid(1).to_hex(),
1661 oid(2).to_hex()
1662 );
1663 let parsed: Vec<PersistedEdit> = serde_json::from_str(&json).unwrap_or_default();
1664 let edits = edits_from_persist(&parsed).ok();
1665 assert_eq!(
1666 edits.and_then(|edits| edits.get(&oid(1)).cloned()),
1667 Some(CommitEdit::Reuse {
1668 existing: oid(2),
1669 parent: oid(2),
1670 })
1671 );
1672 }
1673
1674 #[test]
1675 fn author_script_quotes_values_and_prefixes_date() {
1676 assert_eq!(
1677 author_script("A U Thor <a@example.com> 1700000000 -0130").as_deref(),
1678 Some(
1679 "GIT_AUTHOR_NAME='A U Thor'\nGIT_AUTHOR_EMAIL='a@example.com'\nGIT_AUTHOR_DATE='@1700000000 -0130'\n"
1680 )
1681 );
1682 assert_eq!(
1683 author_script("O'Brien <o@example.com> 1 +0000").as_deref(),
1684 Some(
1685 "GIT_AUTHOR_NAME='O'\\''Brien'\nGIT_AUTHOR_EMAIL='o@example.com'\nGIT_AUTHOR_DATE='@1 +0000'\n"
1686 )
1687 );
1688 assert_eq!(author_script("no email here 1 +0000"), None);
1689 }
1690}