#![allow(missing_docs)]
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use futures::StreamExt;
use itertools::Itertools;
use pollster::FutureExt;
use tracing::instrument;
use crate::backend::{BackendError, BackendResult, CommitId, MergedTreeId};
use crate::commit::Commit;
use crate::dag_walk;
use crate::index::Index;
use crate::matchers::{Matcher, Visit};
use crate::merged_tree::{MergedTree, MergedTreeBuilder};
use crate::object_id::ObjectId;
use crate::op_store::RefTarget;
use crate::repo::{MutableRepo, Repo};
use crate::repo_path::RepoPath;
use crate::revset::{RevsetExpression, RevsetIteratorExt};
use crate::settings::UserSettings;
use crate::store::Store;
use crate::tree::TreeMergeError;
#[instrument(skip(repo))]
pub fn merge_commit_trees(
repo: &dyn Repo,
commits: &[Commit],
) -> Result<MergedTree, TreeMergeError> {
merge_commit_trees_without_repo(repo.store(), repo.index(), commits)
}
#[instrument(skip(index))]
pub fn merge_commit_trees_without_repo(
store: &Arc<Store>,
index: &dyn Index,
commits: &[Commit],
) -> Result<MergedTree, TreeMergeError> {
if commits.is_empty() {
Ok(store.get_root_tree(&store.empty_merged_tree_id())?)
} else {
let mut new_tree = commits[0].tree()?;
let commit_ids = commits
.iter()
.map(|commit| commit.id().clone())
.collect_vec();
for (i, other_commit) in commits.iter().enumerate().skip(1) {
let ancestor_ids = index.common_ancestors(&commit_ids[0..i], &[commit_ids[i].clone()]);
let ancestors: Vec<_> = ancestor_ids
.iter()
.map(|id| store.get_commit(id))
.try_collect()?;
let ancestor_tree = merge_commit_trees_without_repo(store, index, &ancestors)?;
let other_tree = other_commit.tree()?;
new_tree = new_tree.merge(&ancestor_tree, &other_tree)?;
}
Ok(new_tree)
}
}
pub fn restore_tree(
source: &MergedTree,
destination: &MergedTree,
matcher: &dyn Matcher,
) -> BackendResult<MergedTreeId> {
if matcher.visit(RepoPath::root()) == Visit::AllRecursively {
Ok(source.id())
} else {
let mut tree_builder = MergedTreeBuilder::new(destination.id().clone());
async {
let mut diff_stream = source.diff_stream(destination, matcher);
while let Some((repo_path, diff)) = diff_stream.next().await {
let (source_value, _destination_value) = diff?;
tree_builder.set_or_remove(repo_path, source_value);
}
Ok::<(), BackendError>(())
}
.block_on()?;
tree_builder.write_tree(destination.store())
}
}
pub fn rebase_commit(
settings: &UserSettings,
mut_repo: &mut MutableRepo,
old_commit: &Commit,
new_parents: &[Commit],
) -> Result<Commit, TreeMergeError> {
rebase_commit_with_options(
settings,
mut_repo,
old_commit,
new_parents,
&Default::default(),
)
}
pub fn rebase_commit_with_options(
settings: &UserSettings,
mut_repo: &mut MutableRepo,
old_commit: &Commit,
new_parents: &[Commit],
options: &RebaseOptions,
) -> Result<Commit, TreeMergeError> {
let old_parents = old_commit.parents();
let old_parent_trees = old_parents
.iter()
.map(|parent| parent.store_commit().root_tree.clone())
.collect_vec();
let new_parent_trees = new_parents
.iter()
.map(|parent| parent.store_commit().root_tree.clone())
.collect_vec();
let (old_base_tree_id, new_tree_id) = if new_parent_trees == old_parent_trees {
(
None,
old_commit.tree_id().clone(),
)
} else {
let old_base_tree = merge_commit_trees(mut_repo, &old_parents)?;
let new_base_tree = merge_commit_trees(mut_repo, new_parents)?;
let old_tree = old_commit.tree()?;
(
Some(old_base_tree.id()),
new_base_tree.merge(&old_base_tree, &old_tree)?.id(),
)
};
if let [parent] = new_parents {
let should_abandon = match options.empty {
EmptyBehaviour::Keep => false,
EmptyBehaviour::AbandonNewlyEmpty => {
*parent.tree_id() == new_tree_id
&& old_base_tree_id.map_or(false, |id| id != *old_commit.tree_id())
}
EmptyBehaviour::AbandonAllEmpty => *parent.tree_id() == new_tree_id,
};
if should_abandon && !mut_repo.view().is_wc_commit_id(old_commit.id()) {
mut_repo.record_rewritten_commit(old_commit.id().clone(), parent.id().clone());
return Ok(parent.clone());
}
}
let new_parent_ids = new_parents
.iter()
.map(|commit| commit.id().clone())
.collect();
Ok(mut_repo
.rewrite_commit(settings, old_commit)
.set_parents(new_parent_ids)
.set_tree_id(new_tree_id)
.write()?)
}
pub fn rebase_to_dest_parent(
repo: &dyn Repo,
source: &Commit,
destination: &Commit,
) -> Result<MergedTree, TreeMergeError> {
if source.parent_ids() == destination.parent_ids() {
Ok(source.tree()?)
} else {
let destination_parent_tree = merge_commit_trees(repo, &destination.parents())?;
let source_parent_tree = merge_commit_trees(repo, &source.parents())?;
let source_tree = source.tree()?;
let rebased_tree = destination_parent_tree.merge(&source_parent_tree, &source_tree)?;
Ok(rebased_tree)
}
}
pub fn back_out_commit(
settings: &UserSettings,
mut_repo: &mut MutableRepo,
old_commit: &Commit,
new_parents: &[Commit],
) -> Result<Commit, TreeMergeError> {
let old_base_tree = merge_commit_trees(mut_repo, &old_commit.parents())?;
let new_base_tree = merge_commit_trees(mut_repo, new_parents)?;
let old_tree = old_commit.tree()?;
let new_tree = new_base_tree.merge(&old_tree, &old_base_tree)?;
let new_parent_ids = new_parents
.iter()
.map(|commit| commit.id().clone())
.collect();
Ok(mut_repo
.new_commit(settings, new_parent_ids, new_tree.id())
.set_description(format!("backout of commit {}", &old_commit.id().hex()))
.write()?)
}
#[derive(Clone, Default, PartialEq, Eq, Debug)]
pub enum EmptyBehaviour {
#[default]
Keep,
AbandonNewlyEmpty,
AbandonAllEmpty,
}
#[derive(Clone, Default, PartialEq, Eq, Debug)]
pub struct RebaseOptions {
pub empty: EmptyBehaviour,
}
pub(crate) struct DescendantRebaser<'settings, 'repo> {
settings: &'settings UserSettings,
mut_repo: &'repo mut MutableRepo,
parent_mapping: HashMap<CommitId, Vec<CommitId>>,
divergent: HashMap<CommitId, Vec<CommitId>>,
to_visit: Vec<Commit>,
abandoned: HashSet<CommitId>,
new_commits: HashSet<CommitId>,
rebased: HashMap<CommitId, CommitId>,
branches: HashMap<CommitId, HashSet<String>>,
heads_to_add: HashSet<CommitId>,
heads_to_remove: Vec<CommitId>,
options: RebaseOptions,
}
impl<'settings, 'repo> DescendantRebaser<'settings, 'repo> {
pub fn new(
settings: &'settings UserSettings,
mut_repo: &'repo mut MutableRepo,
rewritten: HashMap<CommitId, HashSet<CommitId>>,
abandoned: HashSet<CommitId>,
) -> DescendantRebaser<'settings, 'repo> {
let store = mut_repo.store();
let root_commit_id = store.root_commit_id();
assert!(!abandoned.contains(root_commit_id));
assert!(!rewritten.contains_key(root_commit_id));
let old_commits_expression = RevsetExpression::commits(rewritten.keys().cloned().collect())
.union(&RevsetExpression::commits(
abandoned.iter().cloned().collect(),
));
let heads_to_add_expression = old_commits_expression
.parents()
.minus(&old_commits_expression);
let heads_to_add = heads_to_add_expression
.evaluate_programmatic(mut_repo)
.unwrap()
.iter()
.collect();
let to_visit_expression = old_commits_expression.descendants();
let to_visit_revset = to_visit_expression.evaluate_programmatic(mut_repo).unwrap();
let to_visit: Vec<_> = to_visit_revset.iter().commits(store).try_collect().unwrap();
drop(to_visit_revset);
let to_visit_set: HashSet<CommitId> =
to_visit.iter().map(|commit| commit.id().clone()).collect();
let mut visited = HashSet::new();
let to_visit = dag_walk::topo_order_reverse(
to_visit,
|commit| commit.id().clone(),
|commit| {
visited.insert(commit.id().clone());
let mut dependents = vec![];
for parent in commit.parents() {
if let Some(targets) = rewritten.get(parent.id()) {
for target in targets {
if to_visit_set.contains(target) && !visited.contains(target) {
dependents.push(store.get_commit(target).unwrap());
}
}
}
if to_visit_set.contains(parent.id()) {
dependents.push(parent);
}
}
dependents
},
);
let new_commits = rewritten.values().flatten().cloned().collect();
let mut parent_mapping = HashMap::new();
let mut divergent = HashMap::new();
for (old_commit, new_commits) in rewritten {
if new_commits.len() == 1 {
parent_mapping.insert(old_commit, vec![new_commits.iter().next().unwrap().clone()]);
} else {
let new_commits = mut_repo.index().heads(&mut new_commits.iter());
divergent.insert(old_commit, new_commits);
}
}
let mut branches: HashMap<_, HashSet<_>> = HashMap::new();
for (branch_name, target) in mut_repo.view().local_branches() {
for commit in target.added_ids() {
branches
.entry(commit.clone())
.or_default()
.insert(branch_name.to_owned());
}
}
DescendantRebaser {
settings,
mut_repo,
parent_mapping,
divergent,
to_visit,
abandoned,
new_commits,
rebased: Default::default(),
branches,
heads_to_add,
heads_to_remove: Default::default(),
options: Default::default(),
}
}
pub fn mut_options(&mut self) -> &mut RebaseOptions {
&mut self.options
}
pub fn into_map(self) -> HashMap<CommitId, CommitId> {
self.rebased
}
fn new_parents(&self, old_ids: &[CommitId]) -> Vec<CommitId> {
fn single_substitution_round(
parent_mapping: &HashMap<CommitId, Vec<CommitId>>,
ids: Vec<CommitId>,
) -> (Vec<CommitId>, bool) {
let mut made_replacements = false;
let mut new_ids = vec![];
for id in ids.into_iter() {
match parent_mapping.get(&id) {
None => new_ids.push(id),
Some(replacements) => {
assert!(
!replacements.is_empty(),
"Found empty value for key {id:?} in the parent mapping",
);
made_replacements = true;
new_ids.extend(replacements.iter().cloned())
}
};
}
(new_ids, made_replacements)
}
let mut new_ids: Vec<CommitId> = old_ids.to_vec();
let mut allowed_iterations = 0..self.parent_mapping.len();
loop {
let made_replacements;
(new_ids, made_replacements) = single_substitution_round(&self.parent_mapping, new_ids);
if !made_replacements {
break;
}
allowed_iterations
.next()
.expect("cycle detected in the parent mapping");
}
match new_ids.as_slice() {
[_singleton] => new_ids,
[a, b] if a != b => new_ids,
_ => new_ids.into_iter().unique().collect(),
}
}
fn ref_target_update(old_id: CommitId, new_ids: Vec<CommitId>) -> (RefTarget, RefTarget) {
let old_ids = std::iter::repeat(old_id).take(new_ids.len());
(
RefTarget::from_legacy_form([], old_ids),
RefTarget::from_legacy_form([], new_ids),
)
}
fn update_references(
&mut self,
old_commit_id: CommitId,
new_commit_ids: Vec<CommitId>,
abandoned_old_commit: bool,
) -> Result<(), BackendError> {
self.update_wc_commits(&old_commit_id, &new_commit_ids[0], abandoned_old_commit)?;
if let Some(branch_names) = self.branches.get(&old_commit_id).cloned() {
let mut branch_updates = vec![];
for branch_name in &branch_names {
for new_commit_id in &new_commit_ids {
self.branches
.entry(new_commit_id.clone())
.or_default()
.insert(branch_name.clone());
}
let local_target = self.mut_repo.get_local_branch(branch_name);
for old_add in local_target.added_ids() {
if *old_add == old_commit_id {
branch_updates.push(branch_name.clone());
}
}
}
let (old_target, new_target) =
DescendantRebaser::ref_target_update(old_commit_id.clone(), new_commit_ids);
for branch_name in &branch_updates {
self.mut_repo
.merge_local_branch(branch_name, &old_target, &new_target);
}
}
self.heads_to_add.remove(&old_commit_id);
if !self.new_commits.contains(&old_commit_id) || self.rebased.contains_key(&old_commit_id) {
self.heads_to_remove.push(old_commit_id);
}
Ok(())
}
fn update_wc_commits(
&mut self,
old_commit_id: &CommitId,
new_commit_id: &CommitId,
abandoned_old_commit: bool,
) -> Result<(), BackendError> {
let workspaces_to_update = self
.mut_repo
.view()
.workspaces_for_wc_commit_id(old_commit_id);
if workspaces_to_update.is_empty() {
return Ok(());
}
let new_commit = self.mut_repo.store().get_commit(new_commit_id)?;
let new_wc_commit = if !abandoned_old_commit {
new_commit
} else {
self.mut_repo
.new_commit(
self.settings,
vec![new_commit.id().clone()],
new_commit.tree_id().clone(),
)
.write()?
};
for workspace_id in workspaces_to_update.into_iter() {
self.mut_repo.edit(workspace_id, &new_wc_commit).unwrap();
}
Ok(())
}
fn rebase_one(&mut self, old_commit: Commit) -> Result<(), TreeMergeError> {
let old_commit_id = old_commit.id().clone();
if let Some(new_parent_ids) = self.parent_mapping.get(&old_commit_id).cloned() {
self.update_references(old_commit_id, new_parent_ids, false)?;
return Ok(());
}
if let Some(divergent_ids) = self.divergent.get(&old_commit_id).cloned() {
self.update_references(old_commit_id, divergent_ids, false)?;
return Ok(());
}
let old_parent_ids = old_commit.parent_ids();
let new_parent_ids = self.new_parents(old_parent_ids);
if self.abandoned.contains(&old_commit_id) {
self.parent_mapping
.insert(old_commit_id.clone(), new_parent_ids.clone());
self.update_references(old_commit_id, new_parent_ids, true)?;
return Ok(());
} else if new_parent_ids == old_parent_ids {
return Ok(());
}
let head_set: HashSet<_> = self
.mut_repo
.index()
.heads(&mut new_parent_ids.iter())
.into_iter()
.collect();
let new_parents: Vec<_> = new_parent_ids
.iter()
.filter(|new_parent| head_set.contains(new_parent))
.map(|new_parent_id| self.mut_repo.store().get_commit(new_parent_id))
.try_collect()?;
let new_commit = rebase_commit_with_options(
self.settings,
self.mut_repo,
&old_commit,
&new_parents,
&self.options,
)?;
let previous_rebased_value = self
.rebased
.insert(old_commit_id.clone(), new_commit.id().clone());
let previous_mapping_value = self
.parent_mapping
.insert(old_commit_id.clone(), vec![new_commit.id().clone()]);
assert_eq!(
(previous_rebased_value, previous_mapping_value),
(None, None),
"Trying to rebase the same commit {old_commit_id:?} in two different ways",
);
self.update_references(old_commit_id, vec![new_commit.id().clone()], false)?;
Ok(())
}
pub fn rebase_all(&mut self) -> Result<(), TreeMergeError> {
while let Some(old_commit) = self.to_visit.pop() {
self.rebase_one(old_commit)?;
}
let mut view = self.mut_repo.view().store_view().clone();
for commit_id in &self.heads_to_remove {
view.head_ids.remove(commit_id);
}
for commit_id in &self.heads_to_add {
view.head_ids.insert(commit_id.clone());
}
self.heads_to_remove.clear();
self.heads_to_add.clear();
self.mut_repo.set_view(view);
Ok(())
}
}