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