jj/lib/tests/test_refs.rs
Martin von Zweigbergk 6b1ccd4512 view: add support for merging git ref targets
When there are two concurrent operations, we would resolve conflicting
updates of git refs quite arbitrarily before this change. This change
introduces a new `refs` module with a function for doing a 3-way merge
of ref targets. For example, if both sides moved a ref forward but by
different amounts, we pick the descendant-most target. If we can't
resolve it, we leave it as a conflict. That's fine to do for git refs
because they can be resolved by simply running `jj git refresh` to
import refs again (the underlying git repo is the source of truth).

As with the previous change, I'm doing this now because mostly because
it is a good stepping stone towards branch support (issue #21). We'll
soon use the same 3-way merging for updating the local branch
definition (once we add that) when a branch changes in the git repo or
on a remote.
2021-07-24 19:01:56 -07:00

371 lines
12 KiB
Rust

// Copyright 2021 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 jujutsu_lib::op_store::RefTarget;
use jujutsu_lib::refs::merge_ref_targets;
use jujutsu_lib::testutils;
use jujutsu_lib::testutils::create_random_commit;
#[test]
fn test_merge_ref_targets() {
let settings = testutils::user_settings();
let (_temp_dir, repo) = testutils::init_repo(&settings, false);
// 6 7
// |/
// 5
// | 3 4
// | |/
// | 2
// |/
// 1
let mut tx = repo.start_transaction("test");
let mut_repo = tx.mut_repo();
let commit1 = create_random_commit(&settings, &repo).write_to_repo(mut_repo);
let commit2 = create_random_commit(&settings, &repo)
.set_parents(vec![commit1.id().clone()])
.write_to_repo(mut_repo);
let commit3 = create_random_commit(&settings, &repo)
.set_parents(vec![commit2.id().clone()])
.write_to_repo(mut_repo);
let commit4 = create_random_commit(&settings, &repo)
.set_parents(vec![commit2.id().clone()])
.write_to_repo(mut_repo);
let commit5 = create_random_commit(&settings, &repo)
.set_parents(vec![commit1.id().clone()])
.write_to_repo(mut_repo);
let commit6 = create_random_commit(&settings, &repo)
.set_parents(vec![commit5.id().clone()])
.write_to_repo(mut_repo);
let commit7 = create_random_commit(&settings, &repo)
.set_parents(vec![commit5.id().clone()])
.write_to_repo(mut_repo);
let repo = tx.commit();
let target1 = RefTarget::Normal(commit1.id().clone());
let target2 = RefTarget::Normal(commit2.id().clone());
let target3 = RefTarget::Normal(commit3.id().clone());
let target4 = RefTarget::Normal(commit4.id().clone());
let target5 = RefTarget::Normal(commit5.id().clone());
let target6 = RefTarget::Normal(commit6.id().clone());
let _target7 = RefTarget::Normal(commit7.id().clone());
let index = repo.index();
let index_ref = index.as_index_ref();
// Left moved forward
assert_eq!(
merge_ref_targets(index_ref, Some(&target3), Some(&target1), Some(&target1)),
Some(target3.clone())
);
// Right moved forward
assert_eq!(
merge_ref_targets(index_ref, Some(&target1), Some(&target1), Some(&target3)),
Some(target3.clone())
);
// Left moved backward
assert_eq!(
merge_ref_targets(index_ref, Some(&target1), Some(&target3), Some(&target3)),
Some(target1.clone())
);
// Right moved backward
assert_eq!(
merge_ref_targets(index_ref, Some(&target3), Some(&target3), Some(&target1)),
Some(target1.clone())
);
// Left moved sideways
assert_eq!(
merge_ref_targets(index_ref, Some(&target4), Some(&target3), Some(&target3)),
Some(target4.clone())
);
// Right moved sideways
assert_eq!(
merge_ref_targets(index_ref, Some(&target3), Some(&target3), Some(&target4)),
Some(target4.clone())
);
// Both added same target
assert_eq!(
merge_ref_targets(index_ref, Some(&target3), None, Some(&target3)),
Some(target3.clone())
);
// Left added target, right added descendant target
assert_eq!(
merge_ref_targets(index_ref, Some(&target2), None, Some(&target3)),
Some(target3.clone())
);
// Right added target, left added descendant target
assert_eq!(
merge_ref_targets(index_ref, Some(&target3), None, Some(&target2)),
Some(target3.clone())
);
// Both moved forward to same target
assert_eq!(
merge_ref_targets(index_ref, Some(&target3), Some(&target1), Some(&target3)),
Some(target3.clone())
);
// Both moved forward, left moved further
assert_eq!(
merge_ref_targets(index_ref, Some(&target3), Some(&target1), Some(&target2)),
Some(target3.clone())
);
// Both moved forward, right moved further
assert_eq!(
merge_ref_targets(index_ref, Some(&target2), Some(&target1), Some(&target3)),
Some(target3.clone())
);
// Left and right moved forward to divergent targets
assert_eq!(
merge_ref_targets(index_ref, Some(&target3), Some(&target1), Some(&target4)),
Some(RefTarget::Conflict {
removes: vec![commit1.id().clone()],
adds: vec![commit3.id().clone(), commit4.id().clone()]
})
);
// Left moved back, right moved forward
assert_eq!(
merge_ref_targets(index_ref, Some(&target1), Some(&target2), Some(&target3)),
Some(RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit1.id().clone(), commit3.id().clone()]
})
);
// Right moved back, left moved forward
assert_eq!(
merge_ref_targets(index_ref, Some(&target3), Some(&target2), Some(&target1)),
Some(RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit3.id().clone(), commit1.id().clone()]
})
);
// Left removed
assert_eq!(
merge_ref_targets(index_ref, None, Some(&target3), Some(&target3)),
None
);
// Right removed
assert_eq!(
merge_ref_targets(index_ref, Some(&target3), Some(&target3), None),
None
);
// Left removed, right moved forward
assert_eq!(
merge_ref_targets(index_ref, None, Some(&target1), Some(&target3)),
Some(RefTarget::Conflict {
removes: vec![commit1.id().clone()],
adds: vec![commit3.id().clone()]
})
);
// Right removed, left moved forward
assert_eq!(
merge_ref_targets(index_ref, Some(&target3), Some(&target1), None),
Some(RefTarget::Conflict {
removes: vec![commit1.id().clone()],
adds: vec![commit3.id().clone()]
})
);
// Left became conflicted, right moved forward
assert_eq!(
merge_ref_targets(
index_ref,
Some(&RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit3.id().clone(), commit4.id().clone()]
}),
Some(&target1),
Some(&target3)
),
// TODO: "removes" should have commit 2, just like it does in the next test case
Some(RefTarget::Conflict {
removes: vec![commit1.id().clone()],
adds: vec![commit4.id().clone(), commit3.id().clone()]
})
);
// Right became conflicted, left moved forward
assert_eq!(
merge_ref_targets(
index_ref,
Some(&target3),
Some(&target1),
Some(&RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit3.id().clone(), commit4.id().clone()]
})
),
Some(RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit3.id().clone(), commit4.id().clone()]
})
);
// Existing conflict on left, right moves an "add" sideways
assert_eq!(
merge_ref_targets(
index_ref,
Some(&RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit3.id().clone(), commit4.id().clone()]
}),
Some(&target3),
Some(&target5)
),
Some(RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit4.id().clone(), commit5.id().clone()]
})
);
// Existing conflict on right, left moves an "add" sideways
assert_eq!(
merge_ref_targets(
index_ref,
Some(&target5),
Some(&target3),
Some(&RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit3.id().clone(), commit4.id().clone()]
})
),
Some(RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit5.id().clone(), commit4.id().clone()]
})
);
// Existing conflict on left, right moves an "add" backwards, past point of
// divergence
assert_eq!(
merge_ref_targets(
index_ref,
Some(&RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit3.id().clone(), commit4.id().clone()]
}),
Some(&target3),
Some(&target1)
),
Some(RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit4.id().clone(), commit1.id().clone()]
})
);
// Existing conflict on right, left moves an "add" backwards, past point of
// divergence
assert_eq!(
merge_ref_targets(
index_ref,
Some(&target1),
Some(&target3),
Some(&RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit3.id().clone(), commit4.id().clone()]
})
),
Some(RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit1.id().clone(), commit4.id().clone()]
})
);
// Existing conflict on left, right undoes one side of conflict
assert_eq!(
merge_ref_targets(
index_ref,
Some(&RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit3.id().clone(), commit4.id().clone()]
}),
Some(&target3),
Some(&target2)
),
Some(target4.clone())
);
// Existing conflict on right, left undoes one side of conflict
assert_eq!(
merge_ref_targets(
index_ref,
Some(&target2),
Some(&target3),
Some(&RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit3.id().clone(), commit4.id().clone()]
})
),
Some(target4)
);
// Existing conflict on left, right makes unrelated update
assert_eq!(
merge_ref_targets(
index_ref,
Some(&RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit3.id().clone(), commit4.id().clone()]
}),
Some(&target5),
Some(&target6)
),
Some(RefTarget::Conflict {
removes: vec![commit2.id().clone(), commit5.id().clone()],
adds: vec![
commit3.id().clone(),
commit4.id().clone(),
commit6.id().clone()
]
})
);
// Existing conflict on right, left makes unrelated update
assert_eq!(
merge_ref_targets(
index_ref,
Some(&target6),
Some(&target5),
Some(&RefTarget::Conflict {
removes: vec![commit2.id().clone()],
adds: vec![commit3.id().clone(), commit4.id().clone()]
})
),
Some(RefTarget::Conflict {
removes: vec![commit5.id().clone(), commit2.id().clone()],
adds: vec![
commit6.id().clone(),
commit3.id().clone(),
commit4.id().clone()
]
})
);
}