use std::collections::HashMap;
use std::collections::HashSet;
use std::hash::Hash;
use std::ops::Deref as _;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::Mutex;
use futures::StreamExt as _;
use futures::TryStreamExt as _;
use futures::executor::block_on_stream;
use futures::future::try_join_all;
use itertools::Itertools as _;
use jj_lib::backend::BackendError;
use jj_lib::backend::BackendResult;
use jj_lib::backend::ChangeId;
use jj_lib::backend::CommitId;
use jj_lib::backend::Signature;
use jj_lib::backend::TreeId;
use jj_lib::commit::Commit;
use jj_lib::conflict_labels::ConflictLabels;
use jj_lib::evolution::WalkPredecessorsError;
use jj_lib::evolution::walk_predecessors;
use jj_lib::graph_dominators::FlowGraph;
use jj_lib::graph_dominators::SimpleDirectedGraph;
use jj_lib::graph_dominators::ValueCache;
use jj_lib::index::IndexError;
use jj_lib::merge::Merge;
use jj_lib::merge::MergeBuilder;
use jj_lib::merge::SameChange;
use jj_lib::merged_tree::MergedTree;
use jj_lib::repo::MutableRepo;
use jj_lib::repo::ReadonlyRepo;
use jj_lib::repo::Repo as _;
use jj_lib::revset::ResolvedRevsetExpression;
use jj_lib::revset::RevsetEvaluationError;
use jj_lib::revset::RevsetExpression;
use jj_lib::rewrite::merge_commit_trees_no_resolve;
use jj_lib::store::Store;
use thiserror::Error;
pub type CommitsByChangeId = HashMap<ChangeId, HashMap<CommitId, Commit>>;
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum ConvergedAttribute<T> {
Solved(T),
Unsolved {
base_commit: CommitId,
excluded_divergent_commits: HashSet<CommitId>,
},
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ConvergeResult {
pub author: ConvergedAttribute<Signature>,
pub description: ConvergedAttribute<String>,
pub parents: ConvergedAttribute<Vec<CommitId>>,
pub tree: Option<TreeIdsAndLabels>,
}
#[derive(Debug, Error)]
pub enum ConvergeError {
#[error(transparent)]
Backend(#[from] BackendError),
#[error(transparent)]
Index(#[from] IndexError),
#[error(transparent)]
RevsetEvaluation(#[from] RevsetEvaluationError),
#[error(transparent)]
WalkPredecessors(#[from] WalkPredecessorsError),
#[error(transparent)]
IO(#[from] std::io::Error),
#[error(transparent)]
Other(Box<dyn std::error::Error + Send + Sync>),
}
pub async fn find_divergent_changes(
repo: &Arc<ReadonlyRepo>,
revset_expression: Arc<ResolvedRevsetExpression>,
) -> Result<CommitsByChangeId, RevsetEvaluationError> {
let mut result = CommitsByChangeId::new();
let mut stream = revset_expression.evaluate(repo.as_ref())?.stream();
while let Some(commit_id) = stream.try_next().await? {
let commit = repo.store().get_commit_async(&commit_id).await?;
result
.entry(commit.change_id().clone())
.or_default()
.insert(commit.id().clone(), commit);
}
result.retain(|_, commits| commits.len() > 1);
Ok(result)
}
pub async fn converge_change(
truncated_evolution_graph: &TruncatedEvolutionGraph,
author: Option<Signature>,
description: Option<String>,
parents: Option<Vec<CommitId>>,
tree: Option<TreeIdsAndLabels>,
) -> Result<ConvergeResult, ConvergeError> {
let author = if let Some(author) = author {
ConvergedAttribute::Solved(author)
} else {
converge_author(truncated_evolution_graph).await?
};
let description = if let Some(description) = description {
ConvergedAttribute::Solved(description)
} else {
converge_description(truncated_evolution_graph).await?
};
let parents = if let Some(parents) = parents {
ConvergedAttribute::Solved(parents)
} else {
converge_parents(truncated_evolution_graph).await?
};
let tree = if let Some(tree) = tree {
Some(tree)
} else if let ConvergedAttribute::Solved(parents) = &parents {
let tree = converge_trees(truncated_evolution_graph, parents).await?;
Some(TreeIdsAndLabels::new(tree))
} else {
None
};
Ok(ConvergeResult {
author,
description,
parents,
tree,
})
}
pub async fn apply_solution(
author: Signature,
description: String,
parents: Vec<CommitId>,
tree: TreeIdsAndLabels,
change_id: ChangeId,
divergent_commit_ids: &Vec<CommitId>,
repo_mut: &mut MutableRepo,
) -> Result<(Commit, usize), ConvergeError> {
let merged_tree = tree.to_merged_tree(repo_mut.store());
let solution = repo_mut
.new_commit(parents, merged_tree)
.set_change_id(change_id.clone())
.set_description(description)
.set_author(author)
.set_predecessors(divergent_commit_ids.clone())
.write()
.await?;
for divergent_commit_id in divergent_commit_ids {
repo_mut.set_rewritten_commit(divergent_commit_id.clone(), solution.id().clone());
}
let num_rebased = repo_mut.rebase_descendants().await?;
Ok((solution, num_rebased))
}
pub struct TruncatedEvolutionGraph {
repo: Arc<ReadonlyRepo>,
divergent_commits: Vec<Commit>,
divergent_commit_ids: Vec<CommitId>,
pub flow_graph: FlowGraph<CommitId>,
}
impl TruncatedEvolutionGraph {
pub async fn new(
repo: Arc<ReadonlyRepo>,
divergent_commits: Vec<Commit>,
) -> Result<Self, ConvergeError> {
validate(
divergent_commits.len() > 1,
&format!(
"Expected multiple divergent commits, got {}",
divergent_commits.len()
),
)?;
let divergent_commit_ids = divergent_commits
.iter()
.map(|c| c.id().clone())
.collect_vec();
let divergent_change_id = if divergent_commits.iter().map(|c| c.change_id()).all_equal() {
divergent_commits.first().unwrap().change_id().clone()
} else {
return Err(ConvergeError::Other(
"all divergent commits must have the same change-id".into(),
));
};
let mut edges = vec![];
let mut seen = HashSet::new();
let mut to_visit = HashSet::with_capacity(divergent_commit_ids.len());
to_visit.extend(divergent_commit_ids.iter().cloned());
let evolution_nodes = block_on_stream(
walk_predecessors(&repo, divergent_commit_ids.as_slice()).boxed_local(),
);
let mut initial_nodes = vec![];
for node in evolution_nodes {
let entry = node?;
let commit_id = entry.commit.id();
if *entry.commit.change_id() != divergent_change_id {
continue;
}
to_visit.remove(commit_id);
if !seen.insert(commit_id.clone()) {
continue;
}
let predecessors = entry
.predecessors()
.await?
.iter()
.filter_map(|commit| {
if *commit.change_id() == divergent_change_id {
Some(commit.id().clone())
} else {
None
}
})
.collect_vec();
for predecessor in &predecessors {
edges.push((predecessor.clone(), commit_id.clone()));
}
if predecessors.is_empty() {
initial_nodes.push(commit_id.clone());
if to_visit.is_empty() {
break;
}
} else {
to_visit.extend(predecessors);
}
}
validate(
!initial_nodes.is_empty(),
"Unexpected error: initial_nodes should not be empty",
)?;
let initial_node = if initial_nodes.len() == 1 {
initial_nodes[0].clone()
} else {
let root_commit_id = repo.store().root_commit_id().clone();
for initial_node in initial_nodes {
edges.push((root_commit_id.clone(), initial_node));
}
root_commit_id
};
let flow_graph = FlowGraph::new(SimpleDirectedGraph::new(edges), initial_node);
Ok(Self {
repo,
divergent_commits,
divergent_commit_ids,
flow_graph,
})
}
pub fn repo(&self) -> &Arc<ReadonlyRepo> {
&self.repo
}
pub fn divergent_commits(&self) -> &Vec<Commit> {
&self.divergent_commits
}
pub fn divergent_commit_ids(&self) -> &Vec<CommitId> {
&self.divergent_commit_ids
}
pub fn change_id(&self) -> &ChangeId {
self.divergent_commits[0].change_id()
}
}
async fn converge_author(
graph: &TruncatedEvolutionGraph,
) -> Result<ConvergedAttribute<Signature>, ConvergeError> {
let value_fn = async |c: &Commit| Ok(c.author().clone());
let excluded_divergent_commits = HashSet::default();
let (value_merge, base_commit) =
create_value_merge(graph, &excluded_divergent_commits, value_fn).await?;
if let Some(value) = value_merge.resolve_trivial(SameChange::Accept) {
Ok(ConvergedAttribute::Solved(value.clone()))
} else {
Ok(ConvergedAttribute::Unsolved {
base_commit,
excluded_divergent_commits: HashSet::default(),
})
}
}
async fn converge_description(
graph: &TruncatedEvolutionGraph,
) -> Result<ConvergedAttribute<String>, ConvergeError> {
let value_fn = async |c: &Commit| Ok(c.description().to_string());
let excluded_divergent_commits = HashSet::default();
let (value_merge, base_commit) =
create_value_merge(graph, &excluded_divergent_commits, value_fn).await?;
if let Some(value) = value_merge.resolve_trivial(SameChange::Accept) {
Ok(ConvergedAttribute::Solved(value.clone()))
} else {
Ok(ConvergedAttribute::Unsolved {
base_commit,
excluded_divergent_commits: HashSet::default(),
})
}
}
async fn converge_parents(
graph: &TruncatedEvolutionGraph,
) -> Result<ConvergedAttribute<Vec<CommitId>>, ConvergeError> {
let viable_commits = remove_descendants(graph.repo(), graph.divergent_commit_ids()).await?;
let excluded_divergent_commits: HashSet<CommitId> = graph
.divergent_commit_ids()
.iter()
.filter(|commit_id| !viable_commits.contains(commit_id))
.cloned()
.collect();
let get_parents_fn = async |c: &Commit| Ok(c.parent_ids().to_vec());
let (value_merge, base_commit) =
create_value_merge(graph, &excluded_divergent_commits, get_parents_fn).await?;
if let Some(value) = value_merge.resolve_trivial(SameChange::Accept) {
Ok(ConvergedAttribute::Solved(value.clone()))
} else {
Ok(ConvergedAttribute::Unsolved {
base_commit,
excluded_divergent_commits,
})
}
}
#[derive(Eq, Hash, PartialEq, Clone, Debug)]
pub struct TreeIdsAndLabels {
pub tree_ids: Merge<TreeId>,
pub labels: ConflictLabels,
}
impl TreeIdsAndLabels {
pub fn new(merged_tree: MergedTree) -> Self {
let (tree_ids, labels) = merged_tree.into_tree_ids_and_labels();
Self { tree_ids, labels }
}
pub fn to_merged_tree(&self, store: &Arc<Store>) -> MergedTree {
MergedTree::new(store.clone(), self.tree_ids.clone(), self.labels.clone())
}
}
async fn converge_trees(
truncated_evolution_graph: &TruncatedEvolutionGraph,
parents: &[CommitId],
) -> Result<MergedTree, ConvergeError> {
let repo = truncated_evolution_graph.repo();
let parent_commits: Vec<Commit> =
try_join_all(parents.iter().map(|id| repo.store().get_commit_async(id))).await?;
let parents_merged_tree = merge_commit_trees_no_resolve(repo.as_ref(), &parent_commits).await?;
let rebased_resolved_trees = Arc::new(Mutex::new(HashMap::<CommitId, TreeIdsAndLabels>::new()));
let value_fn = async |commit: &Commit| -> Result<Merge<TreeId>, ConvergeError> {
let tree_ids_and_labels = TreeIdsAndLabels::new(
rebase_tree_onto_solution_parents(commit, parents, &parents_merged_tree, repo).await?,
);
rebased_resolved_trees
.lock()
.unwrap()
.insert(commit.id().clone(), tree_ids_and_labels.clone());
Ok(tree_ids_and_labels.tree_ids.clone())
};
let mut value_cache = ValueCache::new(async |commit_id: &CommitId| {
let commit = repo.store().get_commit_async(commit_id).await?;
value_fn(&commit).await
});
let dominator_value = truncated_evolution_graph
.flow_graph
.find_dominator_value_with_value_cache(
truncated_evolution_graph.divergent_commit_ids(),
&mut value_cache,
)
.await
.map_err(|e| ConvergeError::Other(e.into()))?;
let dominator_producer =
get_value_producer(truncated_evolution_graph, &dominator_value, &value_cache)?;
let base_commit = repo.store().get_commit_async(&dominator_producer).await?;
let rebased_resolved_trees = Arc::try_unwrap(rebased_resolved_trees)
.map_err(|_| ConvergeError::Other("Failed to unwrap rebased_resolved_trees Arc".into()))?
.into_inner()
.unwrap();
let mut terms: Vec<(MergedTree, String)> = Vec::new();
let base_term = get_term_for_tree_merge(
&base_commit,
parents,
&rebased_resolved_trees,
"converge base",
);
terms.push(base_term.clone());
for divergent_commit in truncated_evolution_graph.divergent_commits() {
terms.push(base_term.clone());
terms.push(get_term_for_tree_merge(
divergent_commit,
parents,
&rebased_resolved_trees,
"divergent commit",
));
}
Ok(MergedTree::merge(MergeBuilder::from_iter(terms).build()).await?)
}
fn get_term_for_tree_merge(
commit: &Commit,
parents: &[CommitId],
rebased_resolved_trees: &HashMap<CommitId, TreeIdsAndLabels>,
prefix: &str,
) -> (MergedTree, String) {
let rebased_and_resolved_tree = rebased_resolved_trees
.get(commit.id())
.unwrap()
.to_merged_tree(commit.store());
let conflict_label = if commit.parent_ids() == parents {
format!("{prefix}: {}", commit.conflict_label())
} else {
format!(
"{prefix}: tree of {} rebased onto parents",
commit.conflict_label()
)
};
(rebased_and_resolved_tree, conflict_label)
}
async fn create_value_merge<T, VF>(
graph: &TruncatedEvolutionGraph,
excluded_divergent_commits: &HashSet<CommitId>,
value_fn: VF,
) -> Result<(Merge<T>, CommitId), ConvergeError>
where
T: Eq + Hash + Clone,
VF: AsyncFn(&Commit) -> Result<T, ConvergeError>,
{
let mut value_cache = ValueCache::new(async |commit_id: &CommitId| {
let commit = graph.repo().store().get_commit_async(commit_id).await?;
value_fn(&commit).await
});
let divergent_commits = graph
.divergent_commit_ids()
.iter()
.filter(|id| !excluded_divergent_commits.contains(*id));
let dominator_value = graph
.flow_graph
.find_dominator_value_with_value_cache(divergent_commits.clone(), &mut value_cache)
.await
.map_err(|e| ConvergeError::Other(e.into()))?;
let dominator_producer = get_value_producer(graph, &dominator_value, &value_cache)?;
let mut merge_builder = MergeBuilder::default();
merge_builder.extend([(*dominator_value).clone()]);
for divergent_commit in divergent_commits {
let commit_value = value_cache.get_value(divergent_commit).await?;
merge_builder.extend([(*dominator_value).clone(), (*commit_value).clone()]);
}
Ok((merge_builder.build(), dominator_producer))
}
fn get_value_producer<T, VF>(
truncated_evolution_graph: &TruncatedEvolutionGraph,
value: &Rc<T>,
value_cache: &ValueCache<CommitId, T, VF>,
) -> Result<CommitId, ConvergeError>
where
T: Eq + Hash,
VF: AsyncFn(&CommitId) -> Result<T, ConvergeError>,
{
let producers = value_cache.get_nodes_for_value(value).unwrap();
match producers.len() {
0 => unreachable!(), 1 => return Ok(producers[0].clone()),
_ => {}
}
let resolved_change_targets = truncated_evolution_graph
.repo()
.resolve_change_id(truncated_evolution_graph.change_id())?;
let input_position: HashMap<&CommitId, usize> = truncated_evolution_graph
.divergent_commit_ids()
.iter()
.enumerate()
.map(|(position, commit_id)| (commit_id, position))
.collect();
let producer = producers
.iter()
.min_by_key(|commit_id: &&CommitId| {
let change_offset = match &resolved_change_targets {
Some(change_targets) => change_targets.find_offset(commit_id).unwrap_or(usize::MAX),
None => usize::MAX,
};
let input_position = *input_position.get(commit_id).unwrap_or(&usize::MAX);
(change_offset, input_position, *commit_id)
})
.unwrap()
.clone();
Ok(producer)
}
async fn rebase_tree_onto_solution_parents(
c: &Commit,
parents: &[CommitId],
parents_merged_tree: &MergedTree,
repo: &Arc<ReadonlyRepo>,
) -> BackendResult<MergedTree> {
if c.parent_ids() == parents {
return Ok(c.tree());
}
let mut terms: Vec<(MergedTree, String)> = Vec::new();
terms.push((
parents_merged_tree.clone(),
"converge solution parent(s)".to_string(),
));
terms.push((
c.parent_tree_no_resolve(repo.as_ref()).await?,
c.parents_conflict_label().await?,
));
terms.push((c.tree(), c.conflict_label()));
MergedTree::merge(MergeBuilder::from_iter(terms).build()).await
}
pub async fn remove_descendants(
repo: &Arc<ReadonlyRepo>,
commit_ids: &[CommitId],
) -> Result<HashSet<CommitId>, ConvergeError> {
if commit_ids.is_empty() {
return Ok(HashSet::default());
}
let revset_expression = Arc::new(RevsetExpression::Commits(commit_ids.to_vec())).roots();
let mut result = HashSet::with_capacity(commit_ids.len());
let mut stream = revset_expression.evaluate(repo.deref())?.stream();
while let Some(commit_id) = stream.try_next().await? {
result.insert(commit_id);
}
validate(
!result.is_empty(),
&format!("the result of remove_descendants should never be empty; commits: {commit_ids:?}"),
)?;
Ok(result)
}
fn validate(predicate: bool, msg: &str) -> Result<(), ConvergeError> {
if !predicate {
Err(ConvergeError::Other(msg.into()))
} else {
Ok(())
}
}