1use std::collections::{HashMap, HashSet};
2
3use std::fmt::Write;
4use std::path::PathBuf;
5use std::time::SystemTime;
6
7use bstr::BString;
8use eyre::Context;
9use tracing::warn;
10
11use crate::core::check_out::{CheckOutCommitOptions, CheckoutTarget, check_out_commit};
12use crate::core::effects::Effects;
13use crate::core::eventlog::{EventLogDb, EventTransactionId};
14use crate::core::formatting::Pluralize;
15use crate::core::repo_ext::RepoExt;
16use crate::git::{
17 BranchType, CategorizedReferenceName, GitRunInfo, MaybeZeroOid, NonZeroOid, ReferenceName,
18 Repo, ResolvedReferenceInfo,
19};
20use crate::util::{ExitCode, EyreExitOr};
21
22use super::plan::RebasePlan;
23
24pub fn move_branches<'a>(
28 effects: &Effects,
29 git_run_info: &GitRunInfo,
30 repo: &'a Repo,
31 event_tx_id: EventTransactionId,
32 rewritten_oids_map: &'a HashMap<NonZeroOid, MaybeZeroOid>,
33) -> eyre::Result<()> {
34 let main_branch = repo.get_main_branch()?;
35 let main_branch_name = main_branch.get_reference_name()?;
36 let branch_oid_to_names = repo.get_branch_oid_to_names()?;
37
38 let mut branch_moves: Vec<(NonZeroOid, MaybeZeroOid, &ReferenceName)> = Vec::new();
44 let mut branch_move_err: Option<eyre::Error> = None;
45 'outer: for (old_oid, names) in branch_oid_to_names.iter() {
46 let new_oid = match rewritten_oids_map.get(old_oid) {
47 Some(new_oid) => new_oid,
48 None => continue,
49 };
50 let mut names: Vec<_> = names.iter().collect();
51 names.sort_unstable();
53 match new_oid {
54 MaybeZeroOid::NonZero(new_oid) => {
55 let new_commit = match repo.find_commit_or_fail(*new_oid).wrap_err_with(|| {
56 format!(
57 "Could not find newly-rewritten commit with old OID: {old_oid:?}, new OID: {new_oid:?}",
58 )
59 }) {
60 Ok(commit) => commit,
61 Err(err) => {
62 branch_move_err = Some(err);
63 break 'outer;
64 }
65 };
66
67 for reference_name in names {
68 if let Err(err) = repo.create_reference(
69 reference_name,
70 new_commit.get_oid(),
71 true,
72 "move branches",
73 ) {
74 branch_move_err = Some(eyre::eyre!(err));
75 break 'outer;
76 }
77 branch_moves.push((*old_oid, MaybeZeroOid::NonZero(*new_oid), reference_name));
78 }
79 }
80
81 MaybeZeroOid::Zero => {
82 for reference_name in names {
83 if reference_name == &main_branch_name {
84 let target_oid = match main_branch.get_upstream_branch_target()? {
90 Some(target_oid) => {
91 if let Err(err) = repo.create_reference(
92 &main_branch_name,
93 target_oid,
94 true,
95 "move main branch",
96 ) {
97 branch_move_err = Some(eyre::eyre!(err));
98 break 'outer;
99 }
100 MaybeZeroOid::NonZero(target_oid)
101 }
102 None => {
103 let mut main_branch_reference =
104 repo.get_main_branch()?.into_reference();
105 if let Err(err) = main_branch_reference.delete() {
106 branch_move_err = Some(eyre::eyre!(err));
107 break 'outer;
108 }
109 MaybeZeroOid::Zero
110 }
111 };
112 branch_moves.push((*old_oid, target_oid, reference_name));
113 } else {
114 let branch_name = CategorizedReferenceName::new(reference_name);
115 match branch_name {
116 CategorizedReferenceName::RemoteBranch { .. }
117 | CategorizedReferenceName::OtherRef { .. } => {
118 warn!(?reference_name, "Not deleting non-local-branch reference");
119 }
120 CategorizedReferenceName::LocalBranch { .. } => {
121 let branch_name = branch_name.render_suffix();
122 match repo.find_branch(&branch_name, BranchType::Local) {
123 Ok(Some(mut branch)) => {
124 if let Err(err) = branch.delete() {
125 branch_move_err = Some(eyre::eyre!(err));
126 break 'outer;
127 }
128 }
129 Ok(None) => {
130 warn!(?branch_name, "Branch not found, not deleting")
131 }
132 Err(err) => {
133 branch_move_err = Some(eyre::eyre!(err));
134 break 'outer;
135 }
136 };
137 branch_moves.push((*old_oid, MaybeZeroOid::Zero, reference_name));
138 }
139 }
140 }
141 }
142 }
143 }
144 }
145
146 #[allow(clippy::format_collect)]
147 let branch_moves_stdin: String = branch_moves
148 .into_iter()
149 .map(|(old_oid, new_oid, name)| {
150 format!("{old_oid} {new_oid} {name}\n", name = name.as_str())
151 })
152 .collect();
153 let branch_moves_stdin = BString::from(branch_moves_stdin);
154 git_run_info.run_hook(
155 effects,
156 repo,
157 "reference-transaction",
158 event_tx_id,
159 &["committed"],
160 Some(branch_moves_stdin),
161 )?;
162 match branch_move_err {
163 Some(err) => Err(err),
164 None => Ok(()),
165 }
166}
167
168pub fn check_out_updated_head(
177 effects: &Effects,
178 git_run_info: &GitRunInfo,
179 repo: &Repo,
180 event_log_db: &EventLogDb,
181 event_tx_id: EventTransactionId,
182 rewritten_oids: &HashMap<NonZeroOid, MaybeZeroOid>,
183 previous_head_info: &ResolvedReferenceInfo,
184 skipped_head_updated_oid: Option<NonZeroOid>,
185 check_out_commit_options: &CheckOutCommitOptions,
186) -> EyreExitOr<()> {
187 let checkout_target: ResolvedReferenceInfo = match previous_head_info {
188 ResolvedReferenceInfo {
189 oid: None,
190 reference_name: None,
191 } => {
192 ResolvedReferenceInfo {
194 oid: skipped_head_updated_oid,
195 reference_name: None,
196 }
197 }
198
199 ResolvedReferenceInfo {
200 oid: None,
201 reference_name: Some(reference_name),
202 } => {
203 ResolvedReferenceInfo {
206 oid: None,
207 reference_name: Some(reference_name.clone()),
208 }
209 }
210
211 ResolvedReferenceInfo {
212 oid: Some(previous_head_oid),
213 reference_name: None,
214 } => {
215 match rewritten_oids.get(previous_head_oid) {
217 Some(MaybeZeroOid::NonZero(oid)) => {
218 ResolvedReferenceInfo {
220 oid: Some(*oid),
221 reference_name: None,
222 }
223 }
224 Some(MaybeZeroOid::Zero) => {
225 ResolvedReferenceInfo {
227 oid: skipped_head_updated_oid,
228 reference_name: None,
229 }
230 }
231 None => {
232 ResolvedReferenceInfo {
234 oid: Some(*previous_head_oid),
235 reference_name: None,
236 }
237 }
238 }
239 }
240
241 ResolvedReferenceInfo {
242 oid: Some(_),
243 reference_name: Some(reference_name),
244 } => {
245 match repo.find_reference(reference_name)? {
247 Some(reference) => {
248 let oid = repo.resolve_reference(&reference)?.oid;
258 ResolvedReferenceInfo {
259 oid,
260 reference_name: Some(reference_name.clone()),
261 }
262 }
263
264 None => {
265 ResolvedReferenceInfo {
268 oid: skipped_head_updated_oid,
269 reference_name: None,
270 }
271 }
272 }
273 }
274 };
275
276 let head_info = repo.get_head_info()?;
277 if head_info == checkout_target {
278 return Ok(Ok(()));
279 }
280
281 let checkout_target: CheckoutTarget = match &checkout_target {
282 ResolvedReferenceInfo {
283 oid: None,
284 reference_name: None,
285 } => return Ok(Ok(())),
286
287 ResolvedReferenceInfo {
288 oid: Some(oid),
289 reference_name: None,
290 } => CheckoutTarget::Oid(*oid),
291
292 ResolvedReferenceInfo {
293 oid: _,
294 reference_name: Some(reference_name),
295 } => {
296 let checkout_target = match checkout_target.get_branch_name()? {
299 Some(branch_name) => branch_name,
300 None => reference_name.as_str(),
301 };
302 CheckoutTarget::Reference(ReferenceName::from(checkout_target))
303 }
304 };
305
306 let result = check_out_commit(
307 effects,
308 git_run_info,
309 repo,
310 event_log_db,
311 event_tx_id,
312 Some(checkout_target),
313 check_out_commit_options,
314 )?;
315 Ok(result)
316}
317
318#[derive(Copy, Clone, Debug)]
320pub enum MergeConflictRemediation {
321 Retry,
324
325 Restack,
327
328 Insert,
330}
331
332#[derive(Debug)]
334pub enum FailedMergeInfo {
335 Conflict {
337 commit_oid: NonZeroOid,
339
340 conflicting_paths: HashSet<PathBuf>,
342 },
343
344 CannotRebaseMergeInMemory {
346 commit_oid: NonZeroOid,
348 },
349}
350
351impl FailedMergeInfo {
352 pub fn describe(
355 &self,
356 effects: &Effects,
357 repo: &Repo,
358 remediation: MergeConflictRemediation,
359 ) -> eyre::Result<()> {
360 match self {
361 FailedMergeInfo::Conflict {
362 commit_oid,
363 conflicting_paths,
364 } => {
365 writeln!(
366 effects.get_output_stream(),
367 "This operation would cause a merge conflict:"
368 )?;
369 writeln!(
370 effects.get_output_stream(),
371 "{} ({}) {}",
372 effects.get_glyphs().bullet_point,
373 Pluralize {
374 determiner: None,
375 amount: conflicting_paths.len(),
376 unit: ("conflicting file", "conflicting files"),
377 },
378 effects.get_glyphs().render(
379 repo.friendly_describe_commit_from_oid(effects.get_glyphs(), *commit_oid)?
380 )?
381 )?;
382 }
383
384 FailedMergeInfo::CannotRebaseMergeInMemory { commit_oid } => {
385 writeln!(
386 effects.get_output_stream(),
387 "Merge commits currently can't be rebased in-memory."
388 )?;
389 writeln!(
390 effects.get_output_stream(),
391 "The merge commit was: {}",
392 effects.get_glyphs().render(
393 repo.friendly_describe_commit_from_oid(effects.get_glyphs(), *commit_oid)?
394 )?,
395 )?;
396 }
397 }
398
399 match remediation {
400 MergeConflictRemediation::Retry => {
401 writeln!(
402 effects.get_output_stream(),
403 "To resolve merge conflicts, retry this operation with the --merge option."
404 )?;
405 }
406 MergeConflictRemediation::Restack => {
407 writeln!(
408 effects.get_output_stream(),
409 "To resolve merge conflicts, run: git restack --merge"
410 )?;
411 }
412 MergeConflictRemediation::Insert => {
413 writeln!(
414 effects.get_output_stream(),
415 "To resolve merge conflicts, run: git move -m -s 'siblings(.)'"
416 )?;
417 }
418 }
419
420 Ok(())
421 }
422}
423
424mod in_memory {
425 use std::collections::HashMap;
426 use std::fmt::Write;
427
428 use bstr::{BString, ByteSlice};
429 use eyre::Context;
430 use tracing::{instrument, warn};
431
432 use crate::core::effects::{Effects, OperationIcon, OperationType};
433 use crate::core::eventlog::EventLogDb;
434 use crate::core::gc::mark_commit_reachable;
435 use crate::core::rewrite::execute::check_out_updated_head;
436 use crate::core::rewrite::move_branches;
437 use crate::core::rewrite::plan::{OidOrLabel, RebaseCommand, RebasePlan};
438 use crate::git::{
439 AmendFastOptions, CherryPickFastOptions, CreateCommitFastError, GitRunInfo, MaybeZeroOid,
440 NonZeroOid, Repo,
441 };
442 use crate::util::EyreExitOr;
443
444 use super::{ExecuteRebasePlanOptions, FailedMergeInfo};
445
446 pub enum RebaseInMemoryResult {
447 Succeeded {
448 rewritten_oids: HashMap<NonZeroOid, MaybeZeroOid>,
449
450 new_head_oid: Option<NonZeroOid>,
456 },
457 MergeFailed(FailedMergeInfo),
458 }
459
460 #[instrument]
461 pub fn rebase_in_memory(
462 effects: &Effects,
463 repo: &Repo,
464 rebase_plan: &RebasePlan,
465 options: &ExecuteRebasePlanOptions,
466 ) -> eyre::Result<RebaseInMemoryResult> {
467 if let Some(merge_commit_oid) =
468 rebase_plan
469 .commands
470 .iter()
471 .find_map(|command| match command {
472 RebaseCommand::Merge {
473 commit_oid,
474 commits_to_merge: _,
475 } => Some(commit_oid),
476 RebaseCommand::CreateLabel { .. }
477 | RebaseCommand::Reset { .. }
478 | RebaseCommand::Pick { .. }
479 | RebaseCommand::Replace { .. }
480 | RebaseCommand::Break
481 | RebaseCommand::RegisterExtraPostRewriteHook
482 | RebaseCommand::DetectEmptyCommit { .. }
483 | RebaseCommand::SkipUpstreamAppliedCommit { .. } => None,
484 })
485 {
486 return Ok(RebaseInMemoryResult::MergeFailed(
487 FailedMergeInfo::CannotRebaseMergeInMemory {
488 commit_oid: *merge_commit_oid,
489 },
490 ));
491 }
492
493 let ExecuteRebasePlanOptions {
494 now,
495 event_tx_id: _,
498 preserve_timestamps,
499 force_in_memory: _,
500 force_on_disk: _,
501 dry_run: _,
502 resolve_merge_conflicts: _, check_out_commit_options: _, } = options;
505
506 let mut current_oid = rebase_plan.first_dest_oid;
507 let mut labels: HashMap<String, NonZeroOid> = HashMap::new();
508 let mut rewritten_oids: HashMap<NonZeroOid, MaybeZeroOid> = HashMap::new();
509
510 let head_oid = repo.get_head_info()?.oid;
516 let mut skipped_head_new_oid = None;
517 let mut maybe_set_skipped_head_new_oid = |skipped_head_oid, current_oid| {
518 if Some(skipped_head_oid) == head_oid {
519 skipped_head_new_oid.get_or_insert(current_oid);
520 }
521 };
522
523 let mut i = 0;
524 let num_picks = rebase_plan
525 .commands
526 .iter()
527 .filter(|command| match command {
528 RebaseCommand::CreateLabel { .. }
529 | RebaseCommand::Reset { .. }
530 | RebaseCommand::Break
531 | RebaseCommand::RegisterExtraPostRewriteHook
532 | RebaseCommand::DetectEmptyCommit { .. } => false,
533 RebaseCommand::Pick { .. }
534 | RebaseCommand::Merge { .. }
535 | RebaseCommand::Replace { .. }
536 | RebaseCommand::SkipUpstreamAppliedCommit { .. } => true,
537 })
538 .count();
539 let (effects, progress) = effects.start_operation(OperationType::RebaseCommits);
540
541 for command in rebase_plan.commands.iter() {
542 match command {
543 RebaseCommand::CreateLabel { label_name } => {
544 labels.insert(label_name.clone(), current_oid);
545 }
546
547 RebaseCommand::Reset {
548 target: OidOrLabel::Label(label_name),
549 } => {
550 current_oid = match labels.get(label_name) {
551 Some(oid) => *oid,
552 None => eyre::bail!("BUG: no associated OID for label: {label_name}"),
553 };
554 }
555
556 RebaseCommand::Reset {
557 target: OidOrLabel::Oid(commit_oid),
558 } => {
559 current_oid = match rewritten_oids.get(commit_oid) {
560 Some(MaybeZeroOid::NonZero(rewritten_oid)) => {
561 *rewritten_oid
563 }
564 Some(MaybeZeroOid::Zero) | None => {
565 *commit_oid
569 }
570 };
571 }
572
573 RebaseCommand::Pick {
574 original_commit_oid,
575 commits_to_apply_oids,
576 } => {
577 let current_commit = repo
578 .find_commit_or_fail(current_oid)
579 .wrap_err("Finding current commit")?;
580
581 let original_commit = repo
582 .find_commit_or_fail(*original_commit_oid)
583 .wrap_err("Finding commit to apply")?;
584 i += 1;
585
586 let commit_num = format!("[{i}/{num_picks}]");
587 progress.notify_progress(i, num_picks);
588
589 let commit_message = original_commit.get_message_raw();
590 let commit_message = commit_message.to_str().with_context(|| {
591 eyre::eyre!(
592 "Could not decode commit message for commit: {:?}",
593 original_commit_oid
594 )
595 })?;
596
597 let commit_author = original_commit.get_author();
598 let committer_signature = if *preserve_timestamps {
599 original_commit.get_committer()
600 } else {
601 original_commit.get_committer().update_timestamp(*now)?
602 };
603 let mut rebased_commit_oid = None;
604 let mut rebased_commit = None;
605
606 for commit_oid in commits_to_apply_oids.iter() {
607 let commit_to_apply = repo
608 .find_commit_or_fail(*commit_oid)
609 .wrap_err("Finding commit to apply")?;
610 let commit_description = effects
611 .get_glyphs()
612 .render(commit_to_apply.friendly_describe(effects.get_glyphs())?)?;
613
614 if commit_to_apply.get_parent_count() > 1 {
615 warn!(
616 ?commit_oid,
617 "BUG: Merge commit should have been detected during planning phase"
618 );
619 return Ok(RebaseInMemoryResult::MergeFailed(
620 FailedMergeInfo::CannotRebaseMergeInMemory {
621 commit_oid: *commit_oid,
622 },
623 ));
624 };
625
626 progress.notify_status(
627 OperationIcon::InProgress,
628 format!("Applying patch for commit: {commit_description}"),
629 );
630
631 let maybe_tree = if rebased_commit.is_none() {
637 repo.cherry_pick_fast(
638 &commit_to_apply,
639 ¤t_commit,
640 &CherryPickFastOptions {
641 reuse_parent_tree_if_possible: true,
642 },
643 )
644 } else {
645 repo.amend_fast(
646 &rebased_commit.expect("rebased commit should not be None"),
647 &AmendFastOptions::FromCommit {
648 commit: commit_to_apply,
649 },
650 )
651 };
652 let commit_tree = match maybe_tree {
653 Ok(tree) => tree,
654 Err(CreateCommitFastError::MergeConflict { conflicting_paths }) => {
655 return Ok(RebaseInMemoryResult::MergeFailed(
656 FailedMergeInfo::Conflict {
657 commit_oid: *commit_oid,
658 conflicting_paths,
659 },
660 ));
661 }
662 Err(other) => eyre::bail!(other),
663 };
664
665 progress.notify_status(
669 OperationIcon::InProgress,
670 format!("Committing to repository: {commit_description}"),
671 );
672 rebased_commit_oid = Some(
673 repo.create_commit(
674 None,
675 &commit_author,
676 &committer_signature,
677 commit_message,
678 &commit_tree,
679 vec![¤t_commit],
680 )
681 .wrap_err("Applying rebased commit")?,
682 );
683
684 rebased_commit = repo.find_commit(rebased_commit_oid.unwrap())?;
685 }
686
687 let rebased_commit_oid =
688 rebased_commit_oid.expect("rebased oid should not be None");
689 let commit_description =
690 effects
691 .get_glyphs()
692 .render(repo.friendly_describe_commit_from_oid(
693 effects.get_glyphs(),
694 rebased_commit_oid,
695 )?)?;
696
697 if rebased_commit
698 .expect("rebased commit should not be None")
699 .is_empty()
700 {
701 rewritten_oids.insert(*original_commit_oid, MaybeZeroOid::Zero);
702 maybe_set_skipped_head_new_oid(*original_commit_oid, current_oid);
703
704 writeln!(
705 effects.get_output_stream(),
706 "{commit_num} Skipped now-empty commit: {commit_description}",
707 )?;
708 } else {
709 rewritten_oids.insert(
710 *original_commit_oid,
711 MaybeZeroOid::NonZero(rebased_commit_oid),
712 );
713 for commit_oid in commits_to_apply_oids {
714 rewritten_oids
715 .insert(*commit_oid, MaybeZeroOid::NonZero(rebased_commit_oid));
716 }
717
718 current_oid = rebased_commit_oid;
719
720 writeln!(
721 effects.get_output_stream(),
722 "{commit_num} Committed as: {commit_description}"
723 )?;
724 }
725 }
726
727 RebaseCommand::Merge {
728 commit_oid,
729 commits_to_merge: _,
730 } => {
731 warn!(
732 ?commit_oid,
733 "BUG: Merge commit without replacement should have been detected when starting in-memory rebase"
734 );
735 return Ok(RebaseInMemoryResult::MergeFailed(
736 FailedMergeInfo::CannotRebaseMergeInMemory {
737 commit_oid: *commit_oid,
738 },
739 ));
740 }
741
742 RebaseCommand::Replace {
743 commit_oid,
744 replacement_commit_oid,
745 parents,
746 } => {
747 let original_commit = repo
748 .find_commit_or_fail(*commit_oid)
749 .wrap_err("Finding current commit")?;
750 let original_commit_description = effects
751 .get_glyphs()
752 .render(original_commit.friendly_describe(effects.get_glyphs())?)?;
753
754 i += 1;
755 let commit_num = format!("[{i}/{num_picks}]");
756 progress.notify_progress(i, num_picks);
757 progress.notify_status(
758 OperationIcon::InProgress,
759 format!("Replacing commit: {original_commit_description}"),
760 );
761
762 let replacement_commit = repo.find_commit_or_fail(*replacement_commit_oid)?;
763 let replacement_tree = replacement_commit.get_tree()?;
764 let replacement_message = replacement_commit.get_message_raw();
765 let replacement_commit_message =
766 replacement_message.to_str().with_context(|| {
767 eyre::eyre!(
768 "Could not decode commit message for replacement commit: {:?}",
769 replacement_commit
770 )
771 })?;
772
773 let replacement_commit_description = effects
774 .get_glyphs()
775 .render(replacement_commit.friendly_describe(effects.get_glyphs())?)?;
776 progress.notify_status(
777 OperationIcon::InProgress,
778 format!("Committing to repository: {replacement_commit_description}"),
779 );
780 let committer_signature = if *preserve_timestamps {
781 replacement_commit.get_committer()
782 } else {
783 replacement_commit.get_committer().update_timestamp(*now)?
784 };
785 let parents = {
786 let mut result = Vec::new();
787 for parent in parents {
788 let parent_oid = match parent {
789 OidOrLabel::Oid(oid) => *oid,
790 OidOrLabel::Label(label) => {
791 let oid = labels.get(label).ok_or_else(|| {
792 eyre::eyre!(
793 "Label {label} could not be resolved to a commit"
794 )
795 })?;
796 *oid
797 }
798 };
799 let parent_commit = repo.find_commit_or_fail(parent_oid)?;
800 result.push(parent_commit);
801 }
802 result
803 };
804 let rebased_commit_oid = repo
805 .create_commit(
806 None,
807 &replacement_commit.get_author(),
808 &committer_signature,
809 replacement_commit_message,
810 &replacement_tree,
811 parents.iter().collect(),
812 )
813 .wrap_err("Applying rebased commit")?;
814
815 let commit_description =
816 effects
817 .get_glyphs()
818 .render(repo.friendly_describe_commit_from_oid(
819 effects.get_glyphs(),
820 rebased_commit_oid,
821 )?)?;
822 rewritten_oids.insert(*commit_oid, MaybeZeroOid::NonZero(rebased_commit_oid));
823 current_oid = rebased_commit_oid;
824
825 writeln!(
826 effects.get_output_stream(),
827 "{commit_num} Committed as: {commit_description}"
828 )?;
829 }
830
831 RebaseCommand::Break => {
832 eyre::bail!("`break` not supported for in-memory rebases");
833 }
834
835 RebaseCommand::SkipUpstreamAppliedCommit { commit_oid } => {
836 i += 1;
837 let commit_num = format!("[{i}/{num_picks}]");
838
839 let commit = repo.find_commit_or_fail(*commit_oid)?;
840 rewritten_oids.insert(*commit_oid, MaybeZeroOid::Zero);
841 maybe_set_skipped_head_new_oid(*commit_oid, current_oid);
842
843 let commit_description = commit.friendly_describe(effects.get_glyphs())?;
844 let commit_description = effects.get_glyphs().render(commit_description)?;
845 writeln!(
846 effects.get_output_stream(),
847 "{commit_num} Skipped commit (was already applied upstream): {commit_description}"
848 )?;
849 }
850
851 RebaseCommand::RegisterExtraPostRewriteHook
852 | RebaseCommand::DetectEmptyCommit { .. } => {
853 }
856 }
857 }
858
859 let new_head_oid: Option<NonZeroOid> = match head_oid {
860 None => {
861 None
863 }
864 Some(head_oid) => {
865 match rewritten_oids.get(&head_oid) {
866 Some(MaybeZeroOid::NonZero(new_head_oid)) => {
867 Some(*new_head_oid)
869 }
870 Some(MaybeZeroOid::Zero) => {
871 let new_head_oid = match skipped_head_new_oid {
874 Some(new_head_oid) => new_head_oid,
875 None => {
876 warn!(
877 ?head_oid,
878 "`HEAD` OID was rewritten to 0, but no skipped `HEAD` OID was set",
879 );
880 head_oid
881 }
882 };
883 Some(new_head_oid)
884 }
885 None => {
886 Some(head_oid)
888 }
889 }
890 }
891 };
892 Ok(RebaseInMemoryResult::Succeeded {
893 rewritten_oids,
894 new_head_oid,
895 })
896 }
897
898 pub fn post_rebase_in_memory(
899 effects: &Effects,
900 git_run_info: &GitRunInfo,
901 repo: &Repo,
902 event_log_db: &EventLogDb,
903 rewritten_oids: &HashMap<NonZeroOid, MaybeZeroOid>,
904 skipped_head_updated_oid: Option<NonZeroOid>,
905 options: &ExecuteRebasePlanOptions,
906 ) -> EyreExitOr<()> {
907 let ExecuteRebasePlanOptions {
908 now: _,
909 event_tx_id,
910 preserve_timestamps: _,
911 force_in_memory: _,
912 force_on_disk: _,
913 dry_run: _,
914 resolve_merge_conflicts: _,
915 check_out_commit_options,
916 } = options;
917
918 for new_oid in rewritten_oids.values() {
919 if let MaybeZeroOid::NonZero(new_oid) = new_oid {
920 mark_commit_reachable(repo, *new_oid)?;
921 }
922 }
923
924 let head_info = repo.get_head_info()?;
925 if head_info.oid.is_some() {
926 repo.detach_head(&head_info)?;
929 }
930
931 move_branches(effects, git_run_info, repo, *event_tx_id, rewritten_oids)?;
932
933 #[allow(clippy::format_collect)]
936 let post_rewrite_stdin: String = rewritten_oids
937 .iter()
938 .map(|(old_oid, new_oid)| format!("{old_oid} {new_oid}\n"))
939 .collect();
940 let post_rewrite_stdin = BString::from(post_rewrite_stdin);
941 git_run_info.run_hook(
942 effects,
943 repo,
944 "post-rewrite",
945 *event_tx_id,
946 &["rebase"],
947 Some(post_rewrite_stdin),
948 )?;
949
950 let exit_code = check_out_updated_head(
951 effects,
952 git_run_info,
953 repo,
954 event_log_db,
955 *event_tx_id,
956 rewritten_oids,
957 &head_info,
958 skipped_head_updated_oid,
959 check_out_commit_options,
960 )?;
961 Ok(exit_code)
962 }
963}
964
965mod on_disk {
966 use std::fmt::Write;
967
968 use eyre::Context;
969 use tracing::instrument;
970
971 use crate::core::effects::{Effects, OperationType};
972 use crate::core::rewrite::plan::RebaseCommand;
973 use crate::core::rewrite::plan::RebasePlan;
974 use crate::core::rewrite::rewrite_hooks::save_original_head_info;
975 use crate::git::{GitRunInfo, Repo};
976
977 use crate::util::ExitCode;
978
979 use super::ExecuteRebasePlanOptions;
980
981 pub enum Error {
982 ChangedFilesInRepository,
983 OperationAlreadyInProgress { operation_type: String },
984 }
985
986 fn write_rebase_state_to_disk(
987 effects: &Effects,
988 git_run_info: &GitRunInfo,
989 repo: &Repo,
990 rebase_plan: &RebasePlan,
991 options: &ExecuteRebasePlanOptions,
992 ) -> eyre::Result<Result<(), Error>> {
993 let ExecuteRebasePlanOptions {
994 now: _,
995 event_tx_id: _,
996 preserve_timestamps,
997 force_in_memory: _,
998 force_on_disk: _,
999 dry_run: _,
1000 resolve_merge_conflicts: _,
1001 check_out_commit_options: _, } = options;
1003
1004 let (effects, _progress) = effects.start_operation(OperationType::InitializeRebase);
1005
1006 let head_info = repo.get_head_info()?;
1007
1008 let current_operation_type = repo.get_current_operation_type();
1009 if let Some(current_operation_type) = current_operation_type {
1010 return Ok(Err(Error::OperationAlreadyInProgress {
1011 operation_type: current_operation_type.to_string(),
1012 }));
1013 }
1014
1015 if repo.has_changed_files(&effects, git_run_info)? {
1016 return Ok(Err(Error::ChangedFilesInRepository));
1017 }
1018
1019 let rebase_state_dir = repo.get_rebase_state_dir_path();
1020 std::fs::create_dir_all(&rebase_state_dir).wrap_err_with(|| {
1021 format!(
1022 "Creating rebase state directory at: {:?}",
1023 &rebase_state_dir
1024 )
1025 })?;
1026
1027 let interactive_file_path = rebase_state_dir.join("interactive");
1035 std::fs::write(&interactive_file_path, "")
1036 .wrap_err_with(|| format!("Writing interactive to: {:?}", &interactive_file_path))?;
1037
1038 if let Some(head_oid) = head_info.oid {
1039 let orig_head_file_path = repo.get_path().join("ORIG_HEAD");
1040 std::fs::write(&orig_head_file_path, head_oid.to_string())
1041 .wrap_err_with(|| format!("Writing `ORIG_HEAD` to: {:?}", &orig_head_file_path))?;
1042
1043 let rebase_orig_head_file_path = rebase_state_dir.join("orig-head");
1047 std::fs::write(&rebase_orig_head_file_path, head_oid.to_string()).wrap_err_with(
1048 || format!("Writing `orig-head` to: {:?}", &rebase_orig_head_file_path),
1049 )?;
1050
1051 let head_name_file_path = rebase_state_dir.join("head-name");
1055 std::fs::write(
1056 &head_name_file_path,
1057 head_info
1058 .reference_name
1059 .as_ref()
1060 .map(|reference_name| reference_name.as_str())
1061 .unwrap_or("detached HEAD"),
1062 )
1063 .wrap_err_with(|| format!("Writing head-name to: {:?}", &head_name_file_path))?;
1064
1065 save_original_head_info(repo, &head_info)?;
1066
1067 let rebase_merge_head_file_path = rebase_state_dir.join("head");
1070 std::fs::write(
1071 &rebase_merge_head_file_path,
1072 rebase_plan.first_dest_oid.to_string(),
1073 )
1074 .wrap_err_with(|| format!("Writing head to: {:?}", &rebase_merge_head_file_path))?;
1075 }
1076
1077 let onto_file_path = rebase_state_dir.join("onto");
1081 std::fs::write(&onto_file_path, rebase_plan.first_dest_oid.to_string()).wrap_err_with(
1082 || {
1083 format!(
1084 "Writing onto {:?} to: {:?}",
1085 &rebase_plan.first_dest_oid, &onto_file_path
1086 )
1087 },
1088 )?;
1089
1090 if rebase_plan.commands.iter().any(|command| match command {
1091 RebaseCommand::Pick {
1092 original_commit_oid,
1093 commits_to_apply_oids,
1094 } => !commits_to_apply_oids
1095 .iter()
1096 .any(|oid| oid == original_commit_oid),
1097 _ => false,
1098 }) {
1099 eyre::bail!("Not implemented: replacing commits in an on disk rebase");
1100 }
1101
1102 let todo_file_path = rebase_state_dir.join("git-rebase-todo");
1103 #[allow(clippy::format_collect)]
1104 std::fs::write(
1105 &todo_file_path,
1106 rebase_plan
1107 .commands
1108 .iter()
1109 .map(|command| format!("{}\n", command.to_rebase_command()))
1110 .collect::<String>(),
1111 )
1112 .wrap_err_with(|| {
1113 format!(
1114 "Writing `git-rebase-todo` to: {:?}",
1115 todo_file_path.as_path()
1116 )
1117 })?;
1118
1119 let end_file_path = rebase_state_dir.join("end");
1120 std::fs::write(
1121 end_file_path.as_path(),
1122 format!("{}\n", rebase_plan.commands.len()),
1123 )
1124 .wrap_err_with(|| format!("Writing `end` to: {:?}", end_file_path.as_path()))?;
1125
1126 let keep_redundant_commits_file_path = rebase_state_dir.join("keep_redundant_commits");
1129 std::fs::write(&keep_redundant_commits_file_path, "").wrap_err_with(|| {
1130 format!(
1131 "Writing `keep_redundant_commits` to: {:?}",
1132 &keep_redundant_commits_file_path
1133 )
1134 })?;
1135
1136 if *preserve_timestamps {
1137 let cdate_is_adate_file_path = rebase_state_dir.join("cdate_is_adate");
1138 std::fs::write(&cdate_is_adate_file_path, "").wrap_err_with(|| {
1139 format!(
1140 "Writing `cdate_is_adate` option file to: {:?}",
1141 &cdate_is_adate_file_path
1142 )
1143 })?;
1144 }
1145
1146 if head_info.oid.is_some() {
1150 repo.detach_head(&head_info)?;
1151 }
1152
1153 Ok(Ok(()))
1154 }
1155
1156 #[instrument]
1162 pub fn rebase_on_disk(
1163 effects: &Effects,
1164 git_run_info: &GitRunInfo,
1165 repo: &Repo,
1166 rebase_plan: &RebasePlan,
1167 options: &ExecuteRebasePlanOptions,
1168 ) -> eyre::Result<Result<ExitCode, Error>> {
1169 let ExecuteRebasePlanOptions {
1170 now: _,
1172 event_tx_id,
1173 preserve_timestamps: _,
1174 force_in_memory: _,
1175 force_on_disk: _,
1176 dry_run: _,
1177 resolve_merge_conflicts: _,
1178 check_out_commit_options: _, } = options;
1180
1181 match write_rebase_state_to_disk(effects, git_run_info, repo, rebase_plan, options)? {
1182 Ok(()) => {}
1183 Err(err) => return Ok(Err(err)),
1184 };
1185
1186 writeln!(
1187 effects.get_output_stream(),
1188 "Calling Git for on-disk rebase..."
1189 )?;
1190 match git_run_info.run(effects, Some(*event_tx_id), &["rebase", "--continue"])? {
1191 Ok(()) => Ok(Ok(ExitCode::success())),
1192 Err(err) => Ok(Ok(err)),
1193 }
1194 }
1195}
1196
1197#[derive(Clone, Debug)]
1199pub struct ExecuteRebasePlanOptions {
1200 pub now: SystemTime,
1202
1203 pub event_tx_id: EventTransactionId,
1205
1206 pub preserve_timestamps: bool,
1210
1211 pub force_in_memory: bool,
1213
1214 pub force_on_disk: bool,
1216
1217 pub dry_run: bool,
1220
1221 pub resolve_merge_conflicts: bool,
1224
1225 pub check_out_commit_options: CheckOutCommitOptions,
1227}
1228
1229#[must_use]
1231#[derive(Debug)]
1232pub enum ExecuteRebasePlanResult {
1233 Succeeded {
1235 rewritten_oids: Option<HashMap<NonZeroOid, MaybeZeroOid>>,
1237 },
1238
1239 WouldSucceed,
1241
1242 DeclinedToMerge {
1245 failed_merge_info: FailedMergeInfo,
1247 },
1248
1249 Failed {
1251 exit_code: ExitCode,
1254 },
1255}
1256
1257pub fn execute_rebase_plan(
1260 effects: &Effects,
1261 git_run_info: &GitRunInfo,
1262 repo: &Repo,
1263 event_log_db: &EventLogDb,
1264 rebase_plan: &RebasePlan,
1265 options: &ExecuteRebasePlanOptions,
1266) -> eyre::Result<ExecuteRebasePlanResult> {
1267 let ExecuteRebasePlanOptions {
1268 now: _,
1269 event_tx_id: _,
1270 preserve_timestamps: _,
1271 force_in_memory,
1272 force_on_disk,
1273 dry_run,
1274 resolve_merge_conflicts,
1275 check_out_commit_options: _,
1276 } = options;
1277
1278 if !force_on_disk {
1279 use in_memory::*;
1280 writeln!(
1281 effects.get_output_stream(),
1282 "Attempting rebase in-memory..."
1283 )?;
1284
1285 let failed_merge_info = match rebase_in_memory(effects, repo, rebase_plan, options)? {
1286 RebaseInMemoryResult::MergeFailed(failed_merge_info) => failed_merge_info,
1287
1288 RebaseInMemoryResult::Succeeded {
1289 rewritten_oids,
1290 new_head_oid,
1291 } => {
1292 if *dry_run {
1293 writeln!(
1294 effects.get_output_stream(),
1295 "In-memory rebase would succeed."
1296 )?;
1297 return Ok(ExecuteRebasePlanResult::WouldSucceed);
1298 }
1299
1300 match post_rebase_in_memory(
1305 effects,
1306 git_run_info,
1307 repo,
1308 event_log_db,
1309 &rewritten_oids,
1310 new_head_oid,
1311 options,
1312 )? {
1313 Ok(()) => {}
1314 Err(_exit_code) => {
1315 }
1318 }
1319
1320 writeln!(effects.get_output_stream(), "In-memory rebase succeeded.")?;
1321 return Ok(ExecuteRebasePlanResult::Succeeded {
1322 rewritten_oids: Some(rewritten_oids),
1323 });
1324 }
1325 };
1326
1327 if !resolve_merge_conflicts {
1328 return Ok(ExecuteRebasePlanResult::DeclinedToMerge { failed_merge_info });
1329 }
1330
1331 if *force_in_memory {
1334 writeln!(
1335 effects.get_output_stream(),
1336 "Aborting since an in-memory rebase was requested."
1337 )?;
1338 return Ok(ExecuteRebasePlanResult::DeclinedToMerge { failed_merge_info });
1339 } else {
1340 writeln!(
1341 effects.get_output_stream(),
1342 "Failed to merge in-memory, trying again on-disk..."
1343 )?;
1344 }
1345 }
1346
1347 if !force_in_memory {
1348 use on_disk::*;
1349 match rebase_on_disk(effects, git_run_info, repo, rebase_plan, options)? {
1350 Ok(exit_code) if exit_code.is_success() => {
1351 return Ok(ExecuteRebasePlanResult::Succeeded {
1352 rewritten_oids: None,
1353 });
1354 }
1355 Ok(exit_code) => return Ok(ExecuteRebasePlanResult::Failed { exit_code }),
1356 Err(Error::ChangedFilesInRepository) => {
1357 write!(
1358 effects.get_output_stream(),
1359 "\
1360This operation would modify the working copy, but you have uncommitted changes
1361in your working copy which might be overwritten as a result.
1362Commit your changes and then try again.
1363"
1364 )?;
1365 return Ok(ExecuteRebasePlanResult::Failed {
1366 exit_code: ExitCode(1),
1367 });
1368 }
1369 Err(Error::OperationAlreadyInProgress { operation_type }) => {
1370 writeln!(
1371 effects.get_output_stream(),
1372 "A {operation_type} operation is already in progress."
1373 )?;
1374 writeln!(
1375 effects.get_output_stream(),
1376 "Run git {operation_type} --continue or git {operation_type} --abort to resolve it and proceed."
1377 )?;
1378 return Ok(ExecuteRebasePlanResult::Failed {
1379 exit_code: ExitCode(1),
1380 });
1381 }
1382 }
1383 }
1384
1385 eyre::bail!("Both force_in_memory and force_on_disk were requested, but these options conflict")
1386}