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::{check_out_commit, CheckOutCommitOptions, CheckoutTarget};
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 resolve_merge_conflicts: _, check_out_commit_options: _, } = options;
504
505 let mut current_oid = rebase_plan.first_dest_oid;
506 let mut labels: HashMap<String, NonZeroOid> = HashMap::new();
507 let mut rewritten_oids: HashMap<NonZeroOid, MaybeZeroOid> = HashMap::new();
508
509 let head_oid = repo.get_head_info()?.oid;
515 let mut skipped_head_new_oid = None;
516 let mut maybe_set_skipped_head_new_oid = |skipped_head_oid, current_oid| {
517 if Some(skipped_head_oid) == head_oid {
518 skipped_head_new_oid.get_or_insert(current_oid);
519 }
520 };
521
522 let mut i = 0;
523 let num_picks = rebase_plan
524 .commands
525 .iter()
526 .filter(|command| match command {
527 RebaseCommand::CreateLabel { .. }
528 | RebaseCommand::Reset { .. }
529 | RebaseCommand::Break
530 | RebaseCommand::RegisterExtraPostRewriteHook
531 | RebaseCommand::DetectEmptyCommit { .. } => false,
532 RebaseCommand::Pick { .. }
533 | RebaseCommand::Merge { .. }
534 | RebaseCommand::Replace { .. }
535 | RebaseCommand::SkipUpstreamAppliedCommit { .. } => true,
536 })
537 .count();
538 let (effects, progress) = effects.start_operation(OperationType::RebaseCommits);
539
540 for command in rebase_plan.commands.iter() {
541 match command {
542 RebaseCommand::CreateLabel { label_name } => {
543 labels.insert(label_name.clone(), current_oid);
544 }
545
546 RebaseCommand::Reset {
547 target: OidOrLabel::Label(label_name),
548 } => {
549 current_oid = match labels.get(label_name) {
550 Some(oid) => *oid,
551 None => eyre::bail!("BUG: no associated OID for label: {label_name}"),
552 };
553 }
554
555 RebaseCommand::Reset {
556 target: OidOrLabel::Oid(commit_oid),
557 } => {
558 current_oid = match rewritten_oids.get(commit_oid) {
559 Some(MaybeZeroOid::NonZero(rewritten_oid)) => {
560 *rewritten_oid
562 }
563 Some(MaybeZeroOid::Zero) | None => {
564 *commit_oid
568 }
569 };
570 }
571
572 RebaseCommand::Pick {
573 original_commit_oid,
574 commits_to_apply_oids,
575 } => {
576 let current_commit = repo
577 .find_commit_or_fail(current_oid)
578 .wrap_err("Finding current commit")?;
579
580 let original_commit = repo
581 .find_commit_or_fail(*original_commit_oid)
582 .wrap_err("Finding commit to apply")?;
583 i += 1;
584
585 let commit_num = format!("[{i}/{num_picks}]");
586 progress.notify_progress(i, num_picks);
587
588 let commit_message = original_commit.get_message_raw();
589 let commit_message = commit_message.to_str().with_context(|| {
590 eyre::eyre!(
591 "Could not decode commit message for commit: {:?}",
592 original_commit_oid
593 )
594 })?;
595
596 let commit_author = original_commit.get_author();
597 let committer_signature = if *preserve_timestamps {
598 original_commit.get_committer()
599 } else {
600 original_commit.get_committer().update_timestamp(*now)?
601 };
602 let mut rebased_commit_oid = None;
603 let mut rebased_commit = None;
604
605 for commit_oid in commits_to_apply_oids.iter() {
606 let commit_to_apply = repo
607 .find_commit_or_fail(*commit_oid)
608 .wrap_err("Finding commit to apply")?;
609 let commit_description = effects
610 .get_glyphs()
611 .render(commit_to_apply.friendly_describe(effects.get_glyphs())?)?;
612
613 if commit_to_apply.get_parent_count() > 1 {
614 warn!(
615 ?commit_oid,
616 "BUG: Merge commit should have been detected during planning phase"
617 );
618 return Ok(RebaseInMemoryResult::MergeFailed(
619 FailedMergeInfo::CannotRebaseMergeInMemory {
620 commit_oid: *commit_oid,
621 },
622 ));
623 };
624
625 progress.notify_status(
626 OperationIcon::InProgress,
627 format!("Applying patch for commit: {commit_description}"),
628 );
629
630 let maybe_tree = if rebased_commit.is_none() {
636 repo.cherry_pick_fast(
637 &commit_to_apply,
638 ¤t_commit,
639 &CherryPickFastOptions {
640 reuse_parent_tree_if_possible: true,
641 },
642 )
643 } else {
644 repo.amend_fast(
645 &rebased_commit.expect("rebased commit should not be None"),
646 &AmendFastOptions::FromCommit {
647 commit: commit_to_apply,
648 },
649 )
650 };
651 let commit_tree = match maybe_tree {
652 Ok(tree) => tree,
653 Err(CreateCommitFastError::MergeConflict { conflicting_paths }) => {
654 return Ok(RebaseInMemoryResult::MergeFailed(
655 FailedMergeInfo::Conflict {
656 commit_oid: *commit_oid,
657 conflicting_paths,
658 },
659 ))
660 }
661 Err(other) => eyre::bail!(other),
662 };
663
664 progress.notify_status(
668 OperationIcon::InProgress,
669 format!("Committing to repository: {commit_description}"),
670 );
671 rebased_commit_oid = Some(
672 repo.create_commit(
673 None,
674 &commit_author,
675 &committer_signature,
676 commit_message,
677 &commit_tree,
678 vec![¤t_commit],
679 )
680 .wrap_err("Applying rebased commit")?,
681 );
682
683 rebased_commit = repo.find_commit(rebased_commit_oid.unwrap())?;
684 }
685
686 let rebased_commit_oid =
687 rebased_commit_oid.expect("rebased oid should not be None");
688 let commit_description =
689 effects
690 .get_glyphs()
691 .render(repo.friendly_describe_commit_from_oid(
692 effects.get_glyphs(),
693 rebased_commit_oid,
694 )?)?;
695
696 if rebased_commit
697 .expect("rebased commit should not be None")
698 .is_empty()
699 {
700 rewritten_oids.insert(*original_commit_oid, MaybeZeroOid::Zero);
701 maybe_set_skipped_head_new_oid(*original_commit_oid, current_oid);
702
703 writeln!(
704 effects.get_output_stream(),
705 "{commit_num} Skipped now-empty commit: {commit_description}",
706 )?;
707 } else {
708 rewritten_oids.insert(
709 *original_commit_oid,
710 MaybeZeroOid::NonZero(rebased_commit_oid),
711 );
712 for commit_oid in commits_to_apply_oids {
713 rewritten_oids
714 .insert(*commit_oid, MaybeZeroOid::NonZero(rebased_commit_oid));
715 }
716
717 current_oid = rebased_commit_oid;
718
719 writeln!(
720 effects.get_output_stream(),
721 "{commit_num} Committed as: {commit_description}"
722 )?;
723 }
724 }
725
726 RebaseCommand::Merge {
727 commit_oid,
728 commits_to_merge: _,
729 } => {
730 warn!(
731 ?commit_oid,
732 "BUG: Merge commit without replacement should have been detected when starting in-memory rebase"
733 );
734 return Ok(RebaseInMemoryResult::MergeFailed(
735 FailedMergeInfo::CannotRebaseMergeInMemory {
736 commit_oid: *commit_oid,
737 },
738 ));
739 }
740
741 RebaseCommand::Replace {
742 commit_oid,
743 replacement_commit_oid,
744 parents,
745 } => {
746 let original_commit = repo
747 .find_commit_or_fail(*commit_oid)
748 .wrap_err("Finding current commit")?;
749 let original_commit_description = effects
750 .get_glyphs()
751 .render(original_commit.friendly_describe(effects.get_glyphs())?)?;
752
753 i += 1;
754 let commit_num = format!("[{i}/{num_picks}]");
755 progress.notify_progress(i, num_picks);
756 progress.notify_status(
757 OperationIcon::InProgress,
758 format!("Replacing commit: {original_commit_description}"),
759 );
760
761 let replacement_commit = repo.find_commit_or_fail(*replacement_commit_oid)?;
762 let replacement_tree = replacement_commit.get_tree()?;
763 let replacement_message = replacement_commit.get_message_raw();
764 let replacement_commit_message =
765 replacement_message.to_str().with_context(|| {
766 eyre::eyre!(
767 "Could not decode commit message for replacement commit: {:?}",
768 replacement_commit
769 )
770 })?;
771
772 let replacement_commit_description = effects
773 .get_glyphs()
774 .render(replacement_commit.friendly_describe(effects.get_glyphs())?)?;
775 progress.notify_status(
776 OperationIcon::InProgress,
777 format!("Committing to repository: {replacement_commit_description}"),
778 );
779 let committer_signature = if *preserve_timestamps {
780 replacement_commit.get_committer()
781 } else {
782 replacement_commit.get_committer().update_timestamp(*now)?
783 };
784 let parents = {
785 let mut result = Vec::new();
786 for parent in parents {
787 let parent_oid = match parent {
788 OidOrLabel::Oid(oid) => *oid,
789 OidOrLabel::Label(label) => {
790 let oid = labels.get(label).ok_or_else(|| {
791 eyre::eyre!(
792 "Label {label} could not be resolved to a commit"
793 )
794 })?;
795 *oid
796 }
797 };
798 let parent_commit = repo.find_commit_or_fail(parent_oid)?;
799 result.push(parent_commit);
800 }
801 result
802 };
803 let rebased_commit_oid = repo
804 .create_commit(
805 None,
806 &replacement_commit.get_author(),
807 &committer_signature,
808 replacement_commit_message,
809 &replacement_tree,
810 parents.iter().collect(),
811 )
812 .wrap_err("Applying rebased commit")?;
813
814 let commit_description =
815 effects
816 .get_glyphs()
817 .render(repo.friendly_describe_commit_from_oid(
818 effects.get_glyphs(),
819 rebased_commit_oid,
820 )?)?;
821 rewritten_oids.insert(*commit_oid, MaybeZeroOid::NonZero(rebased_commit_oid));
822 current_oid = rebased_commit_oid;
823
824 writeln!(
825 effects.get_output_stream(),
826 "{commit_num} Committed as: {commit_description}"
827 )?;
828 }
829
830 RebaseCommand::Break => {
831 eyre::bail!("`break` not supported for in-memory rebases");
832 }
833
834 RebaseCommand::SkipUpstreamAppliedCommit { commit_oid } => {
835 i += 1;
836 let commit_num = format!("[{i}/{num_picks}]");
837
838 let commit = repo.find_commit_or_fail(*commit_oid)?;
839 rewritten_oids.insert(*commit_oid, MaybeZeroOid::Zero);
840 maybe_set_skipped_head_new_oid(*commit_oid, current_oid);
841
842 let commit_description = commit.friendly_describe(effects.get_glyphs())?;
843 let commit_description = effects.get_glyphs().render(commit_description)?;
844 writeln!(
845 effects.get_output_stream(),
846 "{commit_num} Skipped commit (was already applied upstream): {commit_description}"
847 )?;
848 }
849
850 RebaseCommand::RegisterExtraPostRewriteHook
851 | RebaseCommand::DetectEmptyCommit { .. } => {
852 }
855 }
856 }
857
858 let new_head_oid: Option<NonZeroOid> = match head_oid {
859 None => {
860 None
862 }
863 Some(head_oid) => {
864 match rewritten_oids.get(&head_oid) {
865 Some(MaybeZeroOid::NonZero(new_head_oid)) => {
866 Some(*new_head_oid)
868 }
869 Some(MaybeZeroOid::Zero) => {
870 let new_head_oid = match skipped_head_new_oid {
873 Some(new_head_oid) => new_head_oid,
874 None => {
875 warn!(
876 ?head_oid,
877 "`HEAD` OID was rewritten to 0, but no skipped `HEAD` OID was set",
878 );
879 head_oid
880 }
881 };
882 Some(new_head_oid)
883 }
884 None => {
885 Some(head_oid)
887 }
888 }
889 }
890 };
891 Ok(RebaseInMemoryResult::Succeeded {
892 rewritten_oids,
893 new_head_oid,
894 })
895 }
896
897 pub fn post_rebase_in_memory(
898 effects: &Effects,
899 git_run_info: &GitRunInfo,
900 repo: &Repo,
901 event_log_db: &EventLogDb,
902 rewritten_oids: &HashMap<NonZeroOid, MaybeZeroOid>,
903 skipped_head_updated_oid: Option<NonZeroOid>,
904 options: &ExecuteRebasePlanOptions,
905 ) -> EyreExitOr<()> {
906 let ExecuteRebasePlanOptions {
907 now: _,
908 event_tx_id,
909 preserve_timestamps: _,
910 force_in_memory: _,
911 force_on_disk: _,
912 resolve_merge_conflicts: _,
913 check_out_commit_options,
914 } = options;
915
916 for new_oid in rewritten_oids.values() {
917 if let MaybeZeroOid::NonZero(new_oid) = new_oid {
918 mark_commit_reachable(repo, *new_oid)?;
919 }
920 }
921
922 let head_info = repo.get_head_info()?;
923 if head_info.oid.is_some() {
924 repo.detach_head(&head_info)?;
927 }
928
929 move_branches(effects, git_run_info, repo, *event_tx_id, rewritten_oids)?;
930
931 #[allow(clippy::format_collect)]
934 let post_rewrite_stdin: String = rewritten_oids
935 .iter()
936 .map(|(old_oid, new_oid)| format!("{old_oid} {new_oid}\n"))
937 .collect();
938 let post_rewrite_stdin = BString::from(post_rewrite_stdin);
939 git_run_info.run_hook(
940 effects,
941 repo,
942 "post-rewrite",
943 *event_tx_id,
944 &["rebase"],
945 Some(post_rewrite_stdin),
946 )?;
947
948 let exit_code = check_out_updated_head(
949 effects,
950 git_run_info,
951 repo,
952 event_log_db,
953 *event_tx_id,
954 rewritten_oids,
955 &head_info,
956 skipped_head_updated_oid,
957 check_out_commit_options,
958 )?;
959 Ok(exit_code)
960 }
961}
962
963mod on_disk {
964 use std::fmt::Write;
965
966 use eyre::Context;
967 use tracing::instrument;
968
969 use crate::core::effects::{Effects, OperationType};
970 use crate::core::rewrite::plan::RebaseCommand;
971 use crate::core::rewrite::plan::RebasePlan;
972 use crate::core::rewrite::rewrite_hooks::save_original_head_info;
973 use crate::git::{GitRunInfo, Repo};
974
975 use crate::util::ExitCode;
976
977 use super::ExecuteRebasePlanOptions;
978
979 pub enum Error {
980 ChangedFilesInRepository,
981 OperationAlreadyInProgress { operation_type: String },
982 }
983
984 fn write_rebase_state_to_disk(
985 effects: &Effects,
986 git_run_info: &GitRunInfo,
987 repo: &Repo,
988 rebase_plan: &RebasePlan,
989 options: &ExecuteRebasePlanOptions,
990 ) -> eyre::Result<Result<(), Error>> {
991 let ExecuteRebasePlanOptions {
992 now: _,
993 event_tx_id: _,
994 preserve_timestamps,
995 force_in_memory: _,
996 force_on_disk: _,
997 resolve_merge_conflicts: _,
998 check_out_commit_options: _, } = options;
1000
1001 let (effects, _progress) = effects.start_operation(OperationType::InitializeRebase);
1002
1003 let head_info = repo.get_head_info()?;
1004
1005 let current_operation_type = repo.get_current_operation_type();
1006 if let Some(current_operation_type) = current_operation_type {
1007 return Ok(Err(Error::OperationAlreadyInProgress {
1008 operation_type: current_operation_type.to_string(),
1009 }));
1010 }
1011
1012 if repo.has_changed_files(&effects, git_run_info)? {
1013 return Ok(Err(Error::ChangedFilesInRepository));
1014 }
1015
1016 let rebase_state_dir = repo.get_rebase_state_dir_path();
1017 std::fs::create_dir_all(&rebase_state_dir).wrap_err_with(|| {
1018 format!(
1019 "Creating rebase state directory at: {:?}",
1020 &rebase_state_dir
1021 )
1022 })?;
1023
1024 let interactive_file_path = rebase_state_dir.join("interactive");
1032 std::fs::write(&interactive_file_path, "")
1033 .wrap_err_with(|| format!("Writing interactive to: {:?}", &interactive_file_path))?;
1034
1035 if let Some(head_oid) = head_info.oid {
1036 let orig_head_file_path = repo.get_path().join("ORIG_HEAD");
1037 std::fs::write(&orig_head_file_path, head_oid.to_string())
1038 .wrap_err_with(|| format!("Writing `ORIG_HEAD` to: {:?}", &orig_head_file_path))?;
1039
1040 let rebase_orig_head_file_path = rebase_state_dir.join("orig-head");
1044 std::fs::write(&rebase_orig_head_file_path, head_oid.to_string()).wrap_err_with(
1045 || format!("Writing `orig-head` to: {:?}", &rebase_orig_head_file_path),
1046 )?;
1047
1048 let head_name_file_path = rebase_state_dir.join("head-name");
1052 std::fs::write(
1053 &head_name_file_path,
1054 head_info
1055 .reference_name
1056 .as_ref()
1057 .map(|reference_name| reference_name.as_str())
1058 .unwrap_or("detached HEAD"),
1059 )
1060 .wrap_err_with(|| format!("Writing head-name to: {:?}", &head_name_file_path))?;
1061
1062 save_original_head_info(repo, &head_info)?;
1063
1064 let rebase_merge_head_file_path = rebase_state_dir.join("head");
1067 std::fs::write(
1068 &rebase_merge_head_file_path,
1069 rebase_plan.first_dest_oid.to_string(),
1070 )
1071 .wrap_err_with(|| format!("Writing head to: {:?}", &rebase_merge_head_file_path))?;
1072 }
1073
1074 let onto_file_path = rebase_state_dir.join("onto");
1078 std::fs::write(&onto_file_path, rebase_plan.first_dest_oid.to_string()).wrap_err_with(
1079 || {
1080 format!(
1081 "Writing onto {:?} to: {:?}",
1082 &rebase_plan.first_dest_oid, &onto_file_path
1083 )
1084 },
1085 )?;
1086
1087 if rebase_plan.commands.iter().any(|command| match command {
1088 RebaseCommand::Pick {
1089 original_commit_oid,
1090 commits_to_apply_oids,
1091 } => !commits_to_apply_oids
1092 .iter()
1093 .any(|oid| oid == original_commit_oid),
1094 _ => false,
1095 }) {
1096 eyre::bail!("Not implemented: replacing commits in an on disk rebase");
1097 }
1098
1099 let todo_file_path = rebase_state_dir.join("git-rebase-todo");
1100 #[allow(clippy::format_collect)]
1101 std::fs::write(
1102 &todo_file_path,
1103 rebase_plan
1104 .commands
1105 .iter()
1106 .map(|command| format!("{}\n", command.to_rebase_command()))
1107 .collect::<String>(),
1108 )
1109 .wrap_err_with(|| {
1110 format!(
1111 "Writing `git-rebase-todo` to: {:?}",
1112 todo_file_path.as_path()
1113 )
1114 })?;
1115
1116 let end_file_path = rebase_state_dir.join("end");
1117 std::fs::write(
1118 end_file_path.as_path(),
1119 format!("{}\n", rebase_plan.commands.len()),
1120 )
1121 .wrap_err_with(|| format!("Writing `end` to: {:?}", end_file_path.as_path()))?;
1122
1123 let keep_redundant_commits_file_path = rebase_state_dir.join("keep_redundant_commits");
1126 std::fs::write(&keep_redundant_commits_file_path, "").wrap_err_with(|| {
1127 format!(
1128 "Writing `keep_redundant_commits` to: {:?}",
1129 &keep_redundant_commits_file_path
1130 )
1131 })?;
1132
1133 if *preserve_timestamps {
1134 let cdate_is_adate_file_path = rebase_state_dir.join("cdate_is_adate");
1135 std::fs::write(&cdate_is_adate_file_path, "").wrap_err_with(|| {
1136 format!(
1137 "Writing `cdate_is_adate` option file to: {:?}",
1138 &cdate_is_adate_file_path
1139 )
1140 })?;
1141 }
1142
1143 if head_info.oid.is_some() {
1147 repo.detach_head(&head_info)?;
1148 }
1149
1150 Ok(Ok(()))
1151 }
1152
1153 #[instrument]
1159 pub fn rebase_on_disk(
1160 effects: &Effects,
1161 git_run_info: &GitRunInfo,
1162 repo: &Repo,
1163 rebase_plan: &RebasePlan,
1164 options: &ExecuteRebasePlanOptions,
1165 ) -> eyre::Result<Result<ExitCode, Error>> {
1166 let ExecuteRebasePlanOptions {
1167 now: _,
1169 event_tx_id,
1170 preserve_timestamps: _,
1171 force_in_memory: _,
1172 force_on_disk: _,
1173 resolve_merge_conflicts: _,
1174 check_out_commit_options: _, } = options;
1176
1177 match write_rebase_state_to_disk(effects, git_run_info, repo, rebase_plan, options)? {
1178 Ok(()) => {}
1179 Err(err) => return Ok(Err(err)),
1180 };
1181
1182 writeln!(
1183 effects.get_output_stream(),
1184 "Calling Git for on-disk rebase..."
1185 )?;
1186 match git_run_info.run(effects, Some(*event_tx_id), &["rebase", "--continue"])? {
1187 Ok(()) => Ok(Ok(ExitCode::success())),
1188 Err(err) => Ok(Ok(err)),
1189 }
1190 }
1191}
1192
1193#[derive(Clone, Debug)]
1195pub struct ExecuteRebasePlanOptions {
1196 pub now: SystemTime,
1198
1199 pub event_tx_id: EventTransactionId,
1201
1202 pub preserve_timestamps: bool,
1206
1207 pub force_in_memory: bool,
1209
1210 pub force_on_disk: bool,
1212
1213 pub resolve_merge_conflicts: bool,
1216
1217 pub check_out_commit_options: CheckOutCommitOptions,
1219}
1220
1221#[must_use]
1223#[derive(Debug)]
1224pub enum ExecuteRebasePlanResult {
1225 Succeeded {
1227 rewritten_oids: Option<HashMap<NonZeroOid, MaybeZeroOid>>,
1229 },
1230
1231 DeclinedToMerge {
1234 failed_merge_info: FailedMergeInfo,
1236 },
1237
1238 Failed {
1240 exit_code: ExitCode,
1243 },
1244}
1245
1246pub fn execute_rebase_plan(
1249 effects: &Effects,
1250 git_run_info: &GitRunInfo,
1251 repo: &Repo,
1252 event_log_db: &EventLogDb,
1253 rebase_plan: &RebasePlan,
1254 options: &ExecuteRebasePlanOptions,
1255) -> eyre::Result<ExecuteRebasePlanResult> {
1256 let ExecuteRebasePlanOptions {
1257 now: _,
1258 event_tx_id: _,
1259 preserve_timestamps: _,
1260 force_in_memory,
1261 force_on_disk,
1262 resolve_merge_conflicts,
1263 check_out_commit_options: _,
1264 } = options;
1265
1266 if !force_on_disk {
1267 use in_memory::*;
1268 writeln!(
1269 effects.get_output_stream(),
1270 "Attempting rebase in-memory..."
1271 )?;
1272
1273 let failed_merge_info = match rebase_in_memory(effects, repo, rebase_plan, options)? {
1274 RebaseInMemoryResult::MergeFailed(failed_merge_info) => failed_merge_info,
1275
1276 RebaseInMemoryResult::Succeeded {
1277 rewritten_oids,
1278 new_head_oid,
1279 } => {
1280 match post_rebase_in_memory(
1285 effects,
1286 git_run_info,
1287 repo,
1288 event_log_db,
1289 &rewritten_oids,
1290 new_head_oid,
1291 options,
1292 )? {
1293 Ok(()) => {}
1294 Err(_exit_code) => {
1295 }
1298 }
1299
1300 writeln!(effects.get_output_stream(), "In-memory rebase succeeded.")?;
1301 return Ok(ExecuteRebasePlanResult::Succeeded {
1302 rewritten_oids: Some(rewritten_oids),
1303 });
1304 }
1305 };
1306
1307 if !resolve_merge_conflicts {
1308 return Ok(ExecuteRebasePlanResult::DeclinedToMerge { failed_merge_info });
1309 }
1310
1311 if *force_in_memory {
1314 writeln!(
1315 effects.get_output_stream(),
1316 "Aborting since an in-memory rebase was requested."
1317 )?;
1318 return Ok(ExecuteRebasePlanResult::DeclinedToMerge { failed_merge_info });
1319 } else {
1320 writeln!(
1321 effects.get_output_stream(),
1322 "Failed to merge in-memory, trying again on-disk..."
1323 )?;
1324 }
1325 }
1326
1327 if !force_in_memory {
1328 use on_disk::*;
1329 match rebase_on_disk(effects, git_run_info, repo, rebase_plan, options)? {
1330 Ok(exit_code) if exit_code.is_success() => {
1331 return Ok(ExecuteRebasePlanResult::Succeeded {
1332 rewritten_oids: None,
1333 });
1334 }
1335 Ok(exit_code) => return Ok(ExecuteRebasePlanResult::Failed { exit_code }),
1336 Err(Error::ChangedFilesInRepository) => {
1337 write!(
1338 effects.get_output_stream(),
1339 "\
1340This operation would modify the working copy, but you have uncommitted changes
1341in your working copy which might be overwritten as a result.
1342Commit your changes and then try again.
1343"
1344 )?;
1345 return Ok(ExecuteRebasePlanResult::Failed {
1346 exit_code: ExitCode(1),
1347 });
1348 }
1349 Err(Error::OperationAlreadyInProgress { operation_type }) => {
1350 writeln!(
1351 effects.get_output_stream(),
1352 "A {operation_type} operation is already in progress."
1353 )?;
1354 writeln!(
1355 effects.get_output_stream(),
1356 "Run git {operation_type} --continue or git {operation_type} --abort to resolve it and proceed."
1357 )?;
1358 return Ok(ExecuteRebasePlanResult::Failed {
1359 exit_code: ExitCode(1),
1360 });
1361 }
1362 }
1363 }
1364
1365 eyre::bail!("Both force_in_memory and force_on_disk were requested, but these options conflict")
1366}