1use std::collections::{HashMap, HashSet};
2use std::fmt::{Display, Write};
3use std::ops::Sub;
4use std::path::PathBuf;
5use std::sync::Arc;
6
7use chashmap::CHashMap;
8use eyre::Context;
9use itertools::Itertools;
10use rayon::{prelude::*, ThreadPool};
11use tracing::{instrument, warn};
12
13use crate::core::dag::{sorted_commit_set, union_all, CommitSet, Dag};
14use crate::core::effects::{Effects, OperationType, WithProgress};
15use crate::core::formatting::Pluralize;
16use crate::core::rewrite::{RepoPool, RepoResource};
17use crate::core::task::ResourcePool;
18use crate::git::{Commit, NonZeroOid, PatchId, Repo};
19
20#[derive(Clone, Debug)]
22pub enum OidOrLabel {
23 Oid(NonZeroOid),
25
26 Label(String),
28}
29
30impl Display for OidOrLabel {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 OidOrLabel::Oid(oid) => write!(f, "{oid}"),
34 OidOrLabel::Label(label) => write!(f, "{label}"),
35 }
36 }
37}
38
39#[derive(Debug)]
41pub enum RebaseCommand {
42 CreateLabel {
45 label_name: String,
47 },
48
49 Reset {
51 target: OidOrLabel,
53 },
54
55 Pick {
61 original_commit_oid: NonZeroOid,
64
65 commits_to_apply_oids: Vec<NonZeroOid>,
74 },
75
76 Merge {
78 commit_oid: NonZeroOid,
80
81 commits_to_merge: Vec<OidOrLabel>,
86 },
87
88 Replace {
93 commit_oid: NonZeroOid,
96
97 replacement_commit_oid: NonZeroOid,
99
100 parents: Vec<OidOrLabel>,
102 },
103
104 Break,
107
108 RegisterExtraPostRewriteHook,
111
112 DetectEmptyCommit {
115 commit_oid: NonZeroOid,
118 },
119
120 SkipUpstreamAppliedCommit {
123 commit_oid: NonZeroOid,
125 },
126}
127
128impl RebaseCommand {
129 pub fn to_rebase_command(&self) -> String {
132 match self {
133 RebaseCommand::CreateLabel { label_name } => format!("label {label_name}"),
134 RebaseCommand::Reset { target } => format!("reset {target}"),
135 RebaseCommand::Pick {
136 original_commit_oid: _,
137 commits_to_apply_oids,
138 } => match commits_to_apply_oids.as_slice() {
139 [] => String::new(),
140 [commit_oid] => format!("pick {commit_oid}"),
141 [..] => unimplemented!("On-disk fixups are not yet supported"),
142 },
143 RebaseCommand::Merge {
144 commit_oid,
145 commits_to_merge,
146 } => format!(
147 "merge -C {} {}",
148 commit_oid,
149 commits_to_merge
150 .iter()
151 .map(|commit_id| commit_id.to_string())
152 .join(" ")
153 ),
154 RebaseCommand::Replace {
155 commit_oid: _,
156 replacement_commit_oid,
157 parents,
158 } => {
159 let parents = parents
160 .iter()
161 .map(|parent| match parent {
162 OidOrLabel::Oid(parent_oid) => format!("-p {parent_oid}"),
163 OidOrLabel::Label(parent_label) => {
164 format!("-p refs/rewritten/{parent_label}")
165 }
166 })
167 .join(" ");
168 format!("exec git show -s --format=%B {replacement_commit_oid} | git commit-tree {parents} {replacement_commit_oid}^{{tree}}")
173 }
174 RebaseCommand::Break => "break".to_string(),
175 RebaseCommand::RegisterExtraPostRewriteHook => {
176 "exec git branchless hook-register-extra-post-rewrite-hook".to_string()
177 }
178 RebaseCommand::DetectEmptyCommit { commit_oid } => {
179 format!("exec git branchless hook-detect-empty-commit {commit_oid}")
180 }
181 RebaseCommand::SkipUpstreamAppliedCommit { commit_oid } => {
182 format!("exec git branchless hook-skip-upstream-applied-commit {commit_oid}")
183 }
184 }
185 }
186}
187
188#[derive(Debug)]
191pub struct RebasePlan {
192 pub first_dest_oid: NonZeroOid,
195
196 pub commands: Vec<RebaseCommand>,
198}
199
200#[derive(Clone, Debug)]
202pub struct RebasePlanPermissions {
203 pub(crate) build_options: BuildRebasePlanOptions,
204 pub(crate) allowed_commits: CommitSet,
205}
206
207impl RebasePlanPermissions {
208 pub fn verify_rewrite_set(
210 dag: &Dag,
211 build_options: BuildRebasePlanOptions,
212 commits: &CommitSet,
213 ) -> eyre::Result<Result<Self, BuildRebasePlanError>> {
214 let commits = dag.query_descendants(commits.clone())?;
217
218 let public_commits = dag.query_public_commits_slow()?;
219 if !build_options.force_rewrite_public_commits {
220 let public_commits_to_move = public_commits.intersection(&commits);
221 if !dag.set_is_empty(&public_commits_to_move)? {
222 return Ok(Err(BuildRebasePlanError::MovePublicCommits {
223 public_commits_to_move,
224 }));
225 }
226 }
227
228 Ok(Ok(RebasePlanPermissions {
229 build_options,
230 allowed_commits: commits,
231 }))
232 }
233}
234
235#[derive(Clone, Debug)]
237struct ConstraintGraph<'a> {
238 dag: &'a Dag,
239 permissions: &'a RebasePlanPermissions,
240
241 inner: HashMap<NonZeroOid, HashSet<NonZeroOid>>,
243
244 fixups: HashMap<NonZeroOid, HashSet<NonZeroOid>>,
246}
247
248impl<'a> ConstraintGraph<'a> {
249 pub fn new(dag: &'a Dag, permissions: &'a RebasePlanPermissions) -> Self {
250 Self {
251 dag,
252 permissions,
253 inner: HashMap::new(),
254 fixups: HashMap::new(),
255 }
256 }
257
258 pub fn add_constraints(&mut self, constraints: &Vec<Constraint>) -> eyre::Result<()> {
259 for constraint in constraints {
260 match constraint {
261 Constraint::MoveChildren { .. } => {
262 }
264
265 Constraint::MoveSubtree {
266 parent_oids,
267 child_oid,
268 } => {
269 for commits in self.inner.values_mut() {
271 commits.remove(child_oid);
272 }
273
274 for parent_oid in parent_oids {
275 self.inner
276 .entry(*parent_oid)
277 .or_default()
278 .insert(*child_oid);
279 }
280 }
281
282 Constraint::FixUpCommit {
283 commit_to_fixup_oid,
284 fixup_commit_oid,
285 } => {
286 for commits in self.inner.values_mut() {
288 commits.remove(fixup_commit_oid);
289 }
290
291 self.fixups
292 .entry(*commit_to_fixup_oid)
293 .or_default()
294 .insert(*fixup_commit_oid);
295 }
296 }
297 }
298
299 let range_heads: HashSet<&NonZeroOid> = constraints
300 .iter()
301 .filter_map(|c| match c {
302 Constraint::MoveSubtree { .. } => None,
303 Constraint::MoveChildren {
304 parent_of_oid: _,
305 children_of_oid,
306 }
307 | Constraint::FixUpCommit {
308 commit_to_fixup_oid: _,
309 fixup_commit_oid: children_of_oid,
310 } => Some(children_of_oid),
311 })
312 .collect();
313
314 let mut move_children =
315 |parent_of_oid: &NonZeroOid, children_of_oid: &NonZeroOid| -> eyre::Result<()> {
316 let mut parent_oid = self.dag.get_only_parent_oid(*parent_of_oid)?;
317 let commits_to_move = self.commits_to_move();
318
319 while range_heads.contains(&parent_oid) && commits_to_move.contains(&parent_oid) {
322 parent_oid = self.dag.get_only_parent_oid(parent_oid)?;
323 }
324
325 let commits_to_move: CommitSet = commits_to_move.into_iter().collect();
326 let source_children: CommitSet = self
327 .dag
328 .query_children(CommitSet::from(*children_of_oid))?
329 .difference(&commits_to_move);
330 let source_children = self.dag.filter_visible_commits(source_children)?;
331
332 for child_oid in self.dag.commit_set_to_vec(&source_children)? {
333 self.inner.entry(parent_oid).or_default().insert(child_oid);
334 }
335
336 Ok(())
337 };
338
339 for constraint in constraints {
340 match constraint {
341 Constraint::MoveChildren {
342 parent_of_oid,
343 children_of_oid,
344 } => move_children(parent_of_oid, children_of_oid)?,
345
346 Constraint::FixUpCommit {
347 commit_to_fixup_oid: _,
348 fixup_commit_oid,
349 } => move_children(fixup_commit_oid, fixup_commit_oid)?,
350
351 Constraint::MoveSubtree { .. } => {
352 }
354 }
355 }
356
357 Ok(())
358 }
359
360 fn add_descendant_constraints(&mut self, effects: &Effects) -> eyre::Result<()> {
365 let (effects, _progress) = effects.start_operation(OperationType::ConstrainCommits);
366 let _effects = effects;
367
368 let all_descendants_of_constrained_nodes = {
369 let mut acc = Vec::new();
370 let commits_to_fixup = self.commits_to_fixup();
371 let commits_to_move: CommitSet = self
372 .commits_to_move()
373 .union(&commits_to_fixup)
374 .cloned()
375 .collect();
376 let descendants = self.dag.query_descendants(commits_to_move.clone())?;
377 let descendants = descendants.difference(&commits_to_move);
378 let descendants = self.dag.filter_visible_commits(descendants)?;
379 let descendant_oids = self.dag.commit_set_to_vec(&descendants)?;
380 for descendant_oid in descendant_oids {
381 let parents = self.dag.query_parent_names(descendant_oid)?;
382 let parent_oids = parents
383 .into_iter()
384 .map(NonZeroOid::try_from)
385 .try_collect()?;
386 acc.push(Constraint::MoveSubtree {
387 parent_oids,
388 child_oid: descendant_oid,
389 });
390 }
391 acc
392 };
393
394 self.add_constraints(&all_descendants_of_constrained_nodes)?;
395 Ok(())
396 }
397
398 fn check_for_cycles_helper(
399 &self,
400 path: &mut Vec<NonZeroOid>,
401 current_oid: NonZeroOid,
402 ) -> Result<(), BuildRebasePlanError> {
403 if path.contains(¤t_oid) {
404 path.push(current_oid);
405 return Err(BuildRebasePlanError::ConstraintCycle {
406 cycle_oids: path.clone(),
407 });
408 }
409
410 path.push(current_oid);
411 if let Some(child_oids) = self.commits_to_move_to(¤t_oid) {
412 for child_oid in child_oids.iter().sorted() {
413 self.check_for_cycles_helper(path, *child_oid)?;
414 }
415 }
416 path.pop();
417 Ok(())
418 }
419
420 fn check_for_cycles(&self, effects: &Effects) -> Result<(), BuildRebasePlanError> {
421 let (effects, _progress) = effects.start_operation(OperationType::CheckForCycles);
422 let _effects = effects;
423
424 for oid in self.parents().iter().sorted() {
426 self.check_for_cycles_helper(&mut Vec::new(), *oid)?;
427 }
428 Ok(())
429 }
430
431 fn check_permissions(&self) -> eyre::Result<Result<(), BuildRebasePlanError>> {
432 let RebasePlanPermissions {
433 build_options: _,
434 allowed_commits,
435 } = &self.permissions;
436 let commits_to_move: CommitSet = self.commits_to_move().into_iter().collect();
437 let illegal_commits_to_move = commits_to_move.difference(allowed_commits);
438 if !self.dag.set_is_empty(&illegal_commits_to_move)? {
439 Ok(Err(BuildRebasePlanError::MoveIllegalCommits {
440 illegal_commits_to_move,
441 }))
442 } else {
443 Ok(Ok(()))
444 }
445 }
446
447 fn find_roots(&self) -> Vec<Constraint> {
448 let unconstrained_fixup_nodes = &self.commits_to_fixup() - &self.commits_to_move();
449 let unconstrained_nodes = {
450 let unconstrained_nodes: HashSet<NonZeroOid> = self.parents().into_iter().collect();
451 let unconstrained_nodes = &unconstrained_nodes - &self.commits_to_move();
452 &unconstrained_nodes - &unconstrained_fixup_nodes
453 };
454
455 let root_edge_iter = unconstrained_nodes
456 .into_iter()
457 .filter_map(|unconstrained_oid| {
458 self.commits_to_move_to(&unconstrained_oid).map(|children| {
459 children
460 .into_iter()
461 .map(move |child_oid| Constraint::MoveSubtree {
462 parent_oids: vec![unconstrained_oid],
463 child_oid,
464 })
465 })
466 })
467 .flatten();
468
469 let fixup_edge_iter = unconstrained_fixup_nodes
470 .into_iter()
471 .map(|commit_to_fixup_oid| Constraint::FixUpCommit {
472 commit_to_fixup_oid,
473 fixup_commit_oid: commit_to_fixup_oid,
475 });
476
477 let mut root_edges: Vec<Constraint> = root_edge_iter.chain(fixup_edge_iter).collect();
478 root_edges.sort_unstable();
479 root_edges
480 }
481
482 fn get_constraints_sorted_for_debug(&self) -> Vec<(NonZeroOid, Vec<NonZeroOid>)> {
483 self.parents()
484 .iter()
485 .map(|parent_oid| {
486 (
487 *parent_oid,
488 self.commits_to_move_to(parent_oid)
489 .map_or(Vec::new(), |children| {
490 children.into_iter().sorted().collect_vec()
491 }),
492 )
493 })
494 .sorted()
495 .collect_vec()
496 }
497
498 pub fn parents(&self) -> Vec<NonZeroOid> {
500 self.inner.keys().copied().collect_vec()
501 }
502
503 pub fn commits_to_move(&self) -> HashSet<NonZeroOid> {
506 self.inner.values().flatten().copied().collect()
507 }
508
509 pub fn commits_to_move_to(&self, parent_oid: &NonZeroOid) -> Option<Vec<NonZeroOid>> {
511 self.inner
512 .get(parent_oid)
513 .map(|child_oids| child_oids.iter().copied().collect())
514 }
515
516 pub fn commits_to_fixup(&self) -> HashSet<NonZeroOid> {
518 self.fixups.keys().copied().collect()
519 }
520
521 pub fn fixup_commits(&self) -> HashSet<NonZeroOid> {
524 self.fixups.values().flatten().copied().collect()
525 }
526}
527
528#[derive(Clone, Debug)]
530struct BuildState<'repo> {
531 constraints: ConstraintGraph<'repo>,
534
535 used_labels: HashSet<String>,
543
544 parent_labels: HashMap<NonZeroOid, String>,
554}
555
556#[derive(Clone, Debug)]
559pub struct RebasePlanBuilder<'a> {
560 dag: &'a Dag,
561 permissions: RebasePlanPermissions,
562
563 initial_constraints: Vec<Constraint>,
565
566 replacement_commits: HashMap<NonZeroOid, NonZeroOid>,
569
570 pub(crate) touched_paths_cache: Arc<CHashMap<NonZeroOid, HashSet<PathBuf>>>,
574}
575
576#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
577enum Constraint {
578 MoveChildren {
581 parent_of_oid: NonZeroOid,
582 children_of_oid: NonZeroOid,
583 },
584
585 MoveSubtree {
588 parent_oids: Vec<NonZeroOid>,
589 child_oid: NonZeroOid,
590 },
591
592 FixUpCommit {
595 commit_to_fixup_oid: NonZeroOid,
596 fixup_commit_oid: NonZeroOid,
597 },
598}
599
600#[derive(Clone, Debug)]
602pub struct BuildRebasePlanOptions {
603 pub force_rewrite_public_commits: bool,
606
607 pub dump_rebase_constraints: bool,
609
610 pub dump_rebase_plan: bool,
612
613 pub detect_duplicate_commits_via_patch_id: bool,
617}
618
619#[derive(Debug)]
621pub enum BuildRebasePlanError {
622 ConstraintCycle {
624 cycle_oids: Vec<NonZeroOid>,
626 },
627
628 MovePublicCommits {
630 public_commits_to_move: CommitSet,
632 },
633
634 MoveIllegalCommits {
637 illegal_commits_to_move: CommitSet,
639 },
640}
641
642impl BuildRebasePlanError {
643 pub fn describe(&self, effects: &Effects, repo: &Repo, dag: &Dag) -> eyre::Result<()> {
645 match self {
646 BuildRebasePlanError::ConstraintCycle { cycle_oids } => {
647 writeln!(
648 effects.get_output_stream(),
649 "This operation failed because it would introduce a cycle:"
650 )?;
651
652 let glyphs = effects.get_glyphs();
653 let num_cycle_commits = cycle_oids.len();
654 for (i, oid) in cycle_oids.iter().enumerate() {
655 let (char1, char2, char3) = if i == 0 {
656 (
657 glyphs.cycle_upper_left_corner,
658 glyphs.cycle_horizontal_line,
659 glyphs.cycle_arrow,
660 )
661 } else if i + 1 == num_cycle_commits {
662 (
663 glyphs.cycle_lower_left_corner,
664 glyphs.cycle_horizontal_line,
665 glyphs.cycle_horizontal_line,
666 )
667 } else {
668 (glyphs.cycle_vertical_line, " ", " ")
669 };
670 writeln!(
671 effects.get_output_stream(),
672 "{}{}{} {}",
673 char1,
674 char2,
675 char3,
676 glyphs.render(repo.friendly_describe_commit_from_oid(glyphs, *oid)?,)?,
677 )?;
678 }
679 }
680
681 BuildRebasePlanError::MovePublicCommits {
682 public_commits_to_move,
683 } => {
684 let example_bad_commit_oid =
685 dag.set_first(public_commits_to_move)?.ok_or_else(|| {
686 eyre::eyre!("BUG: could not get OID of a public commit to move")
687 })?;
688 let example_bad_commit_oid = NonZeroOid::try_from(example_bad_commit_oid)?;
689 let example_bad_commit = repo.find_commit_or_fail(example_bad_commit_oid)?;
690 writeln!(
691 effects.get_output_stream(),
692 "\
693You are trying to rewrite {}, such as: {}
694It is generally not advised to rewrite public commits, because your
695collaborators will have difficulty merging your changes.
696Retry with -f/--force-rewrite to proceed anyways.",
697 Pluralize {
698 determiner: None,
699 amount: dag.set_count(public_commits_to_move)?,
700 unit: ("public commit", "public commits")
701 },
702 effects
703 .get_glyphs()
704 .render(example_bad_commit.friendly_describe(effects.get_glyphs())?)?,
705 )?;
706 }
707
708 BuildRebasePlanError::MoveIllegalCommits {
709 illegal_commits_to_move,
710 } => {
711 writeln!(
712 effects.get_output_stream(),
713 "\
714BUG: The following commits were planned to be moved but not verified:
715{:?}
716This is a bug. Please report it.",
717 dag.commit_set_to_vec(illegal_commits_to_move)?
718 )?;
719 }
720 }
721 Ok(())
722 }
723}
724
725impl<'a> RebasePlanBuilder<'a> {
726 pub fn new(dag: &'a Dag, permissions: RebasePlanPermissions) -> Self {
728 RebasePlanBuilder {
729 dag,
730 permissions,
731 initial_constraints: Default::default(),
732 replacement_commits: Default::default(),
733 touched_paths_cache: Default::default(),
734 }
735 }
736
737 #[instrument]
738 fn make_label_name_inner(&self, state: &mut BuildState, mut preferred_name: String) -> String {
739 if !state.used_labels.contains(&preferred_name) {
740 state.used_labels.insert(preferred_name.clone());
741 preferred_name
742 } else {
743 preferred_name.push('\'');
744 self.make_label_name(state, preferred_name)
745 }
746 }
747
748 fn make_label_name(&self, state: &mut BuildState, preferred_name: impl Into<String>) -> String {
749 self.make_label_name_inner(state, preferred_name.into())
750 }
751
752 fn make_rebase_plan_for_current_commit(
753 &self,
754 effects: &Effects,
755 repo: &Repo,
756 state: &mut BuildState,
757 current_commit: Commit,
758 upstream_patch_ids: &HashSet<PatchId>,
759 mut acc: Vec<RebaseCommand>,
760 ) -> eyre::Result<Vec<RebaseCommand>> {
761 let patch_already_applied_upstream = {
762 if upstream_patch_ids.is_empty() {
763 false
767 } else {
768 match repo.get_patch_id(effects, ¤t_commit)? {
769 Some(current_patch_id) => upstream_patch_ids.contains(¤t_patch_id),
770 None => false,
771 }
772 }
773 };
774
775 let acc = {
776 if patch_already_applied_upstream {
777 acc.push(RebaseCommand::SkipUpstreamAppliedCommit {
778 commit_oid: current_commit.get_oid(),
779 });
780 } else if current_commit.get_parent_count() > 1 {
781 let commits_to_merge: Option<Vec<OidOrLabel>> = current_commit
787 .get_parent_oids()
788 .into_iter()
789 .map(|parent_oid| -> Option<OidOrLabel> {
790 let does_parent_commit_need_rebase =
791 state.constraints.commits_to_move().contains(&parent_oid);
792 if does_parent_commit_need_rebase {
793 state
802 .parent_labels
803 .get(&parent_oid)
804 .map(|parent_label| OidOrLabel::Label(parent_label.clone()))
805 } else {
806 Some(OidOrLabel::Oid(parent_oid))
810 }
811 })
812 .collect();
813
814 let commits_to_merge = match commits_to_merge {
815 Some(commits_to_merge) => {
816 let commit_already_applied = acc.iter().any(|command| match command {
826 RebaseCommand::Merge {
827 commit_oid,
828 commits_to_merge: _,
829 }
830 | RebaseCommand::Replace {
831 commit_oid,
832 replacement_commit_oid: _,
833 parents: _,
834 } => *commit_oid == current_commit.get_oid(),
835 _ => false,
836 });
837 if !commit_already_applied {
838 Some(commits_to_merge)
839 } else {
840 None
841 }
842 }
843
844 None => {
845 None
848 }
849 };
850
851 let commits_to_merge = match commits_to_merge {
852 None => {
853 return Ok(acc);
855 }
856 Some(commits_to_merge) => {
857 commits_to_merge
859 }
860 };
861
862 let (first_parent, merge_parents) = match commits_to_merge.as_slice() {
863 [] => {
864 unreachable!("Already verified that there's at least one parent commit")
865 }
866 [first, rest @ ..] => (first, rest.to_vec()),
867 };
868 acc.push(RebaseCommand::Reset {
869 target: first_parent.clone(),
870 });
871 acc.push(
872 match self.replacement_commits.get(¤t_commit.get_oid()) {
873 None => RebaseCommand::Merge {
874 commit_oid: current_commit.get_oid(),
875 commits_to_merge: merge_parents.to_vec(),
876 },
877 Some(replacement_commit_oid) => RebaseCommand::Replace {
878 commit_oid: current_commit.get_oid(),
879 replacement_commit_oid: *replacement_commit_oid,
880 parents: commits_to_merge,
881 },
882 },
883 );
884 } else if state
885 .constraints
886 .fixup_commits()
887 .contains(¤t_commit.get_oid())
888 {
889 } else {
891 let original_commit_oid = current_commit.get_oid();
894 match self.replacement_commits.get(&original_commit_oid) {
895 Some(replacement_commit_oid) => {
896 let replacement_commit =
897 repo.find_commit_or_fail(*replacement_commit_oid)?;
898 let new_parents = replacement_commit
899 .get_parent_oids()
900 .into_iter()
901 .map(|parent_oid| match state.parent_labels.get(&parent_oid) {
902 None => OidOrLabel::Oid(parent_oid),
903 Some(parent_label) => OidOrLabel::Label(parent_label.clone()),
904 })
905 .collect_vec();
906 acc.push(RebaseCommand::Replace {
907 commit_oid: original_commit_oid,
908 replacement_commit_oid: *replacement_commit_oid,
909 parents: new_parents,
910 });
911 }
912 None => {
913 let commits_to_apply_oids = match state
914 .constraints
915 .fixups
916 .get(&original_commit_oid)
917 {
918 None => vec![original_commit_oid],
919 Some(fixup_commit_oids) => {
920 let mut commits_to_apply = vec![original_commit_oid];
921 commits_to_apply.extend(fixup_commit_oids);
922
923 let commit_set: CommitSet = commits_to_apply.into_iter().collect();
924 let commits_to_apply =
925 sorted_commit_set(repo, state.constraints.dag, &commit_set)?;
926
927 commits_to_apply
928 .iter()
929 .map(|commit| commit.get_oid())
930 .collect()
931 }
932 };
933 acc.push(RebaseCommand::Pick {
934 original_commit_oid,
935 commits_to_apply_oids,
936 });
937 acc.push(RebaseCommand::DetectEmptyCommit {
938 commit_oid: current_commit.get_oid(),
939 });
940 }
941 };
942 }
943 acc
944 };
945
946 let child_commits: Vec<Commit> = state
947 .constraints
948 .commits_to_move_to(¤t_commit.get_oid())
949 .map_or(Ok(Vec::new()), |mut child_oids| {
950 child_oids.sort_unstable();
951 child_oids
952 .into_iter()
953 .map(|child_oid| repo.find_commit_or_fail(child_oid))
954 .try_collect()
955 })?;
956
957 let acc = {
958 if child_commits.iter().any(|child_commit| {
959 child_commit.get_parent_count() > 1
960 || self
961 .replacement_commits
962 .contains_key(&child_commit.get_oid())
963 }) {
964 let command_num = acc.len();
968 let label_name = self.make_label_name(state, format!("parent-{command_num}"));
969 state
970 .parent_labels
971 .insert(current_commit.get_oid(), label_name.clone());
972 let mut acc = acc;
973 acc.push(RebaseCommand::CreateLabel { label_name });
974 acc
975 } else {
976 acc
977 }
978 };
979
980 if child_commits.is_empty() {
981 Ok(acc)
982 } else if child_commits.len() == 1 {
983 let mut child_commits = child_commits;
984 let only_child_commit = child_commits.pop().unwrap();
985 let acc = self.make_rebase_plan_for_current_commit(
986 effects,
987 repo,
988 state,
989 only_child_commit,
990 upstream_patch_ids,
991 acc,
992 )?;
993 Ok(acc)
994 } else {
995 let command_num = acc.len();
996 let label_name = self.make_label_name(state, format!("label-{command_num}"));
997 let mut acc = acc;
998 acc.push(RebaseCommand::CreateLabel {
999 label_name: label_name.clone(),
1000 });
1001 for child_commit in child_commits {
1002 acc = self.make_rebase_plan_for_current_commit(
1003 effects,
1004 repo,
1005 state,
1006 child_commit,
1007 upstream_patch_ids,
1008 acc,
1009 )?;
1010 acc.push(RebaseCommand::Reset {
1011 target: OidOrLabel::Label(label_name.clone()),
1012 });
1013 }
1014 Ok(acc)
1015 }
1016 }
1017
1018 pub fn move_subtree(
1021 &mut self,
1022 source_oid: NonZeroOid,
1023 dest_oids: Vec<NonZeroOid>,
1024 ) -> eyre::Result<()> {
1025 assert!(!dest_oids.is_empty());
1026 self.initial_constraints.push(Constraint::MoveSubtree {
1027 parent_oids: dest_oids,
1028 child_oid: source_oid,
1029 });
1030 Ok(())
1031 }
1032
1033 pub fn move_commit(
1037 &mut self,
1038 source_oid: NonZeroOid,
1039 dest_oid: NonZeroOid,
1040 ) -> eyre::Result<()> {
1041 self.move_range(source_oid, source_oid, dest_oid)
1042 }
1043
1044 pub fn move_range(
1049 &mut self,
1050 source_oid: NonZeroOid,
1051 end_oid: NonZeroOid,
1052 dest_oid: NonZeroOid,
1053 ) -> eyre::Result<()> {
1054 self.initial_constraints.push(Constraint::MoveSubtree {
1055 parent_oids: vec![dest_oid],
1056 child_oid: source_oid,
1057 });
1058 self.initial_constraints.push(Constraint::MoveChildren {
1059 parent_of_oid: source_oid,
1060 children_of_oid: end_oid,
1061 });
1062 Ok(())
1063 }
1064
1065 pub fn fixup_commit(
1069 &mut self,
1070 source_oid: NonZeroOid,
1071 dest_oid: NonZeroOid,
1072 ) -> eyre::Result<()> {
1073 self.initial_constraints.push(Constraint::FixUpCommit {
1074 commit_to_fixup_oid: dest_oid,
1075 fixup_commit_oid: source_oid,
1076 });
1077 Ok(())
1078 }
1079
1080 pub fn replace_commit(
1083 &mut self,
1084 original_oid: NonZeroOid,
1085 replacement_oid: NonZeroOid,
1086 ) -> eyre::Result<()> {
1087 if self.replacement_commits.contains_key(&original_oid) {
1088 eyre::bail!(
1089 "Attempting to rewrite commit {}. Refusing to replace a commit twice.",
1090 original_oid
1091 );
1092 }
1093 self.replacement_commits
1094 .insert(original_oid, replacement_oid);
1095 Ok(())
1096 }
1097
1098 pub fn build(
1100 &self,
1101 effects: &Effects,
1102 pool: &ThreadPool,
1103 repo_pool: &ResourcePool<RepoResource>,
1104 ) -> eyre::Result<Result<Option<RebasePlan>, BuildRebasePlanError>> {
1105 let mut constraints = ConstraintGraph::new(self.dag, &self.permissions);
1106 constraints.add_constraints(&self.initial_constraints)?;
1107 let mut state = BuildState {
1108 constraints,
1109 used_labels: Default::default(),
1110 parent_labels: Default::default(),
1111 };
1112
1113 let (effects, _progress) = effects.start_operation(OperationType::BuildRebasePlan);
1114
1115 let BuildRebasePlanOptions {
1116 force_rewrite_public_commits: _,
1117 dump_rebase_constraints,
1118 dump_rebase_plan,
1119 detect_duplicate_commits_via_patch_id,
1120 } = &self.permissions.build_options;
1121 if *dump_rebase_constraints {
1122 println!(
1125 "Rebase constraints before adding descendants: {:#?}",
1126 state.constraints.get_constraints_sorted_for_debug(),
1127 );
1128 }
1129 state.constraints.add_descendant_constraints(&effects)?;
1130 if *dump_rebase_constraints {
1131 println!(
1134 "Rebase constraints after adding descendants: {:#?}",
1135 state.constraints.get_constraints_sorted_for_debug(),
1136 );
1137 }
1138
1139 if let Err(err) = state.constraints.check_for_cycles(&effects) {
1140 return Ok(Err(err));
1141 }
1142 if let Err(err) = state.constraints.check_permissions()? {
1143 return Ok(Err(err));
1144 }
1145
1146 let repo = repo_pool.try_create()?;
1147 let roots = state.constraints.find_roots();
1148 let mut acc = Vec::new();
1149 let mut first_dest_oid = None;
1150 for constraint in roots {
1151 let (parent_oids, child_oid) = match constraint {
1154 Constraint::MoveSubtree {
1155 parent_oids,
1156 child_oid,
1157 } => (parent_oids, child_oid),
1158
1159 Constraint::FixUpCommit {
1160 commit_to_fixup_oid,
1161 fixup_commit_oid: _,
1162 } => {
1163 let parents = repo.find_commit_or_fail(commit_to_fixup_oid)?.get_parent_oids();
1164 (parents, commit_to_fixup_oid)
1165 },
1166
1167 Constraint::MoveChildren {
1168 parent_of_oid: _,
1169 children_of_oid: _,
1170 } => eyre::bail!("BUG: Invalid constraint encountered while preparing rebase plan.\nThis should be unreachable."),
1171 };
1172
1173 let first_parent_oid = *parent_oids.first().unwrap();
1174 first_dest_oid.get_or_insert(first_parent_oid);
1175 acc.push(RebaseCommand::Reset {
1176 target: OidOrLabel::Oid(first_parent_oid),
1177 });
1178
1179 let upstream_patch_ids = if *detect_duplicate_commits_via_patch_id {
1180 let (effects, _progress) =
1181 effects.start_operation(OperationType::DetectDuplicateCommits);
1182 self.get_upstream_patch_ids(
1183 &effects,
1184 pool,
1185 repo_pool,
1186 &repo,
1187 &mut state,
1188 child_oid,
1189 &parent_oids,
1190 )?
1191 } else {
1192 Default::default()
1193 };
1194 acc = self.make_rebase_plan_for_current_commit(
1195 &effects,
1196 &repo,
1197 &mut state,
1198 repo.find_commit_or_fail(child_oid)?,
1199 &upstream_patch_ids,
1200 acc,
1201 )?;
1202 }
1203
1204 acc.push(RebaseCommand::RegisterExtraPostRewriteHook);
1205
1206 Self::check_all_commits_included_in_rebase_plan(&state, acc.as_slice());
1207
1208 let rebase_plan = first_dest_oid.map(|first_dest_oid| RebasePlan {
1209 first_dest_oid,
1210 commands: acc,
1211 });
1212 if *dump_rebase_plan {
1213 println!("Rebase plan: {rebase_plan:#?}");
1216 }
1217 Ok(Ok(rebase_plan))
1218 }
1219
1220 fn check_all_commits_included_in_rebase_plan(
1221 state: &BuildState,
1222 rebase_commands: &[RebaseCommand],
1223 ) {
1224 let included_commit_oids: HashSet<NonZeroOid> = rebase_commands
1225 .iter()
1226 .flat_map(|rebase_command| match rebase_command {
1227 RebaseCommand::CreateLabel { label_name: _ }
1228 | RebaseCommand::Reset { target: _ }
1229 | RebaseCommand::Break
1230 | RebaseCommand::RegisterExtraPostRewriteHook
1231 | RebaseCommand::DetectEmptyCommit { commit_oid: _ } => Vec::new(),
1232 RebaseCommand::Pick {
1233 original_commit_oid,
1234 commits_to_apply_oids,
1235 } => {
1236 let mut commit_oids = vec![*original_commit_oid];
1237 commit_oids.extend(commits_to_apply_oids);
1238 commit_oids
1239 }
1240 RebaseCommand::Merge {
1241 commit_oid,
1242 commits_to_merge: _,
1243 }
1244 | RebaseCommand::Replace {
1245 commit_oid,
1246 replacement_commit_oid: _,
1247 parents: _,
1248 }
1249 | RebaseCommand::SkipUpstreamAppliedCommit { commit_oid } => vec![*commit_oid],
1250 })
1251 .collect();
1252 let missing_commit_oids = state
1253 .constraints
1254 .commits_to_move()
1255 .sub(&included_commit_oids);
1256 if !missing_commit_oids.is_empty() {
1257 warn!(
1258 ?missing_commit_oids,
1259 "BUG? Not all commits were included in the rebase plan. \
1260 This means that some commits might be missing \
1261 after the rebase has completed."
1262 );
1263 }
1264 }
1265
1266 #[instrument]
1270 fn get_upstream_patch_ids(
1271 &self,
1272 effects: &Effects,
1273 pool: &ThreadPool,
1274 repo_pool: &RepoPool,
1275 repo: &Repo,
1276 state: &mut BuildState,
1277 current_oid: NonZeroOid,
1278 dest_oids: &[NonZeroOid],
1279 ) -> eyre::Result<HashSet<PatchId>> {
1280 let merge_base_oids: Vec<CommitSet> = dest_oids
1281 .iter()
1282 .map(|dest_oid| {
1283 let commit_set: CommitSet = [current_oid, *dest_oid].into_iter().collect();
1284 self.dag.query_gca_all(commit_set)
1285 })
1286 .try_collect()?;
1287 let merge_base_oids = union_all(&merge_base_oids);
1288
1289 let touched_commit_oids: Vec<NonZeroOid> =
1290 state.constraints.commits_to_move().into_iter().collect();
1291
1292 let path = {
1293 let (effects, _progress) = effects.start_operation(OperationType::WalkCommits);
1294 let _effects = effects;
1295 self.dag
1296 .query_range(merge_base_oids, dest_oids.iter().copied().collect())
1297 .wrap_err("Calculating upstream commits")?
1298 };
1299
1300 let path = {
1301 self.filter_path_to_merge_base_commits(
1302 effects,
1303 pool,
1304 repo_pool,
1305 repo,
1306 path,
1307 touched_commit_oids,
1308 )?
1309 };
1310
1311 let (effects, progress) = effects.start_operation(OperationType::GetUpstreamPatchIds);
1314 progress.notify_progress(0, path.len());
1315 let result: HashSet<PatchId> = {
1316 let path_oids = path
1317 .into_iter()
1318 .map(|commit| commit.get_oid())
1319 .collect_vec();
1320
1321 pool.install(|| {
1322 path_oids
1323 .into_par_iter()
1324 .map(|commit_oid| -> eyre::Result<Option<PatchId>> {
1325 let repo = repo_pool.try_create()?;
1326 let commit = match repo.find_commit(commit_oid)? {
1327 Some(commit) => commit,
1328 None => return Ok(None),
1329 };
1330 let result = repo.get_patch_id(&effects, &commit)?;
1331 Ok(result)
1332 })
1333 .inspect(|_| progress.notify_progress_inc(1))
1334 .filter_map(|result| result.transpose())
1335 .collect::<eyre::Result<HashSet<PatchId>>>()
1336 })?
1337 };
1338 Ok(result)
1339 }
1340
1341 fn filter_path_to_merge_base_commits(
1342 &self,
1343 effects: &Effects,
1344 pool: &ThreadPool,
1345 repo_pool: &RepoPool,
1346 repo: &'a Repo,
1347 path: CommitSet,
1348 touched_commit_oids: Vec<NonZeroOid>,
1349 ) -> eyre::Result<Vec<Commit<'a>>> {
1350 let (effects, _progress) = effects.start_operation(OperationType::FilterCommits);
1351 let path = self.dag.commit_set_to_vec(&path)?;
1352
1353 let touched_commits: Vec<Commit> = touched_commit_oids
1354 .into_iter()
1355 .map(|oid| repo.find_commit(oid))
1356 .flatten_ok()
1357 .try_collect()?;
1358 let local_touched_paths: Vec<HashSet<PathBuf>> = touched_commits
1359 .into_iter()
1360 .map(|commit| repo.get_paths_touched_by_commit(&commit))
1361 .try_collect()?;
1362
1363 let filtered_path = {
1364 enum CacheLookupResult<T, U> {
1365 Cached(T),
1366 NotCached(U),
1367 }
1368 let touched_paths_cache = &self.touched_paths_cache;
1369 let path: Vec<CacheLookupResult<Option<NonZeroOid>, NonZeroOid>> = {
1371 let (effects, progress) = effects.start_operation(OperationType::ReadingFromCache);
1372 let _effects = effects;
1373 if touched_paths_cache.is_empty() {
1374 path.into_iter()
1376 .with_progress(progress)
1377 .map(CacheLookupResult::NotCached)
1378 .collect()
1379 } else {
1380 path.into_iter()
1381 .with_progress(progress)
1382 .map(|commit_oid| match touched_paths_cache.get(&commit_oid) {
1383 Some(upstream_touched_paths) => {
1384 if Self::should_check_patch_id(
1385 &upstream_touched_paths,
1386 &local_touched_paths,
1387 ) {
1388 CacheLookupResult::Cached(Some(commit_oid))
1389 } else {
1390 CacheLookupResult::Cached(None)
1391 }
1392 }
1393 None => CacheLookupResult::NotCached(commit_oid),
1394 })
1395 .collect()
1396 }
1397 };
1398
1399 let (effects, progress) = effects.start_operation(OperationType::FilterByTouchedPaths);
1400 let _effects = effects;
1401 pool.install(|| {
1402 progress.notify_progress(0, path.len());
1403 path.into_par_iter()
1404 .map(|commit_oid| {
1405 let commit_oid = match commit_oid {
1406 CacheLookupResult::Cached(result) => return Ok(result),
1407 CacheLookupResult::NotCached(commit_oid) => commit_oid,
1408 };
1409
1410 let repo = repo_pool.try_create()?;
1411 let commit = match repo.find_commit(commit_oid)? {
1412 Some(commit) => commit,
1413 None => return Ok(None),
1414 };
1415 let upstream_touched_paths = repo.get_paths_touched_by_commit(&commit)?;
1416 let result = if Self::should_check_patch_id(
1417 &upstream_touched_paths,
1418 &local_touched_paths,
1419 ) {
1420 Some(commit_oid)
1421 } else {
1422 None
1423 };
1424 touched_paths_cache.insert(commit_oid, upstream_touched_paths);
1425 Ok(result)
1426 })
1427 .inspect(|_| progress.notify_progress_inc(1))
1428 .filter_map(|x| x.transpose())
1429 .collect::<eyre::Result<Vec<NonZeroOid>>>()
1430 })?
1431 };
1432 let filtered_path = filtered_path
1433 .into_iter()
1434 .map(|commit_oid| repo.find_commit_or_fail(commit_oid))
1435 .try_collect()?;
1436
1437 Ok(filtered_path)
1438 }
1439
1440 fn should_check_patch_id(
1441 upstream_touched_paths: &HashSet<PathBuf>,
1442 local_touched_paths: &[HashSet<PathBuf>],
1443 ) -> bool {
1444 local_touched_paths.iter().contains(upstream_touched_paths)
1450 }
1451}