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 chashmap::CHashMap;
8use eyre::Context;
9use itertools::Itertools;
10use rayon::{prelude::*, ThreadPool};
11use tracing::{instrument, warn};
12
13use crate::core::dag::{sorted_commit_set, union_all, CommitSet, Dag};
14use crate::core::effects::{Effects, OperationType, WithProgress};
15use crate::core::formatting::Pluralize;
16use crate::core::rewrite::{RepoPool, RepoResource};
17use crate::core::task::ResourcePool;
18use crate::git::{Commit, NonZeroOid, PatchId, Repo};
19
20/// Represents the target for certain [`RebaseCommand`]s.
21#[derive(Clone, Debug)]
22pub enum OidOrLabel {
23    /// A commit hash to check out directly.
24    Oid(NonZeroOid),
25
26    /// A label created previously with [`RebaseCommand::CreateLabel`].
27    Label(String),
28}
29
30impl Display for OidOrLabel {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        match self {
33            OidOrLabel::Oid(oid) => write!(f, "{oid}"),
34            OidOrLabel::Label(label) => write!(f, "{label}"),
35        }
36    }
37}
38
39/// A command that can be applied for either in-memory or on-disk rebases.
40#[derive(Debug)]
41pub enum RebaseCommand {
42    /// Create a label (a reference stored in `refs/rewritten/`) pointing to the
43    /// current rebase head for later use.
44    CreateLabel {
45        /// The new label name.
46        label_name: String,
47    },
48
49    /// Move the rebase head to the provided label or commit.
50    Reset {
51        /// The target to check out.
52        target: OidOrLabel,
53    },
54
55    /// Apply the provided commits on top of the rebase head, and update the
56    /// rebase head to point to the newly-applied commit.
57    ///
58    /// If multiple commits are provided, they will be squashed into a single
59    /// commit.
60    Pick {
61        /// The original commit, which contains the relevant metadata such as
62        /// the commit message.
63        original_commit_oid: NonZeroOid,
64
65        /// The commits whose patches should be applied to the rebase head.
66        ///  - This will be different from `original_commit_oid` when a commit
67        ///    is being replaced.
68        ///  - If this is a single commit, then the rebase will perform a normal
69        ///    `pick`.
70        ///  - If this is multiple commits, they will all be squashed into a
71        ///    single commit, reusing the metadata (message, author, timestamps,
72        ///    etc) from `original_commit_oid`.
73        commits_to_apply_oids: Vec<NonZeroOid>,
74    },
75
76    /// Merge two or more parent commits.
77    Merge {
78        /// The original merge commit to copy the commit contents from.
79        commit_oid: NonZeroOid,
80
81        /// The other commits to merge into this one. This may be a list of
82        /// either OIDs or strings. This will always be one fewer commit than
83        /// the number of actual parents for this commit, since we always merge
84        /// into the current `HEAD` commit when rebasing.
85        commits_to_merge: Vec<OidOrLabel>,
86    },
87
88    /// When rebasing `commit_oid`, apply it by replacing its commit metadata
89    /// and tree with that of `replacement_commit_oid`, and by replacing its
90    /// parents with `parents`. No patches will be computed or applied, so this
91    /// operation is guaranteed to never cause a merge conflict.
92    Replace {
93        /// The commit to be replaced. It should be part of another rebase command, or else we will
94        /// never encounter it for replacement.
95        commit_oid: NonZeroOid,
96
97        /// The replacement commit whose metadata and tree we'll use.
98        replacement_commit_oid: NonZeroOid,
99
100        /// The new parents for the replaced commit.
101        parents: Vec<OidOrLabel>,
102    },
103
104    /// Pause the rebase, to be resumed later. Only supported in on-disk
105    /// rebases.
106    Break,
107
108    /// On-disk rebases only. Register that we want to run cleanup at the end of
109    /// the rebase, during the `post-rewrite` hook.
110    RegisterExtraPostRewriteHook,
111
112    /// Determine if the current commit is empty. If so, reset the rebase head
113    /// to its parent and record that it was empty in the `rewritten-list`.
114    DetectEmptyCommit {
115        /// The original commit. If the new commit is empty, then the original
116        /// commit will be recorded as skipped.
117        commit_oid: NonZeroOid,
118    },
119
120    /// The commit that would have been applied to the rebase head was already
121    /// applied upstream. Skip it and record it in the `rewritten-list`.
122    SkipUpstreamAppliedCommit {
123        /// The original commit, which will be recorded as skipped.
124        commit_oid: NonZeroOid,
125    },
126}
127
128impl RebaseCommand {
129    /// Convert the command to a string that's used in the `git rebase` plan
130    /// format.
131    pub fn to_rebase_command(&self) -> String {
132        match self {
133            RebaseCommand::CreateLabel { label_name } => format!("label {label_name}"),
134            RebaseCommand::Reset { target } => format!("reset {target}"),
135            RebaseCommand::Pick {
136                original_commit_oid: _,
137                commits_to_apply_oids,
138            } => match commits_to_apply_oids.as_slice() {
139                [] => String::new(),
140                [commit_oid] => format!("pick {commit_oid}"),
141                [..] => unimplemented!("On-disk fixups are not yet supported"),
142            },
143            RebaseCommand::Merge {
144                commit_oid,
145                commits_to_merge,
146            } => format!(
147                "merge -C {} {}",
148                commit_oid,
149                commits_to_merge
150                    .iter()
151                    .map(|commit_id| commit_id.to_string())
152                    .join(" ")
153            ),
154            RebaseCommand::Replace {
155                commit_oid: _,
156                replacement_commit_oid,
157                parents,
158            } => {
159                let parents = parents
160                    .iter()
161                    .map(|parent| match parent {
162                        OidOrLabel::Oid(parent_oid) => format!("-p {parent_oid}"),
163                        OidOrLabel::Label(parent_label) => {
164                            format!("-p refs/rewritten/{parent_label}")
165                        }
166                    })
167                    .join(" ");
168                // FIXME: Untested, this probably doesn't work. Currently,
169                // on-disk rebases for merges with replacement commits are not
170                // generated by any command, so this code path is never
171                // triggered.
172                format!("exec git show -s --format=%B {replacement_commit_oid} | git commit-tree {parents} {replacement_commit_oid}^{{tree}}")
173            }
174            RebaseCommand::Break => "break".to_string(),
175            RebaseCommand::RegisterExtraPostRewriteHook => {
176                "exec git branchless hook-register-extra-post-rewrite-hook".to_string()
177            }
178            RebaseCommand::DetectEmptyCommit { commit_oid } => {
179                format!("exec git branchless hook-detect-empty-commit {commit_oid}")
180            }
181            RebaseCommand::SkipUpstreamAppliedCommit { commit_oid } => {
182                format!("exec git branchless hook-skip-upstream-applied-commit {commit_oid}")
183            }
184        }
185    }
186}
187
188/// Represents a sequence of commands that can be executed to carry out a rebase
189/// operation.
190#[derive(Debug)]
191pub struct RebasePlan {
192    /// The first commit OID that will be checked out. This is necessary to
193    /// support on-disk rebases.
194    pub first_dest_oid: NonZeroOid,
195
196    /// The commands to run.
197    pub commands: Vec<RebaseCommand>,
198}
199
200/// A token representing that the rebase plan has been checked for validity.
201#[derive(Clone, Debug)]
202pub struct RebasePlanPermissions {
203    pub(crate) build_options: BuildRebasePlanOptions,
204    pub(crate) allowed_commits: CommitSet,
205}
206
207impl RebasePlanPermissions {
208    /// Construct a new `RebasePlanPermissions`.
209    pub fn verify_rewrite_set(
210        dag: &Dag,
211        build_options: BuildRebasePlanOptions,
212        commits: &CommitSet,
213    ) -> eyre::Result<Result<Self, BuildRebasePlanError>> {
214        // This isn't necessary for correctness, but helps to produce a better
215        // error message which indicates the magnitude of the issue.
216        let commits = dag.query_descendants(commits.clone())?;
217
218        let public_commits = dag.query_public_commits_slow()?;
219        if !build_options.force_rewrite_public_commits {
220            let public_commits_to_move = public_commits.intersection(&commits);
221            if !dag.set_is_empty(&public_commits_to_move)? {
222                return Ok(Err(BuildRebasePlanError::MovePublicCommits {
223                    public_commits_to_move,
224                }));
225            }
226        }
227
228        Ok(Ok(RebasePlanPermissions {
229            build_options,
230            allowed_commits: commits,
231        }))
232    }
233}
234
235/// Represents the commits (as OIDs) that will be used to build a rebase plan.
236#[derive(Clone, Debug)]
237struct ConstraintGraph<'a> {
238    dag: &'a Dag,
239    permissions: &'a RebasePlanPermissions,
240
241    /// This is a mapping from `x` to `y` if `x` must be applied before `y`
242    inner: HashMap<NonZeroOid, HashSet<NonZeroOid>>,
243
244    /// A mapping of commits being fixed up to the commits being absorbed into them.
245    fixups: HashMap<NonZeroOid, HashSet<NonZeroOid>>,
246}
247
248impl<'a> ConstraintGraph<'a> {
249    pub fn new(dag: &'a Dag, permissions: &'a RebasePlanPermissions) -> Self {
250        Self {
251            dag,
252            permissions,
253            inner: HashMap::new(),
254            fixups: HashMap::new(),
255        }
256    }
257
258    pub fn add_constraints(&mut self, constraints: &Vec<Constraint>) -> eyre::Result<()> {
259        for constraint in constraints {
260            match constraint {
261                Constraint::MoveChildren { .. } => {
262                    // do nothing; these will be handled in the next pass
263                }
264
265                Constraint::MoveSubtree {
266                    parent_oids,
267                    child_oid,
268                } => {
269                    // remove previous (if any) constraints on commit
270                    for commits in self.inner.values_mut() {
271                        commits.remove(child_oid);
272                    }
273
274                    for parent_oid in parent_oids {
275                        self.inner
276                            .entry(*parent_oid)
277                            .or_default()
278                            .insert(*child_oid);
279                    }
280                }
281
282                Constraint::FixUpCommit {
283                    commit_to_fixup_oid,
284                    fixup_commit_oid,
285                } => {
286                    // remove previous (if any) constraints on commit
287                    for commits in self.inner.values_mut() {
288                        commits.remove(fixup_commit_oid);
289                    }
290
291                    self.fixups
292                        .entry(*commit_to_fixup_oid)
293                        .or_default()
294                        .insert(*fixup_commit_oid);
295                }
296            }
297        }
298
299        let range_heads: HashSet<&NonZeroOid> = constraints
300            .iter()
301            .filter_map(|c| match c {
302                Constraint::MoveSubtree { .. } => None,
303                Constraint::MoveChildren {
304                    parent_of_oid: _,
305                    children_of_oid,
306                }
307                | Constraint::FixUpCommit {
308                    commit_to_fixup_oid: _,
309                    fixup_commit_oid: children_of_oid,
310                } => Some(children_of_oid),
311            })
312            .collect();
313
314        let mut move_children =
315            |parent_of_oid: &NonZeroOid, children_of_oid: &NonZeroOid| -> eyre::Result<()> {
316                let mut parent_oid = self.dag.get_only_parent_oid(*parent_of_oid)?;
317                let commits_to_move = self.commits_to_move();
318
319                // If parent_oid is part of another range and is itself
320                // constrained, keep looking for an unconstrained ancestor.
321                while range_heads.contains(&parent_oid) && commits_to_move.contains(&parent_oid) {
322                    parent_oid = self.dag.get_only_parent_oid(parent_oid)?;
323                }
324
325                let commits_to_move: CommitSet = commits_to_move.into_iter().collect();
326                let source_children: CommitSet = self
327                    .dag
328                    .query_children(CommitSet::from(*children_of_oid))?
329                    .difference(&commits_to_move);
330                let source_children = self.dag.filter_visible_commits(source_children)?;
331
332                for child_oid in self.dag.commit_set_to_vec(&source_children)? {
333                    self.inner.entry(parent_oid).or_default().insert(child_oid);
334                }
335
336                Ok(())
337            };
338
339        for constraint in constraints {
340            match constraint {
341                Constraint::MoveChildren {
342                    parent_of_oid,
343                    children_of_oid,
344                } => move_children(parent_of_oid, children_of_oid)?,
345
346                Constraint::FixUpCommit {
347                    commit_to_fixup_oid: _,
348                    fixup_commit_oid,
349                } => move_children(fixup_commit_oid, fixup_commit_oid)?,
350
351                Constraint::MoveSubtree { .. } => {
352                    // do nothing; these were handled in the first pass
353                }
354            }
355        }
356
357        Ok(())
358    }
359
360    /// Add additional edges to the constraint graph for each descendant commit
361    /// of a referred-to commit. This adds enough information to the constraint
362    /// graph that it now represents the actual end-state commit graph that we
363    /// want to create, not just a list of constraints.
364    fn add_descendant_constraints(&mut self, effects: &Effects) -> eyre::Result<()> {
365        let (effects, _progress) = effects.start_operation(OperationType::ConstrainCommits);
366        let _effects = effects;
367
368        let all_descendants_of_constrained_nodes = {
369            let mut acc = Vec::new();
370            let commits_to_fixup = self.commits_to_fixup();
371            let commits_to_move: CommitSet = self
372                .commits_to_move()
373                .union(&commits_to_fixup)
374                .cloned()
375                .collect();
376            let descendants = self.dag.query_descendants(commits_to_move.clone())?;
377            let descendants = descendants.difference(&commits_to_move);
378            let descendants = self.dag.filter_visible_commits(descendants)?;
379            let descendant_oids = self.dag.commit_set_to_vec(&descendants)?;
380            for descendant_oid in descendant_oids {
381                let parents = self.dag.query_parent_names(descendant_oid)?;
382                let parent_oids = parents
383                    .into_iter()
384                    .map(NonZeroOid::try_from)
385                    .try_collect()?;
386                acc.push(Constraint::MoveSubtree {
387                    parent_oids,
388                    child_oid: descendant_oid,
389                });
390            }
391            acc
392        };
393
394        self.add_constraints(&all_descendants_of_constrained_nodes)?;
395        Ok(())
396    }
397
398    fn check_for_cycles_helper(
399        &self,
400        path: &mut Vec<NonZeroOid>,
401        current_oid: NonZeroOid,
402    ) -> Result<(), BuildRebasePlanError> {
403        if path.contains(&current_oid) {
404            path.push(current_oid);
405            return Err(BuildRebasePlanError::ConstraintCycle {
406                cycle_oids: path.clone(),
407            });
408        }
409
410        path.push(current_oid);
411        if let Some(child_oids) = self.commits_to_move_to(&current_oid) {
412            for child_oid in child_oids.iter().sorted() {
413                self.check_for_cycles_helper(path, *child_oid)?;
414            }
415        }
416        path.pop();
417        Ok(())
418    }
419
420    fn check_for_cycles(&self, effects: &Effects) -> Result<(), BuildRebasePlanError> {
421        let (effects, _progress) = effects.start_operation(OperationType::CheckForCycles);
422        let _effects = effects;
423
424        // FIXME: O(n^2) algorithm.
425        for oid in self.parents().iter().sorted() {
426            self.check_for_cycles_helper(&mut Vec::new(), *oid)?;
427        }
428        Ok(())
429    }
430
431    fn check_permissions(&self) -> eyre::Result<Result<(), BuildRebasePlanError>> {
432        let RebasePlanPermissions {
433            build_options: _,
434            allowed_commits,
435        } = &self.permissions;
436        let commits_to_move: CommitSet = self.commits_to_move().into_iter().collect();
437        let illegal_commits_to_move = commits_to_move.difference(allowed_commits);
438        if !self.dag.set_is_empty(&illegal_commits_to_move)? {
439            Ok(Err(BuildRebasePlanError::MoveIllegalCommits {
440                illegal_commits_to_move,
441            }))
442        } else {
443            Ok(Ok(()))
444        }
445    }
446
447    fn find_roots(&self) -> Vec<Constraint> {
448        let unconstrained_fixup_nodes = &self.commits_to_fixup() - &self.commits_to_move();
449        let unconstrained_nodes = {
450            let unconstrained_nodes: HashSet<NonZeroOid> = self.parents().into_iter().collect();
451            let unconstrained_nodes = &unconstrained_nodes - &self.commits_to_move();
452            &unconstrained_nodes - &unconstrained_fixup_nodes
453        };
454
455        let root_edge_iter = unconstrained_nodes
456            .into_iter()
457            .filter_map(|unconstrained_oid| {
458                self.commits_to_move_to(&unconstrained_oid).map(|children| {
459                    children
460                        .into_iter()
461                        .map(move |child_oid| Constraint::MoveSubtree {
462                            parent_oids: vec![unconstrained_oid],
463                            child_oid,
464                        })
465                })
466            })
467            .flatten();
468
469        let fixup_edge_iter = unconstrained_fixup_nodes
470            .into_iter()
471            .map(|commit_to_fixup_oid| Constraint::FixUpCommit {
472                commit_to_fixup_oid,
473                // HACK but this is unused
474                fixup_commit_oid: commit_to_fixup_oid,
475            });
476
477        let mut root_edges: Vec<Constraint> = root_edge_iter.chain(fixup_edge_iter).collect();
478        root_edges.sort_unstable();
479        root_edges
480    }
481
482    fn get_constraints_sorted_for_debug(&self) -> Vec<(NonZeroOid, Vec<NonZeroOid>)> {
483        self.parents()
484            .iter()
485            .map(|parent_oid| {
486                (
487                    *parent_oid,
488                    self.commits_to_move_to(parent_oid)
489                        .map_or(Vec::new(), |children| {
490                            children.into_iter().sorted().collect_vec()
491                        }),
492                )
493            })
494            .sorted()
495            .collect_vec()
496    }
497
498    /// All of the parent (aka destination) OIDs
499    pub fn parents(&self) -> Vec<NonZeroOid> {
500        self.inner.keys().copied().collect_vec()
501    }
502
503    /// All of the constrained children. This is set of all commits which need
504    /// to be rebased. Consequently, their OIDs will change.
505    pub fn commits_to_move(&self) -> HashSet<NonZeroOid> {
506        self.inner.values().flatten().copied().collect()
507    }
508
509    /// All of the constrained children being moved to a particular parent..
510    pub fn commits_to_move_to(&self, parent_oid: &NonZeroOid) -> Option<Vec<NonZeroOid>> {
511        self.inner
512            .get(parent_oid)
513            .map(|child_oids| child_oids.iter().copied().collect())
514    }
515
516    /// All of the commits being fixed up.
517    pub fn commits_to_fixup(&self) -> HashSet<NonZeroOid> {
518        self.fixups.keys().copied().collect()
519    }
520
521    /// All of the fixup commits. This is set of all commits which need
522    /// to be absorbed into other commits and removed from the commit graph.
523    pub fn fixup_commits(&self) -> HashSet<NonZeroOid> {
524        self.fixups.values().flatten().copied().collect()
525    }
526}
527
528/// Mutable state modified while building the rebase plan.
529#[derive(Clone, Debug)]
530struct BuildState<'repo> {
531    /// Contains all of `initial_constraints` (in `RebasePlanBuilder`) plus
532    /// any implied constraints added (such as for descendant commits).
533    constraints: ConstraintGraph<'repo>,
534
535    /// When we're rebasing a commit with OID X, its commit hash will change to
536    /// some OID X', which we don't know ahead of time. However, we may still
537    /// need to refer to X' as part of the rest of the rebase (such as when
538    /// rebasing a tree: we have to back up to a parent node and start a new
539    /// branch). Whenever we make a commit that we want to address later, we add
540    /// a "label" pointing to that commit. This field is used to ensure that we
541    /// don't use the same label twice.
542    used_labels: HashSet<String>,
543
544    /// When rebasing merge commits, we build a rebase plan by traversing the
545    /// new commit graph in depth-first order. When we encounter a merge commit,
546    /// we first have to make sure that all of its parents have been committed.
547    /// (If not, then we stop this sub-traversal and wait for a later traversal
548    /// to hit the same merge commit).
549    ///
550    /// When replacing commits and specifying a parent which is also being
551    /// rebased, we need to refer to it via label instead of OID, to ensure that
552    /// we use the rewritten parent.
553    parent_labels: HashMap<NonZeroOid, String>,
554}
555
556/// Builder for a rebase plan. Unlike regular Git rebases, a `git-branchless`
557/// rebase plan can move multiple unrelated subtrees to unrelated destinations.
558#[derive(Clone, Debug)]
559pub struct RebasePlanBuilder<'a> {
560    dag: &'a Dag,
561    permissions: RebasePlanPermissions,
562
563    /// The constraints specified by the caller.
564    initial_constraints: Vec<Constraint>,
565
566    /// Mapping of commits that should be replaced to the commits that they should be replaced
567    /// with.
568    replacement_commits: HashMap<NonZeroOid, NonZeroOid>,
569
570    /// Cache mapping from commit OID to the paths changed in the diff for that
571    /// commit. The value is `None` if the commit doesn't have an associated
572    /// diff (i.e. is a merge commit).
573    pub(crate) touched_paths_cache: Arc<CHashMap<NonZeroOid, HashSet<PathBuf>>>,
574}
575
576#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
577enum Constraint {
578    /// Indicates that the children of `children_of` should be moved on top of
579    /// nearest unmoved ancestor of `parent_of`.
580    MoveChildren {
581        parent_of_oid: NonZeroOid,
582        children_of_oid: NonZeroOid,
583    },
584
585    /// Indicates that `child` and all of its descendants should be moved on top
586    /// of `parent`.
587    MoveSubtree {
588        parent_oids: Vec<NonZeroOid>,
589        child_oid: NonZeroOid,
590    },
591
592    /// Indicates that `fixup_commit` should be squashed into `commit_to_fixup`
593    /// and all of its descendants should be moved on top of its parent.
594    FixUpCommit {
595        commit_to_fixup_oid: NonZeroOid,
596        fixup_commit_oid: NonZeroOid,
597    },
598}
599
600/// Options used to build a rebase plan.
601#[derive(Clone, Debug)]
602pub struct BuildRebasePlanOptions {
603    /// Force rewriting public commits, even though other users may have access
604    /// to those commits.
605    pub force_rewrite_public_commits: bool,
606
607    /// Print the rebase constraints for debugging.
608    pub dump_rebase_constraints: bool,
609
610    /// Print the rebase plan for debugging.
611    pub dump_rebase_plan: bool,
612
613    /// Calculate the patch ID for each upstream commit and compare them to the
614    /// patch IDs in the to-be-rebased commits. Commits which have patch IDs
615    /// which are already upstream are skipped.
616    pub detect_duplicate_commits_via_patch_id: bool,
617}
618
619/// An error caused when attempting to build a rebase plan.
620#[derive(Debug)]
621pub enum BuildRebasePlanError {
622    /// There was a cycle in the requested graph to be built.
623    ConstraintCycle {
624        /// The OIDs of the commits in the cycle. The first and the last OIDs are the same.
625        cycle_oids: Vec<NonZeroOid>,
626    },
627
628    /// The user was trying to move public commits.
629    MovePublicCommits {
630        /// The public commits which the user was trying to move.
631        public_commits_to_move: CommitSet,
632    },
633
634    /// The user was trying to move commits that weren't verified before the
635    /// rebase plan was built. This probably indicates a bug in the code.
636    MoveIllegalCommits {
637        /// The illegal commits which the user was trying to move.
638        illegal_commits_to_move: CommitSet,
639    },
640}
641
642impl BuildRebasePlanError {
643    /// Write the error message to `out`.
644    pub fn describe(&self, effects: &Effects, repo: &Repo, dag: &Dag) -> eyre::Result<()> {
645        match self {
646            BuildRebasePlanError::ConstraintCycle { cycle_oids } => {
647                writeln!(
648                    effects.get_output_stream(),
649                    "This operation failed because it would introduce a cycle:"
650                )?;
651
652                let glyphs = effects.get_glyphs();
653                let num_cycle_commits = cycle_oids.len();
654                for (i, oid) in cycle_oids.iter().enumerate() {
655                    let (char1, char2, char3) = if i == 0 {
656                        (
657                            glyphs.cycle_upper_left_corner,
658                            glyphs.cycle_horizontal_line,
659                            glyphs.cycle_arrow,
660                        )
661                    } else if i + 1 == num_cycle_commits {
662                        (
663                            glyphs.cycle_lower_left_corner,
664                            glyphs.cycle_horizontal_line,
665                            glyphs.cycle_horizontal_line,
666                        )
667                    } else {
668                        (glyphs.cycle_vertical_line, " ", " ")
669                    };
670                    writeln!(
671                        effects.get_output_stream(),
672                        "{}{}{} {}",
673                        char1,
674                        char2,
675                        char3,
676                        glyphs.render(repo.friendly_describe_commit_from_oid(glyphs, *oid)?,)?,
677                    )?;
678                }
679            }
680
681            BuildRebasePlanError::MovePublicCommits {
682                public_commits_to_move,
683            } => {
684                let example_bad_commit_oid =
685                    dag.set_first(public_commits_to_move)?.ok_or_else(|| {
686                        eyre::eyre!("BUG: could not get OID of a public commit to move")
687                    })?;
688                let example_bad_commit_oid = NonZeroOid::try_from(example_bad_commit_oid)?;
689                let example_bad_commit = repo.find_commit_or_fail(example_bad_commit_oid)?;
690                writeln!(
691                    effects.get_output_stream(),
692                    "\
693You are trying to rewrite {}, such as: {}
694It is generally not advised to rewrite public commits, because your
695collaborators will have difficulty merging your changes.
696Retry with -f/--force-rewrite to proceed anyways.",
697                    Pluralize {
698                        determiner: None,
699                        amount: dag.set_count(public_commits_to_move)?,
700                        unit: ("public commit", "public commits")
701                    },
702                    effects
703                        .get_glyphs()
704                        .render(example_bad_commit.friendly_describe(effects.get_glyphs())?)?,
705                )?;
706            }
707
708            BuildRebasePlanError::MoveIllegalCommits {
709                illegal_commits_to_move,
710            } => {
711                writeln!(
712                    effects.get_output_stream(),
713                    "\
714BUG: The following commits were planned to be moved but not verified:
715{:?}
716This is a bug. Please report it.",
717                    dag.commit_set_to_vec(illegal_commits_to_move)?
718                )?;
719            }
720        }
721        Ok(())
722    }
723}
724
725impl<'a> RebasePlanBuilder<'a> {
726    /// Constructor.
727    pub fn new(dag: &'a Dag, permissions: RebasePlanPermissions) -> Self {
728        RebasePlanBuilder {
729            dag,
730            permissions,
731            initial_constraints: Default::default(),
732            replacement_commits: Default::default(),
733            touched_paths_cache: Default::default(),
734        }
735    }
736
737    #[instrument]
738    fn make_label_name_inner(&self, state: &mut BuildState, mut preferred_name: String) -> String {
739        if !state.used_labels.contains(&preferred_name) {
740            state.used_labels.insert(preferred_name.clone());
741            preferred_name
742        } else {
743            preferred_name.push('\'');
744            self.make_label_name(state, preferred_name)
745        }
746    }
747
748    fn make_label_name(&self, state: &mut BuildState, preferred_name: impl Into<String>) -> String {
749        self.make_label_name_inner(state, preferred_name.into())
750    }
751
752    fn make_rebase_plan_for_current_commit(
753        &self,
754        effects: &Effects,
755        repo: &Repo,
756        state: &mut BuildState,
757        current_commit: Commit,
758        upstream_patch_ids: &HashSet<PatchId>,
759        mut acc: Vec<RebaseCommand>,
760    ) -> eyre::Result<Vec<RebaseCommand>> {
761        let patch_already_applied_upstream = {
762            if upstream_patch_ids.is_empty() {
763                // Save time in the common case that there are no
764                // similar-looking upstream commits, so that we don't have
765                // to calculate the diff for the patch ID.
766                false
767            } else {
768                match repo.get_patch_id(effects, &current_commit)? {
769                    Some(current_patch_id) => upstream_patch_ids.contains(&current_patch_id),
770                    None => false,
771                }
772            }
773        };
774
775        let acc = {
776            if patch_already_applied_upstream {
777                acc.push(RebaseCommand::SkipUpstreamAppliedCommit {
778                    commit_oid: current_commit.get_oid(),
779                });
780            } else if current_commit.get_parent_count() > 1 {
781                // This is a merge commit. We need to make sure that all parent
782                // commits have been applied, and only then proceed with
783                // applying this commit. Note that parent commits may or may not
784                // be part of the set of commits to rebase (i.e. may or may not
785                // be mentioned in the constraints).
786                let commits_to_merge: Option<Vec<OidOrLabel>> = current_commit
787                    .get_parent_oids()
788                    .into_iter()
789                    .map(|parent_oid| -> Option<OidOrLabel> {
790                        let does_parent_commit_need_rebase =
791                            state.constraints.commits_to_move().contains(&parent_oid);
792                        if does_parent_commit_need_rebase {
793                            // Since the parent commit will be rebased, it will
794                            // also have a new OID, so we need to address it by
795                            // the label it set when it was committed rather
796                            // than by OID.
797                            //
798                            // If the label is not yet present, then it means
799                            // that not all parents have been applied yet, so
800                            // we'll postpone this commit.
801                            state
802                                .parent_labels
803                                .get(&parent_oid)
804                                .map(|parent_label| OidOrLabel::Label(parent_label.clone()))
805                        } else {
806                            // This parent commit was not supposed to be
807                            // rebased, so its OID won't change and we can
808                            // address it OID directly.
809                            Some(OidOrLabel::Oid(parent_oid))
810                        }
811                    })
812                    .collect();
813
814                let commits_to_merge = match commits_to_merge {
815                    Some(commits_to_merge) => {
816                        // We've already checked to make sure that all parent
817                        // commits which are being moved have had their labels
818                        // applied. However, if the commit has multiple parents
819                        // which were *not* moved, then we would visit this
820                        // commit and attempt to reapply it multiple times. The
821                        // simplest fix is to not reapply merge commits which
822                        // have already been applied.
823                        //
824                        // FIXME: O(n^2) algorithm.
825                        let commit_already_applied = acc.iter().any(|command| match command {
826                            RebaseCommand::Merge {
827                                commit_oid,
828                                commits_to_merge: _,
829                            }
830                            | RebaseCommand::Replace {
831                                commit_oid,
832                                replacement_commit_oid: _,
833                                parents: _,
834                            } => *commit_oid == current_commit.get_oid(),
835                            _ => false,
836                        });
837                        if !commit_already_applied {
838                            Some(commits_to_merge)
839                        } else {
840                            None
841                        }
842                    }
843
844                    None => {
845                        // Wait for the caller to come back to this commit later
846                        // and then proceed to any child commits.
847                        None
848                    }
849                };
850
851                let commits_to_merge = match commits_to_merge {
852                    None => {
853                        // Some parents need to be committed. Wait for caller to come back to this commit later and then proceed to any child commits.
854                        return Ok(acc);
855                    }
856                    Some(commits_to_merge) => {
857                        // All parents have been committed.
858                        commits_to_merge
859                    }
860                };
861
862                let (first_parent, merge_parents) = match commits_to_merge.as_slice() {
863                    [] => {
864                        unreachable!("Already verified that there's at least one parent commit")
865                    }
866                    [first, rest @ ..] => (first, rest.to_vec()),
867                };
868                acc.push(RebaseCommand::Reset {
869                    target: first_parent.clone(),
870                });
871                acc.push(
872                    match self.replacement_commits.get(&current_commit.get_oid()) {
873                        None => RebaseCommand::Merge {
874                            commit_oid: current_commit.get_oid(),
875                            commits_to_merge: merge_parents.to_vec(),
876                        },
877                        Some(replacement_commit_oid) => RebaseCommand::Replace {
878                            commit_oid: current_commit.get_oid(),
879                            replacement_commit_oid: *replacement_commit_oid,
880                            parents: commits_to_merge,
881                        },
882                    },
883                );
884            } else if state
885                .constraints
886                .fixup_commits()
887                .contains(&current_commit.get_oid())
888            {
889                // Do nothing for fixup commits.
890            } else {
891                // Normal one-parent commit (or a zero-parent commit?), just
892                // rebase it and continue.
893                let original_commit_oid = current_commit.get_oid();
894                match self.replacement_commits.get(&original_commit_oid) {
895                    Some(replacement_commit_oid) => {
896                        let replacement_commit =
897                            repo.find_commit_or_fail(*replacement_commit_oid)?;
898                        let new_parents = replacement_commit
899                            .get_parent_oids()
900                            .into_iter()
901                            .map(|parent_oid| match state.parent_labels.get(&parent_oid) {
902                                None => OidOrLabel::Oid(parent_oid),
903                                Some(parent_label) => OidOrLabel::Label(parent_label.clone()),
904                            })
905                            .collect_vec();
906                        acc.push(RebaseCommand::Replace {
907                            commit_oid: original_commit_oid,
908                            replacement_commit_oid: *replacement_commit_oid,
909                            parents: new_parents,
910                        });
911                    }
912                    None => {
913                        let commits_to_apply_oids = match state
914                            .constraints
915                            .fixups
916                            .get(&original_commit_oid)
917                        {
918                            None => vec![original_commit_oid],
919                            Some(fixup_commit_oids) => {
920                                let mut commits_to_apply = vec![original_commit_oid];
921                                commits_to_apply.extend(fixup_commit_oids);
922
923                                let commit_set: CommitSet = commits_to_apply.into_iter().collect();
924                                let commits_to_apply =
925                                    sorted_commit_set(repo, state.constraints.dag, &commit_set)?;
926
927                                commits_to_apply
928                                    .iter()
929                                    .map(|commit| commit.get_oid())
930                                    .collect()
931                            }
932                        };
933                        acc.push(RebaseCommand::Pick {
934                            original_commit_oid,
935                            commits_to_apply_oids,
936                        });
937                        acc.push(RebaseCommand::DetectEmptyCommit {
938                            commit_oid: current_commit.get_oid(),
939                        });
940                    }
941                };
942            }
943            acc
944        };
945
946        let child_commits: Vec<Commit> = state
947            .constraints
948            .commits_to_move_to(&current_commit.get_oid())
949            .map_or(Ok(Vec::new()), |mut child_oids| {
950                child_oids.sort_unstable();
951                child_oids
952                    .into_iter()
953                    .map(|child_oid| repo.find_commit_or_fail(child_oid))
954                    .try_collect()
955            })?;
956
957        let acc = {
958            if child_commits.iter().any(|child_commit| {
959                child_commit.get_parent_count() > 1
960                    || self
961                        .replacement_commits
962                        .contains_key(&child_commit.get_oid())
963            }) {
964                // If this commit has any merge commits or replaced commits as
965                // children, create a label so that the child can reference this
966                // commit later for merging.
967                let command_num = acc.len();
968                let label_name = self.make_label_name(state, format!("parent-{command_num}"));
969                state
970                    .parent_labels
971                    .insert(current_commit.get_oid(), label_name.clone());
972                let mut acc = acc;
973                acc.push(RebaseCommand::CreateLabel { label_name });
974                acc
975            } else {
976                acc
977            }
978        };
979
980        if child_commits.is_empty() {
981            Ok(acc)
982        } else if child_commits.len() == 1 {
983            let mut child_commits = child_commits;
984            let only_child_commit = child_commits.pop().unwrap();
985            let acc = self.make_rebase_plan_for_current_commit(
986                effects,
987                repo,
988                state,
989                only_child_commit,
990                upstream_patch_ids,
991                acc,
992            )?;
993            Ok(acc)
994        } else {
995            let command_num = acc.len();
996            let label_name = self.make_label_name(state, format!("label-{command_num}"));
997            let mut acc = acc;
998            acc.push(RebaseCommand::CreateLabel {
999                label_name: label_name.clone(),
1000            });
1001            for child_commit in child_commits {
1002                acc = self.make_rebase_plan_for_current_commit(
1003                    effects,
1004                    repo,
1005                    state,
1006                    child_commit,
1007                    upstream_patch_ids,
1008                    acc,
1009                )?;
1010                acc.push(RebaseCommand::Reset {
1011                    target: OidOrLabel::Label(label_name.clone()),
1012                });
1013            }
1014            Ok(acc)
1015        }
1016    }
1017
1018    /// Generate a sequence of rebase steps that cause the subtree at `source_oid`
1019    /// to be rebased on top of `dest_oids`.
1020    pub fn move_subtree(
1021        &mut self,
1022        source_oid: NonZeroOid,
1023        dest_oids: Vec<NonZeroOid>,
1024    ) -> eyre::Result<()> {
1025        assert!(!dest_oids.is_empty());
1026        self.initial_constraints.push(Constraint::MoveSubtree {
1027            parent_oids: dest_oids,
1028            child_oid: source_oid,
1029        });
1030        Ok(())
1031    }
1032
1033    /// Generate a sequence of rebase steps that cause the commit at
1034    /// `source_oid` to be rebased on top of `dest_oid`, and for the descendants
1035    /// of `source_oid` to be rebased on top of its parent.
1036    pub fn move_commit(
1037        &mut self,
1038        source_oid: NonZeroOid,
1039        dest_oid: NonZeroOid,
1040    ) -> eyre::Result<()> {
1041        self.move_range(source_oid, source_oid, dest_oid)
1042    }
1043
1044    /// Generate a sequence of rebase steps that cause the range from
1045    /// `source_oid` to `end_oid` to be rebased on top of `dest_oid`, and for
1046    /// the descendants of `end_oid` to be rebased on top of the parent of
1047    /// `source_oid`.
1048    pub fn move_range(
1049        &mut self,
1050        source_oid: NonZeroOid,
1051        end_oid: NonZeroOid,
1052        dest_oid: NonZeroOid,
1053    ) -> eyre::Result<()> {
1054        self.initial_constraints.push(Constraint::MoveSubtree {
1055            parent_oids: vec![dest_oid],
1056            child_oid: source_oid,
1057        });
1058        self.initial_constraints.push(Constraint::MoveChildren {
1059            parent_of_oid: source_oid,
1060            children_of_oid: end_oid,
1061        });
1062        Ok(())
1063    }
1064
1065    /// Generate a sequence of rebase steps that cause the commit at
1066    /// `source_oid` to be squashed into `dest_oid`, and for the descendants
1067    /// of `source_oid` to be rebased on top of its parent.
1068    pub fn fixup_commit(
1069        &mut self,
1070        source_oid: NonZeroOid,
1071        dest_oid: NonZeroOid,
1072    ) -> eyre::Result<()> {
1073        self.initial_constraints.push(Constraint::FixUpCommit {
1074            commit_to_fixup_oid: dest_oid,
1075            fixup_commit_oid: source_oid,
1076        });
1077        Ok(())
1078    }
1079
1080    /// Instruct the rebase planner to replace the commit at `original_oid` with the commit at
1081    /// `replacement_oid`.
1082    pub fn replace_commit(
1083        &mut self,
1084        original_oid: NonZeroOid,
1085        replacement_oid: NonZeroOid,
1086    ) -> eyre::Result<()> {
1087        if self.replacement_commits.contains_key(&original_oid) {
1088            eyre::bail!(
1089                "Attempting to rewrite commit {}. Refusing to replace a commit twice.",
1090                original_oid
1091            );
1092        }
1093        self.replacement_commits
1094            .insert(original_oid, replacement_oid);
1095        Ok(())
1096    }
1097
1098    /// Create the rebase plan. Returns `None` if there were no commands in the rebase plan.
1099    pub fn build(
1100        &self,
1101        effects: &Effects,
1102        pool: &ThreadPool,
1103        repo_pool: &ResourcePool<RepoResource>,
1104    ) -> eyre::Result<Result<Option<RebasePlan>, BuildRebasePlanError>> {
1105        let mut constraints = ConstraintGraph::new(self.dag, &self.permissions);
1106        constraints.add_constraints(&self.initial_constraints)?;
1107        let mut state = BuildState {
1108            constraints,
1109            used_labels: Default::default(),
1110            parent_labels: Default::default(),
1111        };
1112
1113        let (effects, _progress) = effects.start_operation(OperationType::BuildRebasePlan);
1114
1115        let BuildRebasePlanOptions {
1116            force_rewrite_public_commits: _,
1117            dump_rebase_constraints,
1118            dump_rebase_plan,
1119            detect_duplicate_commits_via_patch_id,
1120        } = &self.permissions.build_options;
1121        if *dump_rebase_constraints {
1122            // For test: don't print to `effects.get_output_stream()`, as it will
1123            // be suppressed.
1124            println!(
1125                "Rebase constraints before adding descendants: {:#?}",
1126                state.constraints.get_constraints_sorted_for_debug(),
1127            );
1128        }
1129        state.constraints.add_descendant_constraints(&effects)?;
1130        if *dump_rebase_constraints {
1131            // For test: don't print to `effects.get_output_stream()`, as it will
1132            // be suppressed.
1133            println!(
1134                "Rebase constraints after adding descendants: {:#?}",
1135                state.constraints.get_constraints_sorted_for_debug(),
1136            );
1137        }
1138
1139        if let Err(err) = state.constraints.check_for_cycles(&effects) {
1140            return Ok(Err(err));
1141        }
1142        if let Err(err) = state.constraints.check_permissions()? {
1143            return Ok(Err(err));
1144        }
1145
1146        let repo = repo_pool.try_create()?;
1147        let roots = state.constraints.find_roots();
1148        let mut acc = Vec::new();
1149        let mut first_dest_oid = None;
1150        for constraint in roots {
1151            // NOTE that these are coming from find_roots() and are not the
1152            // original Constraints used to build the plan
1153            let (parent_oids, child_oid) = match constraint {
1154                Constraint::MoveSubtree {
1155                    parent_oids,
1156                    child_oid,
1157                } => (parent_oids, child_oid),
1158
1159                Constraint::FixUpCommit {
1160                    commit_to_fixup_oid,
1161                    fixup_commit_oid: _,
1162                } => {
1163                    let parents = repo.find_commit_or_fail(commit_to_fixup_oid)?.get_parent_oids();
1164                    (parents, commit_to_fixup_oid)
1165                },
1166
1167                Constraint::MoveChildren {
1168                    parent_of_oid: _,
1169                    children_of_oid: _,
1170                } => eyre::bail!("BUG: Invalid constraint encountered while preparing rebase plan.\nThis should be unreachable."),
1171            };
1172
1173            let first_parent_oid = *parent_oids.first().unwrap();
1174            first_dest_oid.get_or_insert(first_parent_oid);
1175            acc.push(RebaseCommand::Reset {
1176                target: OidOrLabel::Oid(first_parent_oid),
1177            });
1178
1179            let upstream_patch_ids = if *detect_duplicate_commits_via_patch_id {
1180                let (effects, _progress) =
1181                    effects.start_operation(OperationType::DetectDuplicateCommits);
1182                self.get_upstream_patch_ids(
1183                    &effects,
1184                    pool,
1185                    repo_pool,
1186                    &repo,
1187                    &mut state,
1188                    child_oid,
1189                    &parent_oids,
1190                )?
1191            } else {
1192                Default::default()
1193            };
1194            acc = self.make_rebase_plan_for_current_commit(
1195                &effects,
1196                &repo,
1197                &mut state,
1198                repo.find_commit_or_fail(child_oid)?,
1199                &upstream_patch_ids,
1200                acc,
1201            )?;
1202        }
1203
1204        acc.push(RebaseCommand::RegisterExtraPostRewriteHook);
1205
1206        Self::check_all_commits_included_in_rebase_plan(&state, acc.as_slice());
1207
1208        let rebase_plan = first_dest_oid.map(|first_dest_oid| RebasePlan {
1209            first_dest_oid,
1210            commands: acc,
1211        });
1212        if *dump_rebase_plan {
1213            // For test: don't print to `effects.get_output_stream()`, as it will
1214            // be suppressed.
1215            println!("Rebase plan: {rebase_plan:#?}");
1216        }
1217        Ok(Ok(rebase_plan))
1218    }
1219
1220    fn check_all_commits_included_in_rebase_plan(
1221        state: &BuildState,
1222        rebase_commands: &[RebaseCommand],
1223    ) {
1224        let included_commit_oids: HashSet<NonZeroOid> = rebase_commands
1225            .iter()
1226            .flat_map(|rebase_command| match rebase_command {
1227                RebaseCommand::CreateLabel { label_name: _ }
1228                | RebaseCommand::Reset { target: _ }
1229                | RebaseCommand::Break
1230                | RebaseCommand::RegisterExtraPostRewriteHook
1231                | RebaseCommand::DetectEmptyCommit { commit_oid: _ } => Vec::new(),
1232                RebaseCommand::Pick {
1233                    original_commit_oid,
1234                    commits_to_apply_oids,
1235                } => {
1236                    let mut commit_oids = vec![*original_commit_oid];
1237                    commit_oids.extend(commits_to_apply_oids);
1238                    commit_oids
1239                }
1240                RebaseCommand::Merge {
1241                    commit_oid,
1242                    commits_to_merge: _,
1243                }
1244                | RebaseCommand::Replace {
1245                    commit_oid,
1246                    replacement_commit_oid: _,
1247                    parents: _,
1248                }
1249                | RebaseCommand::SkipUpstreamAppliedCommit { commit_oid } => vec![*commit_oid],
1250            })
1251            .collect();
1252        let missing_commit_oids = state
1253            .constraints
1254            .commits_to_move()
1255            .sub(&included_commit_oids);
1256        if !missing_commit_oids.is_empty() {
1257            warn!(
1258                ?missing_commit_oids,
1259                "BUG? Not all commits were included in the rebase plan. \
1260                This means that some commits might be missing \
1261                after the rebase has completed."
1262            );
1263        }
1264    }
1265
1266    /// Get the patch IDs for commits between `current_oid` and `dest_oids`,
1267    /// filtered to only the commits which might have the same patch ID as a
1268    /// commit being rebased.
1269    #[instrument]
1270    fn get_upstream_patch_ids(
1271        &self,
1272        effects: &Effects,
1273        pool: &ThreadPool,
1274        repo_pool: &RepoPool,
1275        repo: &Repo,
1276        state: &mut BuildState,
1277        current_oid: NonZeroOid,
1278        dest_oids: &[NonZeroOid],
1279    ) -> eyre::Result<HashSet<PatchId>> {
1280        let merge_base_oids: Vec<CommitSet> = dest_oids
1281            .iter()
1282            .map(|dest_oid| {
1283                let commit_set: CommitSet = [current_oid, *dest_oid].into_iter().collect();
1284                self.dag.query_gca_all(commit_set)
1285            })
1286            .try_collect()?;
1287        let merge_base_oids = union_all(&merge_base_oids);
1288
1289        let touched_commit_oids: Vec<NonZeroOid> =
1290            state.constraints.commits_to_move().into_iter().collect();
1291
1292        let path = {
1293            let (effects, _progress) = effects.start_operation(OperationType::WalkCommits);
1294            let _effects = effects;
1295            self.dag
1296                .query_range(merge_base_oids, dest_oids.iter().copied().collect())
1297                .wrap_err("Calculating upstream commits")?
1298        };
1299
1300        let path = {
1301            self.filter_path_to_merge_base_commits(
1302                effects,
1303                pool,
1304                repo_pool,
1305                repo,
1306                path,
1307                touched_commit_oids,
1308            )?
1309        };
1310
1311        // FIXME: we may recalculate common patch IDs many times, should be
1312        // cached.
1313        let (effects, progress) = effects.start_operation(OperationType::GetUpstreamPatchIds);
1314        progress.notify_progress(0, path.len());
1315        let result: HashSet<PatchId> = {
1316            let path_oids = path
1317                .into_iter()
1318                .map(|commit| commit.get_oid())
1319                .collect_vec();
1320
1321            pool.install(|| {
1322                path_oids
1323                    .into_par_iter()
1324                    .map(|commit_oid| -> eyre::Result<Option<PatchId>> {
1325                        let repo = repo_pool.try_create()?;
1326                        let commit = match repo.find_commit(commit_oid)? {
1327                            Some(commit) => commit,
1328                            None => return Ok(None),
1329                        };
1330                        let result = repo.get_patch_id(&effects, &commit)?;
1331                        Ok(result)
1332                    })
1333                    .inspect(|_| progress.notify_progress_inc(1))
1334                    .filter_map(|result| result.transpose())
1335                    .collect::<eyre::Result<HashSet<PatchId>>>()
1336            })?
1337        };
1338        Ok(result)
1339    }
1340
1341    fn filter_path_to_merge_base_commits(
1342        &self,
1343        effects: &Effects,
1344        pool: &ThreadPool,
1345        repo_pool: &RepoPool,
1346        repo: &'a Repo,
1347        path: CommitSet,
1348        touched_commit_oids: Vec<NonZeroOid>,
1349    ) -> eyre::Result<Vec<Commit<'a>>> {
1350        let (effects, _progress) = effects.start_operation(OperationType::FilterCommits);
1351        let path = self.dag.commit_set_to_vec(&path)?;
1352
1353        let touched_commits: Vec<Commit> = touched_commit_oids
1354            .into_iter()
1355            .map(|oid| repo.find_commit(oid))
1356            .flatten_ok()
1357            .try_collect()?;
1358        let local_touched_paths: Vec<HashSet<PathBuf>> = touched_commits
1359            .into_iter()
1360            .map(|commit| repo.get_paths_touched_by_commit(&commit))
1361            .try_collect()?;
1362
1363        let filtered_path = {
1364            enum CacheLookupResult<T, U> {
1365                Cached(T),
1366                NotCached(U),
1367            }
1368            let touched_paths_cache = &self.touched_paths_cache;
1369            // Check cache before distributing work to threads.
1370            let path: Vec<CacheLookupResult<Option<NonZeroOid>, NonZeroOid>> = {
1371                let (effects, progress) = effects.start_operation(OperationType::ReadingFromCache);
1372                let _effects = effects;
1373                if touched_paths_cache.is_empty() {
1374                    // Fast path for when the cache hasn't been populated.
1375                    path.into_iter()
1376                        .with_progress(progress)
1377                        .map(CacheLookupResult::NotCached)
1378                        .collect()
1379                } else {
1380                    path.into_iter()
1381                        .with_progress(progress)
1382                        .map(|commit_oid| match touched_paths_cache.get(&commit_oid) {
1383                            Some(upstream_touched_paths) => {
1384                                if Self::should_check_patch_id(
1385                                    &upstream_touched_paths,
1386                                    &local_touched_paths,
1387                                ) {
1388                                    CacheLookupResult::Cached(Some(commit_oid))
1389                                } else {
1390                                    CacheLookupResult::Cached(None)
1391                                }
1392                            }
1393                            None => CacheLookupResult::NotCached(commit_oid),
1394                        })
1395                        .collect()
1396                }
1397            };
1398
1399            let (effects, progress) = effects.start_operation(OperationType::FilterByTouchedPaths);
1400            let _effects = effects;
1401            pool.install(|| {
1402                progress.notify_progress(0, path.len());
1403                path.into_par_iter()
1404                    .map(|commit_oid| {
1405                        let commit_oid = match commit_oid {
1406                            CacheLookupResult::Cached(result) => return Ok(result),
1407                            CacheLookupResult::NotCached(commit_oid) => commit_oid,
1408                        };
1409
1410                        let repo = repo_pool.try_create()?;
1411                        let commit = match repo.find_commit(commit_oid)? {
1412                            Some(commit) => commit,
1413                            None => return Ok(None),
1414                        };
1415                        let upstream_touched_paths = repo.get_paths_touched_by_commit(&commit)?;
1416                        let result = if Self::should_check_patch_id(
1417                            &upstream_touched_paths,
1418                            &local_touched_paths,
1419                        ) {
1420                            Some(commit_oid)
1421                        } else {
1422                            None
1423                        };
1424                        touched_paths_cache.insert(commit_oid, upstream_touched_paths);
1425                        Ok(result)
1426                    })
1427                    .inspect(|_| progress.notify_progress_inc(1))
1428                    .filter_map(|x| x.transpose())
1429                    .collect::<eyre::Result<Vec<NonZeroOid>>>()
1430            })?
1431        };
1432        let filtered_path = filtered_path
1433            .into_iter()
1434            .map(|commit_oid| repo.find_commit_or_fail(commit_oid))
1435            .try_collect()?;
1436
1437        Ok(filtered_path)
1438    }
1439
1440    fn should_check_patch_id(
1441        upstream_touched_paths: &HashSet<PathBuf>,
1442        local_touched_paths: &[HashSet<PathBuf>],
1443    ) -> bool {
1444        // It's possible that the same commit was applied after a parent
1445        // commit renamed a certain path. In that case, this check won't
1446        // trigger. We'll rely on the empty-commit check after the
1447        // commit has been made to deduplicate the commit in that case.
1448        // FIXME: this code path could be optimized further.
1449        local_touched_paths.iter().contains(upstream_touched_paths)
1450    }
1451}