mirror of
https://github.com/martinvonz/jj.git
synced 2025-01-30 08:05:05 +00:00
d961f61623
All the information needed for calculating the evolution state is now in the index, so let's use it. This speeds up calculation of the evolution state from 1.53s to 150ms in the git.git repo. In the Linux repo, it was sped up from 28.9s to 3.07s. That's still unbearably slow (and still pretty slow in the git.git repo too). We may need to keep a persistent cache of the evolution state, but that will have to come later; this improvement is good enough for now.
1015 lines
36 KiB
Rust
1015 lines
36 KiB
Rust
// Copyright 2020 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use crate::commit::Commit;
|
|
use crate::commit_builder::CommitBuilder;
|
|
use crate::dag_walk::{bfs, closest_common_node, leaves, walk_ancestors};
|
|
use crate::repo::{MutableRepo, ReadonlyRepo, RepoRef};
|
|
use crate::repo_path::DirRepoPath;
|
|
use crate::rewrite::{merge_commit_trees, rebase_commit};
|
|
use crate::settings::UserSettings;
|
|
use crate::store::{ChangeId, CommitId};
|
|
use crate::store_wrapper::StoreWrapper;
|
|
use crate::transaction::Transaction;
|
|
use crate::trees::merge_trees;
|
|
|
|
// TODO: Combine some maps/sets and use a struct as value instead.
|
|
// TODO: Move some of this into the index?
|
|
#[derive(Debug, Clone, Default)]
|
|
struct State {
|
|
children: HashMap<CommitId, HashSet<CommitId>>,
|
|
/// Contains all successors whether they have the same change id or not.
|
|
successors: HashMap<CommitId, HashSet<CommitId>>,
|
|
/// Contains the subset of the keys in `successors` for which there is a
|
|
/// successor with the same change id.
|
|
obsolete_commits: HashSet<CommitId>,
|
|
pruned_commits: HashSet<CommitId>,
|
|
orphan_commits: HashSet<CommitId>,
|
|
/// If there's more than one element in the value, then the change is
|
|
/// divergent.
|
|
non_obsoletes_by_changeid: HashMap<ChangeId, HashSet<CommitId>>,
|
|
}
|
|
|
|
impl State {
|
|
fn calculate(repo: RepoRef) -> State {
|
|
let view = repo.view();
|
|
let index = repo.index();
|
|
let mut state = State::default();
|
|
let head_ids: Vec<_> = view.heads().iter().cloned().collect();
|
|
let mut change_to_commits = HashMap::new();
|
|
for head_id in &head_ids {
|
|
state.children.insert(head_id.clone(), HashSet::new());
|
|
}
|
|
for entry in index.walk_revs(&head_ids, &[]) {
|
|
let commit_id = entry.commit_id();
|
|
change_to_commits
|
|
.entry(entry.change_id().clone())
|
|
.or_insert_with(HashSet::new)
|
|
.insert(commit_id.clone());
|
|
if entry.is_pruned() {
|
|
state.pruned_commits.insert(commit_id.clone());
|
|
}
|
|
for parent_pos in entry.parent_positions() {
|
|
let parent_entry = index.entry_by_pos(parent_pos);
|
|
let parent_id = parent_entry.commit_id();
|
|
state
|
|
.children
|
|
.entry(parent_id.clone())
|
|
.or_insert_with(HashSet::new)
|
|
.insert(commit_id.clone());
|
|
}
|
|
for predecessor_pos in entry.predecessor_positions() {
|
|
let predecessor_entry = index.entry_by_pos(predecessor_pos);
|
|
let predecessor_id = predecessor_entry.commit_id();
|
|
state
|
|
.successors
|
|
.entry(predecessor_id.clone())
|
|
.or_insert_with(HashSet::new)
|
|
.insert(commit_id.clone());
|
|
if predecessor_entry.change_id() == entry.change_id() {
|
|
state.obsolete_commits.insert(predecessor_id.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find non-obsolete commits by change id (potentially divergent commits)
|
|
for (change_id, commit_ids) in change_to_commits {
|
|
let non_obsoletes: HashSet<CommitId> = commit_ids
|
|
.difference(&state.obsolete_commits)
|
|
.cloned()
|
|
.collect();
|
|
state
|
|
.non_obsoletes_by_changeid
|
|
.insert(change_id, non_obsoletes);
|
|
}
|
|
// Find orphans by walking to the children of obsolete commits
|
|
let mut work: Vec<CommitId> = state.obsolete_commits.iter().cloned().collect();
|
|
work.extend(state.pruned_commits.iter().cloned());
|
|
while !work.is_empty() {
|
|
let commit_id = work.pop().unwrap();
|
|
for child in state.children.get(&commit_id).unwrap() {
|
|
if state.orphan_commits.insert(child.clone()) {
|
|
work.push(child.clone());
|
|
}
|
|
}
|
|
}
|
|
state.orphan_commits = state
|
|
.orphan_commits
|
|
.iter()
|
|
.filter(|commit_id| {
|
|
!(state.obsolete_commits.contains(commit_id)
|
|
|| state.pruned_commits.contains(commit_id))
|
|
})
|
|
.cloned()
|
|
.collect();
|
|
|
|
state
|
|
}
|
|
|
|
fn successors(&self, commit_id: &CommitId) -> HashSet<CommitId> {
|
|
self.successors
|
|
.get(commit_id)
|
|
.cloned()
|
|
.unwrap_or_else(HashSet::new)
|
|
}
|
|
|
|
fn is_obsolete(&self, commit_id: &CommitId) -> bool {
|
|
self.obsolete_commits.contains(commit_id)
|
|
}
|
|
|
|
fn is_orphan(&self, commit_id: &CommitId) -> bool {
|
|
self.orphan_commits.contains(commit_id)
|
|
}
|
|
|
|
fn is_divergent(&self, change_id: &ChangeId) -> bool {
|
|
self.non_obsoletes_by_changeid
|
|
.get(change_id)
|
|
.map_or(false, |non_obsoletes| non_obsoletes.len() > 1)
|
|
}
|
|
|
|
fn add_commit(&mut self, commit: &Commit) {
|
|
self.add_commit_data(
|
|
commit.id(),
|
|
commit.change_id(),
|
|
&commit.parent_ids(),
|
|
&commit.predecessor_ids(),
|
|
commit.is_pruned(),
|
|
);
|
|
}
|
|
|
|
fn add_commit_data(
|
|
&mut self,
|
|
commit_id: &CommitId,
|
|
change_id: &ChangeId,
|
|
parents: &[CommitId],
|
|
predecessors: &[CommitId],
|
|
is_pruned: bool,
|
|
) {
|
|
// TODO: Error out (or ignore?) if the root id is a predecessor or divergent
|
|
// (adding the root once should be fine). Perhaps this is not the right
|
|
// place to do that (we don't know what the root id is here).
|
|
for parent in parents {
|
|
self.children
|
|
.entry(parent.clone())
|
|
.or_default()
|
|
.insert(commit_id.clone());
|
|
}
|
|
if is_pruned {
|
|
self.pruned_commits.insert(commit_id.clone());
|
|
}
|
|
// Update the non_obsoletes_by_changeid by adding the new commit and removing
|
|
// the predecessors.
|
|
self.non_obsoletes_by_changeid
|
|
.entry(change_id.clone())
|
|
.or_default()
|
|
.insert(commit_id.clone());
|
|
for predecessor in predecessors {
|
|
self.successors
|
|
.entry(predecessor.clone())
|
|
.or_default()
|
|
.insert(commit_id.clone());
|
|
let became_obsolete = self
|
|
.non_obsoletes_by_changeid
|
|
.get_mut(change_id)
|
|
.unwrap()
|
|
.remove(predecessor);
|
|
// Mark descendants as orphans if the predecessor just became obsolete.
|
|
if became_obsolete {
|
|
assert!(self.obsolete_commits.insert(predecessor.clone()));
|
|
|
|
let mut descendants = HashSet::new();
|
|
for descendant in bfs(
|
|
vec![predecessor.clone()],
|
|
Box::new(|commit_id| commit_id.clone()),
|
|
Box::new(|commit_id| {
|
|
self.children
|
|
.get(commit_id)
|
|
.cloned()
|
|
.unwrap_or_else(HashSet::new)
|
|
}),
|
|
) {
|
|
descendants.insert(descendant);
|
|
}
|
|
descendants.remove(predecessor);
|
|
descendants = descendants
|
|
.iter()
|
|
.filter(|commit_id| {
|
|
!(self.obsolete_commits.contains(commit_id)
|
|
|| self.pruned_commits.contains(commit_id))
|
|
})
|
|
.cloned()
|
|
.collect();
|
|
self.orphan_commits.extend(descendants);
|
|
}
|
|
}
|
|
// Mark the new commit an orphan if any of its parents are obsolete, pruned, or
|
|
// orphans. Note that this has to be done late, in case a parent just got marked
|
|
// as obsolete or orphan above.
|
|
let is_orphan = parents.iter().any(|parent| {
|
|
self.obsolete_commits.contains(parent)
|
|
|| self.pruned_commits.contains(parent)
|
|
|| self.orphan_commits.contains(commit_id)
|
|
});
|
|
if is_orphan {
|
|
self.orphan_commits.insert(commit_id.clone());
|
|
}
|
|
}
|
|
|
|
pub fn new_parent(&self, store: &StoreWrapper, old_parent_id: &CommitId) -> HashSet<CommitId> {
|
|
let mut new_parents = HashSet::new();
|
|
if let Some(successor_ids) = self.successors.get(old_parent_id) {
|
|
let old_parent = store.get_commit(old_parent_id).unwrap();
|
|
let successors: HashSet<_> = successor_ids
|
|
.iter()
|
|
.map(|id| store.get_commit(id).unwrap())
|
|
.collect();
|
|
let mut children = HashMap::new();
|
|
for successor in &successors {
|
|
for parent in successor.parents() {
|
|
if let Some(parent) = successors.get(&parent) {
|
|
children
|
|
.entry(parent.clone())
|
|
.or_insert_with(HashSet::new)
|
|
.insert(successor.clone());
|
|
}
|
|
}
|
|
}
|
|
let mut all_candidates = HashSet::new();
|
|
for successor in &successors {
|
|
if successor.change_id() != old_parent.change_id() {
|
|
continue;
|
|
}
|
|
|
|
// Start with the successor as candidate.
|
|
let mut candidates = HashSet::new();
|
|
candidates.insert(successor.clone());
|
|
|
|
// If the successor has children that are successors of the same
|
|
// commit, we consider the original commit to be a split. We then return
|
|
// the tip-most successor.
|
|
candidates = leaves(
|
|
candidates,
|
|
&mut |commit: &Commit| -> HashSet<Commit> {
|
|
if let Some(children) = children.get(commit) {
|
|
children.clone()
|
|
} else {
|
|
HashSet::new()
|
|
}
|
|
},
|
|
&|commit: &Commit| -> CommitId { commit.id().clone() },
|
|
);
|
|
|
|
// If a successor is pruned, use its parent(s) instead.
|
|
candidates = leaves(
|
|
candidates,
|
|
&mut |commit: &Commit| -> Vec<Commit> {
|
|
if commit.is_pruned() {
|
|
commit.parents()
|
|
} else {
|
|
vec![]
|
|
}
|
|
},
|
|
&|commit: &Commit| -> CommitId { commit.id().clone() },
|
|
);
|
|
|
|
for candidate in candidates {
|
|
all_candidates.insert(candidate.clone());
|
|
}
|
|
}
|
|
|
|
// Filter out candidates that are ancestors of or other candidates.
|
|
let non_heads: Vec<_> = all_candidates
|
|
.iter()
|
|
.flat_map(|commit| commit.parents())
|
|
.collect();
|
|
for commit in walk_ancestors(non_heads) {
|
|
all_candidates.remove(&commit);
|
|
}
|
|
|
|
for candidate in all_candidates {
|
|
// TODO: Make this not recursive
|
|
for effective_successor in self.new_parent(store, candidate.id()) {
|
|
new_parents.insert(effective_successor);
|
|
}
|
|
}
|
|
}
|
|
if new_parents.is_empty() {
|
|
// TODO: Should we go to the parents here too if the commit is pruned?
|
|
new_parents.insert(old_parent_id.clone());
|
|
}
|
|
new_parents
|
|
}
|
|
}
|
|
|
|
pub enum EvolutionRef<'a, 'm: 'a, 'r: 'm> {
|
|
Readonly(&'a ReadonlyEvolution<'r>),
|
|
Mutable(&'a MutableEvolution<'m, 'r>),
|
|
}
|
|
|
|
impl EvolutionRef<'_, '_, '_> {
|
|
pub fn successors(&self, commit_id: &CommitId) -> HashSet<CommitId> {
|
|
match self {
|
|
EvolutionRef::Readonly(evolution) => evolution.successors(commit_id),
|
|
EvolutionRef::Mutable(evolution) => evolution.successors(commit_id),
|
|
}
|
|
}
|
|
|
|
pub fn is_obsolete(&self, commit_id: &CommitId) -> bool {
|
|
match self {
|
|
EvolutionRef::Readonly(evolution) => evolution.is_obsolete(commit_id),
|
|
EvolutionRef::Mutable(evolution) => evolution.is_obsolete(commit_id),
|
|
}
|
|
}
|
|
|
|
pub fn is_orphan(&self, commit_id: &CommitId) -> bool {
|
|
match self {
|
|
EvolutionRef::Readonly(evolution) => evolution.is_orphan(commit_id),
|
|
EvolutionRef::Mutable(evolution) => evolution.is_orphan(commit_id),
|
|
}
|
|
}
|
|
|
|
pub fn is_divergent(&self, change_id: &ChangeId) -> bool {
|
|
match self {
|
|
EvolutionRef::Readonly(evolution) => evolution.is_divergent(change_id),
|
|
EvolutionRef::Mutable(evolution) => evolution.is_divergent(change_id),
|
|
}
|
|
}
|
|
|
|
/// Given a current parent, finds the new parent candidates. If the current
|
|
/// parent is not obsolete, then a singleton set of that commit will be
|
|
/// returned.
|
|
///
|
|
/// * If a successor is pruned, its parent(s) will instead be included (or
|
|
/// their parents if they are also pruned).
|
|
///
|
|
/// * If the commit has multiple live successors, the tip-most one(s) of
|
|
/// them will be chosen.
|
|
///
|
|
/// The second case is more complex than it probably seems. For example,
|
|
/// let's say commit A was split into B, A', and C (where A' has the same
|
|
/// change id as A). Then C is rebased to somewhere else and becomes C'.
|
|
/// We will choose that C' as effective successor even though it has a
|
|
/// different change id and is not a descendant of one that does.
|
|
pub fn new_parent(&self, old_parent_id: &CommitId) -> HashSet<CommitId> {
|
|
match self {
|
|
EvolutionRef::Readonly(evolution) => evolution.new_parent(old_parent_id),
|
|
EvolutionRef::Mutable(evolution) => evolution.new_parent(old_parent_id),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ReadonlyEvolution<'r> {
|
|
repo: &'r ReadonlyRepo,
|
|
state: Mutex<Option<Arc<State>>>,
|
|
}
|
|
|
|
pub trait EvolveListener {
|
|
fn orphan_evolved(&mut self, tx: &mut Transaction, orphan: &Commit, new_commit: &Commit);
|
|
fn orphan_target_ambiguous(&mut self, tx: &mut Transaction, orphan: &Commit);
|
|
fn divergent_resolved(
|
|
&mut self,
|
|
tx: &mut Transaction,
|
|
divergents: &[Commit],
|
|
resolved: &Commit,
|
|
);
|
|
fn divergent_no_common_predecessor(
|
|
&mut self,
|
|
tx: &mut Transaction,
|
|
commit1: &Commit,
|
|
commit2: &Commit,
|
|
);
|
|
}
|
|
|
|
impl<'r> ReadonlyEvolution<'r> {
|
|
pub fn new(repo: &'r ReadonlyRepo) -> Self {
|
|
ReadonlyEvolution {
|
|
repo,
|
|
state: Mutex::new(None),
|
|
}
|
|
}
|
|
|
|
fn get_state(&self) -> Arc<State> {
|
|
let mut locked_state = self.state.lock().unwrap();
|
|
if locked_state.is_none() {
|
|
locked_state.replace(Arc::new(State::calculate(self.repo.as_repo_ref())));
|
|
}
|
|
locked_state.as_ref().unwrap().clone()
|
|
}
|
|
|
|
pub fn start_modification<'m>(&self, repo: &'m MutableRepo<'r>) -> MutableEvolution<'r, 'm> {
|
|
MutableEvolution {
|
|
repo,
|
|
state: self.get_state().as_ref().clone(),
|
|
}
|
|
}
|
|
|
|
pub fn successors(&self, commit_id: &CommitId) -> HashSet<CommitId> {
|
|
self.get_state().successors(commit_id)
|
|
}
|
|
|
|
pub fn is_obsolete(&self, commit_id: &CommitId) -> bool {
|
|
self.get_state().is_obsolete(commit_id)
|
|
}
|
|
|
|
pub fn is_orphan(&self, commit_id: &CommitId) -> bool {
|
|
self.get_state().is_orphan(commit_id)
|
|
}
|
|
|
|
pub fn is_divergent(&self, change_id: &ChangeId) -> bool {
|
|
self.get_state().is_divergent(change_id)
|
|
}
|
|
|
|
pub fn new_parent(&self, old_parent_id: &CommitId) -> HashSet<CommitId> {
|
|
self.get_state()
|
|
.new_parent(self.repo.store(), old_parent_id)
|
|
}
|
|
}
|
|
|
|
pub struct MutableEvolution<'r: 'm, 'm> {
|
|
repo: &'m MutableRepo<'r>,
|
|
state: State,
|
|
}
|
|
|
|
impl MutableEvolution<'_, '_> {
|
|
pub fn successors(&self, commit_id: &CommitId) -> HashSet<CommitId> {
|
|
self.state.successors(commit_id)
|
|
}
|
|
|
|
pub fn is_obsolete(&self, commit_id: &CommitId) -> bool {
|
|
self.state.is_obsolete(commit_id)
|
|
}
|
|
|
|
pub fn is_orphan(&self, commit_id: &CommitId) -> bool {
|
|
self.state.is_orphan(commit_id)
|
|
}
|
|
|
|
pub fn is_divergent(&self, change_id: &ChangeId) -> bool {
|
|
self.state.is_divergent(change_id)
|
|
}
|
|
|
|
pub fn new_parent(&self, old_parent_id: &CommitId) -> HashSet<CommitId> {
|
|
self.state.new_parent(self.repo.store(), old_parent_id)
|
|
}
|
|
|
|
pub fn add_commit(&mut self, commit: &Commit) {
|
|
self.state.add_commit(commit);
|
|
}
|
|
|
|
pub fn invalidate(&mut self) {
|
|
self.state = State::calculate(self.repo.as_repo_ref());
|
|
}
|
|
}
|
|
|
|
pub fn evolve(
|
|
user_settings: &UserSettings,
|
|
tx: &mut Transaction,
|
|
listener: &mut dyn EvolveListener,
|
|
) {
|
|
let store = tx.store().clone();
|
|
|
|
// Resolving divergence can creates new orphans but not vice versa, so resolve
|
|
// divergence first.
|
|
let divergent_changes: Vec<_> = tx
|
|
.as_repo_mut()
|
|
.evolution_mut()
|
|
.state
|
|
.non_obsoletes_by_changeid
|
|
.values()
|
|
.filter(|non_obsoletes| non_obsoletes.len() > 1)
|
|
.cloned()
|
|
.collect();
|
|
for commit_ids in divergent_changes {
|
|
let commits: HashSet<Commit> = commit_ids
|
|
.iter()
|
|
.map(|id| store.get_commit(&id).unwrap())
|
|
.collect();
|
|
evolve_divergent_change(user_settings, &store, tx, listener, &commits);
|
|
}
|
|
|
|
// Dom't reuse the state from above, since the divergence-resolution may have
|
|
// created new orphans, or resolved existing orphans.
|
|
let orphans: HashSet<Commit> = tx
|
|
.as_repo_mut()
|
|
.evolution_mut()
|
|
.state
|
|
.orphan_commits
|
|
.iter()
|
|
.map(|id| store.get_commit(&id).unwrap())
|
|
.collect();
|
|
let non_heads: HashSet<Commit> = orphans.iter().flat_map(|commit| commit.parents()).collect();
|
|
let orphan_heads: HashSet<Commit> = orphans.difference(&non_heads).cloned().collect();
|
|
let mut orphans_topo_order = vec![];
|
|
for commit in bfs(
|
|
orphan_heads,
|
|
Box::new(|commit| commit.id().clone()),
|
|
Box::new(|commit| {
|
|
commit
|
|
.parents()
|
|
.iter()
|
|
.filter(|commit| orphans.contains(commit))
|
|
.cloned()
|
|
.collect::<Vec<_>>()
|
|
}),
|
|
) {
|
|
orphans_topo_order.push(commit);
|
|
}
|
|
|
|
while !orphans_topo_order.is_empty() {
|
|
let orphan = orphans_topo_order.pop().unwrap();
|
|
let old_parents = orphan.parents();
|
|
let mut new_parents = vec![];
|
|
let mut ambiguous_new_parents = false;
|
|
let evolution = tx.as_repo_mut().evolution();
|
|
for old_parent in &old_parents {
|
|
let new_parent_candidates = evolution.new_parent(old_parent.id());
|
|
if new_parent_candidates.len() > 1 {
|
|
ambiguous_new_parents = true;
|
|
break;
|
|
}
|
|
new_parents.push(
|
|
store
|
|
.get_commit(new_parent_candidates.iter().next().unwrap())
|
|
.unwrap(),
|
|
);
|
|
}
|
|
if ambiguous_new_parents {
|
|
listener.orphan_target_ambiguous(tx, &orphan);
|
|
} else {
|
|
let new_commit = rebase_commit(user_settings, tx, &orphan, &new_parents);
|
|
listener.orphan_evolved(tx, &orphan, &new_commit);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn evolve_divergent_change(
|
|
user_settings: &UserSettings,
|
|
store: &Arc<StoreWrapper>,
|
|
tx: &mut Transaction,
|
|
listener: &mut dyn EvolveListener,
|
|
commits: &HashSet<Commit>,
|
|
) {
|
|
// Resolve divergence pair-wise, starting with the two oldest commits.
|
|
let mut commits: Vec<Commit> = commits.iter().cloned().collect();
|
|
commits.sort_by(|a: &Commit, b: &Commit| a.committer().timestamp.cmp(&b.committer().timestamp));
|
|
commits.reverse();
|
|
|
|
// Create a copy to pass to the listener
|
|
let sources = commits.clone();
|
|
|
|
while commits.len() > 1 {
|
|
let commit2 = commits.pop().unwrap();
|
|
let commit1 = commits.pop().unwrap();
|
|
|
|
let common_predecessor = closest_common_node(
|
|
vec![commit1.clone()],
|
|
vec![commit2.clone()],
|
|
&|commit: &Commit| commit.predecessors(),
|
|
&|commit: &Commit| commit.id().clone(),
|
|
);
|
|
match common_predecessor {
|
|
None => {
|
|
listener.divergent_no_common_predecessor(tx, &commit1, &commit2);
|
|
return;
|
|
}
|
|
Some(common_predecessor) => {
|
|
let resolved_commit = evolve_two_divergent_commits(
|
|
user_settings,
|
|
store,
|
|
tx,
|
|
&common_predecessor,
|
|
&commit1,
|
|
&commit2,
|
|
);
|
|
commits.push(resolved_commit);
|
|
}
|
|
}
|
|
}
|
|
|
|
let resolved = commits.pop().unwrap();
|
|
listener.divergent_resolved(tx, &sources, &resolved);
|
|
}
|
|
|
|
fn evolve_two_divergent_commits(
|
|
user_settings: &UserSettings,
|
|
store: &Arc<StoreWrapper>,
|
|
tx: &mut Transaction,
|
|
common_predecessor: &Commit,
|
|
commit1: &Commit,
|
|
commit2: &Commit,
|
|
) -> Commit {
|
|
let new_parents = commit1.parents();
|
|
let rebased_tree2 = if commit2.parents() == new_parents {
|
|
commit2.tree()
|
|
} else {
|
|
let old_base_tree = merge_commit_trees(tx.as_repo_ref(), &commit2.parents());
|
|
let new_base_tree = merge_commit_trees(tx.as_repo_ref(), &new_parents);
|
|
let tree_id = merge_trees(&new_base_tree, &old_base_tree, &commit2.tree()).unwrap();
|
|
store.get_tree(&DirRepoPath::root(), &tree_id).unwrap()
|
|
};
|
|
let rebased_predecessor_tree = if common_predecessor.parents() == new_parents {
|
|
common_predecessor.tree()
|
|
} else {
|
|
let old_base_tree = merge_commit_trees(tx.as_repo_ref(), &common_predecessor.parents());
|
|
let new_base_tree = merge_commit_trees(tx.as_repo_ref(), &new_parents);
|
|
let tree_id =
|
|
merge_trees(&new_base_tree, &old_base_tree, &common_predecessor.tree()).unwrap();
|
|
store.get_tree(&DirRepoPath::root(), &tree_id).unwrap()
|
|
};
|
|
|
|
let resolved_tree =
|
|
merge_trees(&commit1.tree(), &rebased_predecessor_tree, &rebased_tree2).unwrap();
|
|
|
|
// TODO: Merge commit description and other commit metadata. How do we deal with
|
|
// conflicts? It's probably best to interactively ask the caller (which
|
|
// might ask the user in interactive use).
|
|
CommitBuilder::for_rewrite_from(user_settings, store, &commit1)
|
|
.set_tree(resolved_tree)
|
|
.set_predecessors(vec![commit1.id().clone(), commit2.id().clone()])
|
|
.write_to_transaction(tx)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn add_commit_data_initial() {
|
|
let mut state = State::default();
|
|
|
|
let initial_commit = CommitId::from_hex("aaa111");
|
|
let initial_change = ChangeId::from_hex("aaa111");
|
|
|
|
state.add_commit_data(&initial_commit, &initial_change, &[], &[], false);
|
|
assert!(!state.is_obsolete(&initial_commit));
|
|
assert!(!state.is_orphan(&initial_commit));
|
|
assert!(!state.is_divergent(&initial_change));
|
|
}
|
|
|
|
#[test]
|
|
fn add_commit_data_pruned() {
|
|
let mut state = State::default();
|
|
|
|
let initial_commit = CommitId::from_hex("aaa111");
|
|
let initial_change = ChangeId::from_hex("aaa111");
|
|
|
|
state.add_commit_data(&initial_commit, &initial_change, &[], &[], true);
|
|
assert!(!state.is_obsolete(&initial_commit));
|
|
assert!(!state.is_orphan(&initial_commit));
|
|
assert!(!state.is_divergent(&initial_change));
|
|
}
|
|
|
|
#[test]
|
|
fn add_commit_data_creating_orphan() {
|
|
let mut state = State::default();
|
|
|
|
let initial_commit = CommitId::from_hex("aaa111");
|
|
let initial_change = ChangeId::from_hex("aaa111");
|
|
let orphan_commit1 = CommitId::from_hex("bbb111");
|
|
let orphan_change1 = ChangeId::from_hex("bbb111");
|
|
let orphan_commit2 = CommitId::from_hex("ccc111");
|
|
let orphan_change2 = ChangeId::from_hex("ccc111");
|
|
let obsolete_orphan_commit = CommitId::from_hex("ddd111");
|
|
let obsolete_orphan_change = ChangeId::from_hex("ddd111");
|
|
let pruned_orphan_commit = CommitId::from_hex("eee111");
|
|
let rewritten_commit = CommitId::from_hex("aaa222");
|
|
|
|
state.add_commit_data(&initial_commit, &initial_change, &[], &[], false);
|
|
state.add_commit_data(
|
|
&orphan_commit1,
|
|
&orphan_change1,
|
|
&[initial_commit.clone()],
|
|
&[],
|
|
false,
|
|
);
|
|
state.add_commit_data(
|
|
&orphan_commit2,
|
|
&orphan_change2,
|
|
&[orphan_commit1.clone()],
|
|
&[],
|
|
false,
|
|
);
|
|
state.add_commit_data(
|
|
&obsolete_orphan_commit,
|
|
&obsolete_orphan_change,
|
|
&[initial_commit.clone()],
|
|
&[],
|
|
false,
|
|
);
|
|
state.add_commit_data(
|
|
&pruned_orphan_commit,
|
|
&obsolete_orphan_change,
|
|
&[initial_commit.clone()],
|
|
&[obsolete_orphan_commit.clone()],
|
|
true,
|
|
);
|
|
state.add_commit_data(
|
|
&rewritten_commit,
|
|
&initial_change,
|
|
&[],
|
|
&[initial_commit.clone()],
|
|
false,
|
|
);
|
|
assert!(state.is_orphan(&orphan_commit1));
|
|
assert!(state.is_orphan(&orphan_commit2));
|
|
assert!(!state.is_orphan(&obsolete_orphan_commit));
|
|
assert!(!state.is_orphan(&pruned_orphan_commit));
|
|
assert!(!state.is_obsolete(&orphan_commit1));
|
|
assert!(!state.is_obsolete(&orphan_commit2));
|
|
assert!(state.is_obsolete(&obsolete_orphan_commit));
|
|
assert!(!state.is_obsolete(&pruned_orphan_commit));
|
|
}
|
|
|
|
#[test]
|
|
fn add_commit_data_new_commit_on_obsolete() {
|
|
let mut state = State::default();
|
|
|
|
let initial_commit = CommitId::from_hex("aaa111");
|
|
let initial_change = ChangeId::from_hex("aaa111");
|
|
let rewritten_commit = CommitId::from_hex("aaa222");
|
|
let new_commit = CommitId::from_hex("bbb111");
|
|
let new_change = ChangeId::from_hex("bbb111");
|
|
|
|
state.add_commit_data(&initial_commit, &initial_change, &[], &[], false);
|
|
state.add_commit_data(
|
|
&rewritten_commit,
|
|
&initial_change,
|
|
&[],
|
|
&[initial_commit.clone()],
|
|
false,
|
|
);
|
|
state.add_commit_data(
|
|
&new_commit,
|
|
&new_change,
|
|
&[initial_commit.clone()],
|
|
&[],
|
|
false,
|
|
);
|
|
assert!(state.is_orphan(&new_commit));
|
|
}
|
|
|
|
#[test]
|
|
fn add_commit_data_new_commit_on_orphan() {
|
|
let mut state = State::default();
|
|
|
|
let initial_commit = CommitId::from_hex("aaa111");
|
|
let initial_change = ChangeId::from_hex("aaa111");
|
|
let rewritten_commit = CommitId::from_hex("aaa222");
|
|
let orphan_commit = CommitId::from_hex("bbb111");
|
|
let orphan_change = ChangeId::from_hex("bbb111");
|
|
let new_commit = CommitId::from_hex("bbb111");
|
|
let new_change = ChangeId::from_hex("bbb111");
|
|
|
|
state.add_commit_data(&initial_commit, &initial_change, &[], &[], false);
|
|
state.add_commit_data(
|
|
&rewritten_commit,
|
|
&initial_change,
|
|
&[],
|
|
&[initial_commit.clone()],
|
|
false,
|
|
);
|
|
state.add_commit_data(
|
|
&orphan_commit,
|
|
&orphan_change,
|
|
&[initial_commit.clone()],
|
|
&[],
|
|
false,
|
|
);
|
|
state.add_commit_data(
|
|
&new_commit,
|
|
&new_change,
|
|
&[orphan_commit.clone()],
|
|
&[],
|
|
false,
|
|
);
|
|
assert!(state.is_orphan(&new_commit));
|
|
}
|
|
|
|
#[test]
|
|
fn add_commit_data_new_commit_on_pruned() {
|
|
let mut state = State::default();
|
|
|
|
let pruned_commit = CommitId::from_hex("aaa111");
|
|
let pruned_change = ChangeId::from_hex("aaa111");
|
|
let new_commit = CommitId::from_hex("bbb111");
|
|
let new_change = ChangeId::from_hex("bbb111");
|
|
|
|
state.add_commit_data(&pruned_commit, &pruned_change, &[], &[], true);
|
|
state.add_commit_data(
|
|
&new_commit,
|
|
&new_change,
|
|
&[pruned_commit.clone()],
|
|
&[],
|
|
false,
|
|
);
|
|
assert!(state.is_orphan(&new_commit));
|
|
}
|
|
|
|
#[test]
|
|
fn add_commit_data_rewrite_as_child() {
|
|
let mut state = State::default();
|
|
|
|
let initial_commit = CommitId::from_hex("aaa111");
|
|
let initial_change = ChangeId::from_hex("aaa111");
|
|
let rewritten_commit = CommitId::from_hex("aaa222");
|
|
|
|
state.add_commit_data(&initial_commit, &initial_change, &[], &[], false);
|
|
// The new commit is both a child and a successor of the initial commit
|
|
state.add_commit_data(
|
|
&rewritten_commit,
|
|
&initial_change,
|
|
&[initial_commit.clone()],
|
|
&[initial_commit.clone()],
|
|
false,
|
|
);
|
|
assert!(state.is_obsolete(&initial_commit));
|
|
assert!(!state.is_obsolete(&rewritten_commit));
|
|
assert!(!state.is_orphan(&initial_commit));
|
|
assert!(state.is_orphan(&rewritten_commit));
|
|
assert!(!state.is_divergent(&initial_change));
|
|
}
|
|
|
|
#[test]
|
|
fn add_commit_data_duplicates() {
|
|
let mut state = State::default();
|
|
|
|
let initial_commit = CommitId::from_hex("aaa111");
|
|
let initial_change = ChangeId::from_hex("aaa111");
|
|
let duplicate_commit1 = CommitId::from_hex("bbb111");
|
|
let duplicate_change1 = ChangeId::from_hex("bbb111");
|
|
let duplicate_commit2 = CommitId::from_hex("ccc111");
|
|
let duplicate_change2 = ChangeId::from_hex("ccc111");
|
|
|
|
state.add_commit_data(&initial_commit, &initial_change, &[], &[], false);
|
|
state.add_commit_data(
|
|
&duplicate_commit1,
|
|
&duplicate_change1,
|
|
&[],
|
|
&[initial_commit.clone()],
|
|
false,
|
|
);
|
|
state.add_commit_data(
|
|
&duplicate_commit2,
|
|
&duplicate_change2,
|
|
&[],
|
|
&[initial_commit.clone()],
|
|
false,
|
|
);
|
|
assert!(!state.is_obsolete(&initial_commit));
|
|
assert!(!state.is_obsolete(&duplicate_commit1));
|
|
assert!(!state.is_obsolete(&duplicate_commit2));
|
|
assert!(!state.is_divergent(&initial_change));
|
|
assert!(!state.is_divergent(&duplicate_change1));
|
|
assert!(!state.is_divergent(&duplicate_change2));
|
|
assert_eq!(
|
|
state.successors(&initial_commit),
|
|
hashset!(duplicate_commit1.clone(), duplicate_commit2.clone())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn add_commit_data_divergent() {
|
|
let mut state = State::default();
|
|
|
|
let initial_commit = CommitId::from_hex("aaa111");
|
|
let initial_change = ChangeId::from_hex("aaa111");
|
|
let rewritten_commit1 = CommitId::from_hex("aaa222");
|
|
let rewritten_commit2 = CommitId::from_hex("aaa333");
|
|
|
|
state.add_commit_data(&initial_commit, &initial_change, &[], &[], false);
|
|
state.add_commit_data(
|
|
&rewritten_commit1,
|
|
&initial_change,
|
|
&[],
|
|
&[initial_commit.clone()],
|
|
false,
|
|
);
|
|
state.add_commit_data(
|
|
&rewritten_commit2,
|
|
&initial_change,
|
|
&[],
|
|
&[initial_commit.clone()],
|
|
false,
|
|
);
|
|
assert!(state.is_obsolete(&initial_commit));
|
|
assert!(!state.is_obsolete(&rewritten_commit1));
|
|
assert!(!state.is_obsolete(&rewritten_commit2));
|
|
assert!(state.is_divergent(&initial_change));
|
|
assert_eq!(
|
|
state.successors(&initial_commit),
|
|
hashset!(rewritten_commit1.clone(), rewritten_commit2.clone())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn add_commit_data_divergent_pruned() {
|
|
let mut state = State::default();
|
|
|
|
let initial_commit = CommitId::from_hex("aaa111");
|
|
let initial_change = ChangeId::from_hex("aaa111");
|
|
let rewritten_pruned = CommitId::from_hex("aaa222");
|
|
let rewritten_non_pruned = CommitId::from_hex("aaa333");
|
|
|
|
state.add_commit_data(&initial_commit, &initial_change, &[], &[], false);
|
|
state.add_commit_data(
|
|
&rewritten_pruned,
|
|
&initial_change,
|
|
&[],
|
|
&[initial_commit.clone()],
|
|
true,
|
|
);
|
|
state.add_commit_data(
|
|
&rewritten_non_pruned,
|
|
&initial_change,
|
|
&[],
|
|
&[initial_commit.clone()],
|
|
false,
|
|
);
|
|
assert!(state.is_obsolete(&initial_commit));
|
|
assert!(!state.is_obsolete(&rewritten_pruned));
|
|
assert!(!state.is_obsolete(&rewritten_non_pruned));
|
|
// It's still divergent even if one side is pruned
|
|
assert!(state.is_divergent(&initial_change));
|
|
assert_eq!(
|
|
state.successors(&initial_commit),
|
|
hashset!(rewritten_pruned.clone(), rewritten_non_pruned.clone())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn add_commit_data_divergent_unrelated() {
|
|
let mut state = State::default();
|
|
|
|
let initial_commit = CommitId::from_hex("aaa111");
|
|
let initial_change = ChangeId::from_hex("aaa111");
|
|
let rewritten_commit = CommitId::from_hex("aaa222");
|
|
|
|
state.add_commit_data(&initial_commit, &initial_change, &[], &[], false);
|
|
// Same change id as the initial commit but no predecessor relationship to it
|
|
state.add_commit_data(&rewritten_commit, &initial_change, &[], &[], false);
|
|
assert!(!state.is_obsolete(&initial_commit));
|
|
assert!(!state.is_obsolete(&rewritten_commit));
|
|
assert!(state.is_divergent(&initial_change));
|
|
assert_eq!(state.successors(&initial_commit), hashset!());
|
|
}
|
|
|
|
#[test]
|
|
fn add_commit_data_divergent_convergent() {
|
|
let mut state = State::default();
|
|
|
|
let initial_commit = CommitId::from_hex("aaa111");
|
|
let initial_change = ChangeId::from_hex("aaa111");
|
|
let rewritten_commit1 = CommitId::from_hex("aaa222");
|
|
let rewritten_commit2 = CommitId::from_hex("aaa333");
|
|
let convergent_commit = CommitId::from_hex("aaa444");
|
|
|
|
state.add_commit_data(&initial_commit, &initial_change, &[], &[], false);
|
|
state.add_commit_data(
|
|
&rewritten_commit1,
|
|
&initial_change,
|
|
&[],
|
|
&[initial_commit.clone()],
|
|
false,
|
|
);
|
|
state.add_commit_data(
|
|
&rewritten_commit2,
|
|
&initial_change,
|
|
&[],
|
|
&[initial_commit.clone()],
|
|
false,
|
|
);
|
|
state.add_commit_data(
|
|
&convergent_commit,
|
|
&initial_change,
|
|
&[],
|
|
&[rewritten_commit1.clone(), rewritten_commit2.clone()],
|
|
false,
|
|
);
|
|
assert!(state.is_obsolete(&initial_commit));
|
|
assert!(state.is_obsolete(&rewritten_commit1));
|
|
assert!(state.is_obsolete(&rewritten_commit2));
|
|
assert!(!state.is_obsolete(&convergent_commit));
|
|
assert!(!state.is_divergent(&initial_change));
|
|
assert_eq!(
|
|
state.successors(&rewritten_commit1),
|
|
hashset!(convergent_commit.clone())
|
|
);
|
|
assert_eq!(
|
|
state.successors(&rewritten_commit2),
|
|
hashset!(convergent_commit.clone())
|
|
);
|
|
}
|
|
}
|