Skip to main content

branchless/core/rewrite/
plan.rs

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/// Represents the target for certain [`RebaseCommand`]s.
22#[derive(Clone, Debug)]
23pub enum OidOrLabel {
24    /// A commit hash to check out directly.
25    Oid(NonZeroOid),
26
27    /// A label created previously with [`RebaseCommand::CreateLabel`].
28    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/// A command that can be applied for either in-memory or on-disk rebases.
41#[derive(Debug)]
42pub enum RebaseCommand {
43    /// Create a label (a reference stored in `refs/rewritten/`) pointing to the
44    /// current rebase head for later use.
45    CreateLabel {
46        /// The new label name.
47        label_name: String,
48    },
49
50    /// Move the rebase head to the provided label or commit.
51    Reset {
52        /// The target to check out.
53        target: OidOrLabel,
54    },
55
56    /// Apply the provided commits on top of the rebase head, and update the
57    /// rebase head to point to the newly-applied commit.
58    ///
59    /// If multiple commits are provided, they will be squashed into a single
60    /// commit.
61    Pick {
62        /// The original commit, which contains the relevant metadata such as
63        /// the commit message.
64        original_commit_oid: NonZeroOid,
65
66        /// The commits whose patches should be applied to the rebase head.
67        ///  - This will be different from `original_commit_oid` when a commit
68        ///    is being replaced.
69        ///  - If this is a single commit, then the rebase will perform a normal
70        ///    `pick`.
71        ///  - If this is multiple commits, they will all be squashed into a
72        ///    single commit, reusing the metadata (message, author, timestamps,
73        ///    etc) from `original_commit_oid`.
74        commits_to_apply_oids: Vec<NonZeroOid>,
75    },
76
77    /// Merge two or more parent commits.
78    Merge {
79        /// The original merge commit to copy the commit contents from.
80        commit_oid: NonZeroOid,
81
82        /// The other commits to merge into this one. This may be a list of
83        /// either OIDs or strings. This will always be one fewer commit than
84        /// the number of actual parents for this commit, since we always merge
85        /// into the current `HEAD` commit when rebasing.
86        commits_to_merge: Vec<OidOrLabel>,
87    },
88
89    /// When rebasing `commit_oid`, apply it by replacing its commit metadata
90    /// and tree with that of `replacement_commit_oid`, and by replacing its
91    /// parents with `parents`. No patches will be computed or applied, so this
92    /// operation is guaranteed to never cause a merge conflict.
93    Replace {
94        /// The commit to be replaced. It should be part of another rebase command, or else we will
95        /// never encounter it for replacement.
96        commit_oid: NonZeroOid,
97
98        /// The replacement commit whose metadata and tree we'll use.
99        replacement_commit_oid: NonZeroOid,
100
101        /// The new parents for the replaced commit.
102        parents: Vec<OidOrLabel>,
103    },
104
105    /// Pause the rebase, to be resumed later. Only supported in on-disk
106    /// rebases.
107    Break,
108
109    /// On-disk rebases only. Register that we want to run cleanup at the end of
110    /// the rebase, during the `post-rewrite` hook.
111    RegisterExtraPostRewriteHook,
112
113    /// Determine if the current commit is empty. If so, reset the rebase head
114    /// to its parent and record that it was empty in the `rewritten-list`.
115    DetectEmptyCommit {
116        /// The original commit. If the new commit is empty, then the original
117        /// commit will be recorded as skipped.
118        commit_oid: NonZeroOid,
119    },
120
121    /// The commit that would have been applied to the rebase head was already
122    /// applied upstream. Skip it and record it in the `rewritten-list`.
123    SkipUpstreamAppliedCommit {
124        /// The original commit, which will be recorded as skipped.
125        commit_oid: NonZeroOid,
126    },
127}
128
129impl RebaseCommand {
130    /// Convert the command to a string that's used in the `git rebase` plan
131    /// format.
132    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                // FIXME: Untested, this probably doesn't work. Currently,
170                // on-disk rebases for merges with replacement commits are not
171                // generated by any command, so this code path is never
172                // triggered.
173                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/// Represents a sequence of commands that can be executed to carry out a rebase
192/// operation.
193#[derive(Debug)]
194pub struct RebasePlan {
195    /// The first commit OID that will be checked out. This is necessary to
196    /// support on-disk rebases.
197    pub first_dest_oid: NonZeroOid,
198
199    /// The commands to run.
200    pub commands: Vec<RebaseCommand>,
201}
202
203/// A token representing that the rebase plan has been checked for validity.
204#[derive(Clone, Debug)]
205pub struct RebasePlanPermissions {
206    pub(crate) build_options: BuildRebasePlanOptions,
207    pub(crate) allowed_commits: CommitSet,
208}
209
210impl RebasePlanPermissions {
211    /// Construct a new `RebasePlanPermissions`.
212    pub fn verify_rewrite_set(
213        dag: &Dag,
214        build_options: BuildRebasePlanOptions,
215        commits: &CommitSet,
216    ) -> eyre::Result<Result<Self, BuildRebasePlanError>> {
217        // This isn't necessary for correctness, but helps to produce a better
218        // error message which indicates the magnitude of the issue.
219        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/// Represents the commits (as OIDs) that will be used to build a rebase plan.
239#[derive(Clone, Debug)]
240struct ConstraintGraph<'a> {
241    dag: &'a Dag,
242    permissions: &'a RebasePlanPermissions,
243
244    /// This is a mapping from `x` to `y` if `x` must be applied before `y`
245    inner: HashMap<NonZeroOid, HashSet<NonZeroOid>>,
246
247    /// A mapping of commits being fixed up to the commits being absorbed into them.
248    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                    // do nothing; these will be handled in the next pass
266                }
267
268                Constraint::MoveSubtree {
269                    parent_oids,
270                    child_oid,
271                } => {
272                    // remove previous (if any) constraints on commit
273                    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                    // remove previous (if any) constraints on commit
290                    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                // If parent_oid is part of another range and is itself
323                // constrained, keep looking for an unconstrained ancestor.
324                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                    // do nothing; these were handled in the first pass
356                }
357            }
358        }
359
360        Ok(())
361    }
362
363    /// Add additional edges to the constraint graph for each descendant commit
364    /// of a referred-to commit. This adds enough information to the constraint
365    /// graph that it now represents the actual end-state commit graph that we
366    /// want to create, not just a list of constraints.
367    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(&current_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(&current_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        // FIXME: O(n^2) algorithm.
428        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                // HACK but this is unused
477                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    /// All of the parent (aka destination) OIDs
502    pub fn parents(&self) -> Vec<NonZeroOid> {
503        self.inner.keys().copied().collect_vec()
504    }
505
506    /// All of the constrained children. This is set of all commits which need
507    /// to be rebased. Consequently, their OIDs will change.
508    pub fn commits_to_move(&self) -> HashSet<NonZeroOid> {
509        self.inner.values().flatten().copied().collect()
510    }
511
512    /// All of the constrained children being moved to a particular parent..
513    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    /// All of the commits being fixed up.
520    pub fn commits_to_fixup(&self) -> HashSet<NonZeroOid> {
521        self.fixups.keys().copied().collect()
522    }
523
524    /// All of the fixup commits. This is set of all commits which need
525    /// to be absorbed into other commits and removed from the commit graph.
526    pub fn fixup_commits(&self) -> HashSet<NonZeroOid> {
527        self.fixups.values().flatten().copied().collect()
528    }
529}
530
531/// Mutable state modified while building the rebase plan.
532#[derive(Clone, Debug)]
533struct BuildState<'repo> {
534    /// Contains all of `initial_constraints` (in `RebasePlanBuilder`) plus
535    /// any implied constraints added (such as for descendant commits).
536    constraints: ConstraintGraph<'repo>,
537
538    /// When we're rebasing a commit with OID X, its commit hash will change to
539    /// some OID X', which we don't know ahead of time. However, we may still
540    /// need to refer to X' as part of the rest of the rebase (such as when
541    /// rebasing a tree: we have to back up to a parent node and start a new
542    /// branch). Whenever we make a commit that we want to address later, we add
543    /// a "label" pointing to that commit. This field is used to ensure that we
544    /// don't use the same label twice.
545    used_labels: HashSet<String>,
546
547    /// When rebasing merge commits, we build a rebase plan by traversing the
548    /// new commit graph in depth-first order. When we encounter a merge commit,
549    /// we first have to make sure that all of its parents have been committed.
550    /// (If not, then we stop this sub-traversal and wait for a later traversal
551    /// to hit the same merge commit).
552    ///
553    /// When replacing commits and specifying a parent which is also being
554    /// rebased, we need to refer to it via label instead of OID, to ensure that
555    /// we use the rewritten parent.
556    parent_labels: HashMap<NonZeroOid, String>,
557}
558
559/// Builder for a rebase plan. Unlike regular Git rebases, a `git-branchless`
560/// rebase plan can move multiple unrelated subtrees to unrelated destinations.
561#[derive(Clone, Debug)]
562pub struct RebasePlanBuilder<'a> {
563    dag: &'a Dag,
564    permissions: RebasePlanPermissions,
565
566    /// The constraints specified by the caller.
567    initial_constraints: Vec<Constraint>,
568
569    /// Mapping of commits that should be replaced to the commits that they should be replaced
570    /// with.
571    replacement_commits: HashMap<NonZeroOid, NonZeroOid>,
572
573    /// Cache mapping from commit OID to the paths changed in the diff for that
574    /// commit. The value is `None` if the commit doesn't have an associated
575    /// diff (i.e. is a merge commit).
576    pub(crate) touched_paths_cache: Arc<CHashMap<NonZeroOid, HashSet<PathBuf>>>,
577}
578
579#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
580enum Constraint {
581    /// Indicates that the children of `children_of` should be moved on top of
582    /// nearest unmoved ancestor of `parent_of`.
583    MoveChildren {
584        parent_of_oid: NonZeroOid,
585        children_of_oid: NonZeroOid,
586    },
587
588    /// Indicates that `child` and all of its descendants should be moved on top
589    /// of `parent`.
590    MoveSubtree {
591        parent_oids: Vec<NonZeroOid>,
592        child_oid: NonZeroOid,
593    },
594
595    /// Indicates that `fixup_commit` should be squashed into `commit_to_fixup`
596    /// and all of its descendants should be moved on top of its parent.
597    FixUpCommit {
598        commit_to_fixup_oid: NonZeroOid,
599        fixup_commit_oid: NonZeroOid,
600    },
601}
602
603/// Options used to build a rebase plan.
604#[derive(Clone, Debug)]
605pub struct BuildRebasePlanOptions {
606    /// Force rewriting public commits, even though other users may have access
607    /// to those commits.
608    pub force_rewrite_public_commits: bool,
609
610    /// Print the rebase constraints for debugging.
611    pub dump_rebase_constraints: bool,
612
613    /// Print the rebase plan for debugging.
614    pub dump_rebase_plan: bool,
615
616    /// Calculate the patch ID for each upstream commit and compare them to the
617    /// patch IDs in the to-be-rebased commits. Commits which have patch IDs
618    /// which are already upstream are skipped.
619    pub detect_duplicate_commits_via_patch_id: bool,
620}
621
622/// An error caused when attempting to build a rebase plan.
623#[derive(Debug)]
624pub enum BuildRebasePlanError {
625    /// There was a cycle in the requested graph to be built.
626    ConstraintCycle {
627        /// The OIDs of the commits in the cycle. The first and the last OIDs are the same.
628        cycle_oids: Vec<NonZeroOid>,
629    },
630
631    /// The user was trying to move public commits.
632    MovePublicCommits {
633        /// The public commits which the user was trying to move.
634        public_commits_to_move: CommitSet,
635    },
636
637    /// The user was trying to move commits that weren't verified before the
638    /// rebase plan was built. This probably indicates a bug in the code.
639    MoveIllegalCommits {
640        /// The illegal commits which the user was trying to move.
641        illegal_commits_to_move: CommitSet,
642    },
643}
644
645impl BuildRebasePlanError {
646    /// Write the error message to `out`.
647    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    /// Constructor.
730    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                // Save time in the common case that there are no
767                // similar-looking upstream commits, so that we don't have
768                // to calculate the diff for the patch ID.
769                false
770            } else {
771                match repo.get_patch_id(effects, &current_commit)? {
772                    Some(current_patch_id) => upstream_patch_ids.contains(&current_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                // This is a merge commit. We need to make sure that all parent
785                // commits have been applied, and only then proceed with
786                // applying this commit. Note that parent commits may or may not
787                // be part of the set of commits to rebase (i.e. may or may not
788                // be mentioned in the constraints).
789                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                            // Since the parent commit will be rebased, it will
797                            // also have a new OID, so we need to address it by
798                            // the label it set when it was committed rather
799                            // than by OID.
800                            //
801                            // If the label is not yet present, then it means
802                            // that not all parents have been applied yet, so
803                            // we'll postpone this commit.
804                            state
805                                .parent_labels
806                                .get(&parent_oid)
807                                .map(|parent_label| OidOrLabel::Label(parent_label.clone()))
808                        } else {
809                            // This parent commit was not supposed to be
810                            // rebased, so its OID won't change and we can
811                            // address it OID directly.
812                            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                        // We've already checked to make sure that all parent
820                        // commits which are being moved have had their labels
821                        // applied. However, if the commit has multiple parents
822                        // which were *not* moved, then we would visit this
823                        // commit and attempt to reapply it multiple times. The
824                        // simplest fix is to not reapply merge commits which
825                        // have already been applied.
826                        //
827                        // FIXME: O(n^2) algorithm.
828                        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                        // Wait for the caller to come back to this commit later
849                        // and then proceed to any child commits.
850                        None
851                    }
852                };
853
854                let commits_to_merge = match commits_to_merge {
855                    None => {
856                        // Some parents need to be committed. Wait for caller to come back to this commit later and then proceed to any child commits.
857                        return Ok(acc);
858                    }
859                    Some(commits_to_merge) => {
860                        // All parents have been committed.
861                        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(&current_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(&current_commit.get_oid())
891            {
892                // Do nothing for fixup commits.
893            } else {
894                // Normal one-parent commit (or a zero-parent commit?), just
895                // rebase it and continue.
896                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(&current_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                // If this commit has any merge commits or replaced commits as
968                // children, create a label so that the child can reference this
969                // commit later for merging.
970                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    /// Generate a sequence of rebase steps that cause the subtree at `source_oid`
1022    /// to be rebased on top of `dest_oids`.
1023    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    /// Generate a sequence of rebase steps that cause the commit at
1039    /// `source_oid` to be rebased on top of `dest_oid`, and for the descendants
1040    /// of `source_oid` to be rebased on top of its parent.
1041    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    /// Generate a sequence of rebase steps that cause the range from
1050    /// `source_oid` to `end_oid` to be rebased on top of `dest_oid`, and for
1051    /// the descendants of `end_oid` to be rebased on top of the parent of
1052    /// `source_oid`.
1053    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    /// Generate a sequence of rebase steps that cause the commit at
1071    /// `source_oid` to be squashed into `dest_oid`, and for the descendants
1072    /// of `source_oid` to be rebased on top of its parent.
1073    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    /// Instruct the rebase planner to "reparent" the commit at `source_oid`
1086    /// onto `parent_oids`, keeping all contents of `source_oid` exactly the same.
1087    ///
1088    /// Note that in the end the descendant commits of `source_oid` will be
1089    /// automatically rebased on top of `source_oid` following normal patch
1090    /// application logic, hence the whole subtree of `source_oid` will be
1091    /// effectively reparented on top of `parent_oids`.
1092    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        // To keep the contents of all descendant commits the same, forcibly
1103        // replace the child commit, and then rely on normal patch
1104        // application for its descendants.
1105        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    /// Instruct the rebase planner to replace the commit at `original_oid` with the commit at
1131    /// `replacement_oid`.
1132    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    /// Create the rebase plan. Returns `None` if there were no commands in the rebase plan.
1149    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            // For test: don't print to `effects.get_output_stream()`, as it will
1173            // be suppressed.
1174            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            // For test: don't print to `effects.get_output_stream()`, as it will
1182            // be suppressed.
1183            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            // NOTE that these are coming from find_roots() and are not the
1202            // original Constraints used to build the plan
1203            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            // For test: don't print to `effects.get_output_stream()`, as it will
1268            // be suppressed.
1269            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    /// Get the patch IDs for commits between `current_oid` and `dest_oids`,
1321    /// filtered to only the commits which might have the same patch ID as a
1322    /// commit being rebased.
1323    #[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        // FIXME: we may recalculate common patch IDs many times, should be
1366        // cached.
1367        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            // Check cache before distributing work to threads.
1424            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                    // Fast path for when the cache hasn't been populated.
1429                    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        // It's possible that the same commit was applied after a parent
1499        // commit renamed a certain path. In that case, this check won't
1500        // trigger. We'll rely on the empty-commit check after the
1501        // commit has been made to deduplicate the commit in that case.
1502        // FIXME: this code path could be optimized further.
1503        local_touched_paths.iter().contains(upstream_touched_paths)
1504    }
1505}