#![allow(missing_docs)]
use crate::backend::CommitId;
use crate::conflicts::Conflict;
use crate::index::Index;
use crate::merge::trivial_merge;
use crate::op_store::{BranchTarget, RefTarget, RefTargetExt as _};
pub fn merge_ref_targets(
index: &dyn Index,
left: Option<&RefTarget>,
base: Option<&RefTarget>,
right: Option<&RefTarget>,
) -> Option<RefTarget> {
if let Some(resolved) = trivial_merge(&[base], &[left, right]) {
return resolved.cloned();
}
let conflict = Conflict::new(
vec![ref_target_to_conflict(base)],
vec![ref_target_to_conflict(left), ref_target_to_conflict(right)],
)
.flatten()
.simplify();
if conflict.as_resolved().is_some() {
conflict_to_ref_target(conflict)
} else {
let conflict = merge_ref_targets_non_trivial(index, conflict);
conflict_to_ref_target(conflict)
}
}
fn conflict_to_ref_target(conflict: Conflict<Option<CommitId>>) -> Option<RefTarget> {
match conflict.as_resolved() {
Some(Some(id)) => Some(RefTarget::Normal(id.clone())),
Some(None) => None, None => {
let (removes, adds) = conflict.into_legacy_form();
Some(RefTarget::Conflict { removes, adds })
}
}
}
fn ref_target_to_conflict(maybe_target: Option<&RefTarget>) -> Conflict<Option<CommitId>> {
if let Some(target) = maybe_target {
Conflict::from_legacy_form(
target.removes().iter().cloned(),
target.adds().iter().cloned(),
)
} else {
Conflict::resolved(None) }
}
fn merge_ref_targets_non_trivial(
index: &dyn Index,
conflict: Conflict<Option<CommitId>>,
) -> Conflict<Option<CommitId>> {
let (mut removes, mut adds) = conflict.take();
while let Some((remove_index, add_index)) = find_pair_to_remove(index, &removes, &adds) {
removes.remove(remove_index);
adds.remove(add_index);
}
Conflict::new(removes, adds)
}
fn find_pair_to_remove(
index: &dyn Index,
removes: &[Option<CommitId>],
adds: &[Option<CommitId>],
) -> Option<(usize, usize)> {
for (add_index1, add1) in adds.iter().enumerate() {
for (add_index2, add2) in adds.iter().enumerate().skip(add_index1 + 1) {
let (add_index, add_id) = match (add1, add2) {
(Some(id1), Some(id2)) if id1 == id2 => (add_index1, id1),
(Some(id1), Some(id2)) if index.is_ancestor(id1, id2) => (add_index1, id1),
(Some(id1), Some(id2)) if index.is_ancestor(id2, id1) => (add_index2, id2),
_ => continue,
};
if let Some(remove_index) = removes.iter().position(|remove| match remove {
Some(id) => index.is_ancestor(id, add_id),
None => true, }) {
return Some((remove_index, add_index));
}
}
}
None
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct BranchPushUpdate {
pub old_target: Option<CommitId>,
pub new_target: Option<CommitId>,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum BranchPushAction {
Update(BranchPushUpdate),
AlreadyMatches,
LocalConflicted,
RemoteConflicted,
}
pub fn classify_branch_push_action(
branch_target: &BranchTarget,
remote_name: &str,
) -> BranchPushAction {
let local_target = branch_target.local_target.as_ref();
let remote_target = branch_target.remote_targets.get(remote_name);
if local_target == remote_target {
BranchPushAction::AlreadyMatches
} else if local_target.is_conflict() {
BranchPushAction::LocalConflicted
} else if remote_target.is_conflict() {
BranchPushAction::RemoteConflicted
} else {
BranchPushAction::Update(BranchPushUpdate {
old_target: remote_target.as_normal().cloned(),
new_target: local_target.as_normal().cloned(),
})
}
}
#[cfg(test)]
mod tests {
use maplit::btreemap;
use super::*;
use crate::backend::ObjectId;
#[test]
fn test_classify_branch_push_action_unchanged() {
let commit_id1 = CommitId::from_hex("11");
let branch = BranchTarget {
local_target: RefTarget::normal(commit_id1.clone()),
remote_targets: btreemap! {
"origin".to_string() => RefTarget::normal(commit_id1).unwrap(),
},
};
assert_eq!(
classify_branch_push_action(&branch, "origin"),
BranchPushAction::AlreadyMatches
);
}
#[test]
fn test_classify_branch_push_action_added() {
let commit_id1 = CommitId::from_hex("11");
let branch = BranchTarget {
local_target: RefTarget::normal(commit_id1.clone()),
remote_targets: btreemap! {},
};
assert_eq!(
classify_branch_push_action(&branch, "origin"),
BranchPushAction::Update(BranchPushUpdate {
old_target: None,
new_target: Some(commit_id1),
})
);
}
#[test]
fn test_classify_branch_push_action_removed() {
let commit_id1 = CommitId::from_hex("11");
let branch = BranchTarget {
local_target: None,
remote_targets: btreemap! {
"origin".to_string() => RefTarget::normal(commit_id1.clone()).unwrap(),
},
};
assert_eq!(
classify_branch_push_action(&branch, "origin"),
BranchPushAction::Update(BranchPushUpdate {
old_target: Some(commit_id1),
new_target: None,
})
);
}
#[test]
fn test_classify_branch_push_action_updated() {
let commit_id1 = CommitId::from_hex("11");
let commit_id2 = CommitId::from_hex("22");
let branch = BranchTarget {
local_target: RefTarget::normal(commit_id2.clone()),
remote_targets: btreemap! {
"origin".to_string() => RefTarget::normal(commit_id1.clone()).unwrap(),
},
};
assert_eq!(
classify_branch_push_action(&branch, "origin"),
BranchPushAction::Update(BranchPushUpdate {
old_target: Some(commit_id1),
new_target: Some(commit_id2),
})
);
}
#[test]
fn test_classify_branch_push_action_local_conflicted() {
let commit_id1 = CommitId::from_hex("11");
let commit_id2 = CommitId::from_hex("22");
let branch = BranchTarget {
local_target: RefTarget::from_legacy_form([], [commit_id1.clone(), commit_id2]),
remote_targets: btreemap! {
"origin".to_string() => RefTarget::normal(commit_id1).unwrap(),
},
};
assert_eq!(
classify_branch_push_action(&branch, "origin"),
BranchPushAction::LocalConflicted
);
}
#[test]
fn test_classify_branch_push_action_remote_conflicted() {
let commit_id1 = CommitId::from_hex("11");
let commit_id2 = CommitId::from_hex("22");
let branch = BranchTarget {
local_target: RefTarget::normal(commit_id1.clone()),
remote_targets: btreemap! {
"origin".to_string() => RefTarget::from_legacy_form(
[],
[commit_id1, commit_id2],
).unwrap(),
},
};
assert_eq!(
classify_branch_push_action(&branch, "origin"),
BranchPushAction::RemoteConflicted
);
}
}