2022-11-26 23:57:50 +00:00
|
|
|
// Copyright 2021 The Jujutsu Authors
|
2021-07-19 06:04:21 +00:00
|
|
|
//
|
|
|
|
// 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.
|
|
|
|
|
2023-07-10 15:17:00 +00:00
|
|
|
#![allow(missing_docs)]
|
|
|
|
|
2023-10-05 18:50:35 +00:00
|
|
|
use itertools::EitherOrBoth;
|
|
|
|
|
2021-09-12 06:52:38 +00:00
|
|
|
use crate::backend::CommitId;
|
2023-02-13 05:57:49 +00:00
|
|
|
use crate::index::Index;
|
2024-08-22 18:18:15 +00:00
|
|
|
use crate::merge::trivial_merge;
|
|
|
|
use crate::merge::Merge;
|
|
|
|
use crate::op_store::RefTarget;
|
|
|
|
use crate::op_store::RemoteRef;
|
2021-07-19 06:04:21 +00:00
|
|
|
|
2023-10-05 18:50:35 +00:00
|
|
|
/// Compares `refs1` and `refs2` targets, yields entry if they differ.
|
|
|
|
///
|
|
|
|
/// `refs1` and `refs2` must be sorted by `K`.
|
2023-10-20 23:00:07 +00:00
|
|
|
pub fn diff_named_ref_targets<'a, 'b, K: Ord>(
|
2023-10-05 18:50:35 +00:00
|
|
|
refs1: impl IntoIterator<Item = (K, &'a RefTarget)>,
|
|
|
|
refs2: impl IntoIterator<Item = (K, &'b RefTarget)>,
|
|
|
|
) -> impl Iterator<Item = (K, (&'a RefTarget, &'b RefTarget))> {
|
2023-10-20 22:37:57 +00:00
|
|
|
iter_named_pairs(
|
|
|
|
refs1,
|
|
|
|
refs2,
|
|
|
|
|| RefTarget::absent_ref(),
|
|
|
|
|| RefTarget::absent_ref(),
|
|
|
|
)
|
|
|
|
.filter(|(_, (target1, target2))| target1 != target2)
|
2023-10-05 18:50:35 +00:00
|
|
|
}
|
|
|
|
|
2023-10-20 07:45:22 +00:00
|
|
|
/// Compares remote `refs1` and `refs2` pairs, yields entry if they differ.
|
|
|
|
///
|
|
|
|
/// `refs1` and `refs2` must be sorted by `K`.
|
|
|
|
pub fn diff_named_remote_refs<'a, 'b, K: Ord>(
|
|
|
|
refs1: impl IntoIterator<Item = (K, &'a RemoteRef)>,
|
|
|
|
refs2: impl IntoIterator<Item = (K, &'b RemoteRef)>,
|
|
|
|
) -> impl Iterator<Item = (K, (&'a RemoteRef, &'b RemoteRef))> {
|
|
|
|
iter_named_pairs(
|
|
|
|
refs1,
|
|
|
|
refs2,
|
|
|
|
|| RemoteRef::absent_ref(),
|
|
|
|
|| RemoteRef::absent_ref(),
|
|
|
|
)
|
|
|
|
.filter(|(_, (ref1, ref2))| ref1 != ref2)
|
|
|
|
}
|
|
|
|
|
2023-10-20 22:37:57 +00:00
|
|
|
/// Iterates local `refs1` and remote `refs2` pairs by name.
|
2023-10-05 18:50:35 +00:00
|
|
|
///
|
|
|
|
/// `refs1` and `refs2` must be sorted by `K`.
|
2023-10-20 22:37:57 +00:00
|
|
|
pub fn iter_named_local_remote_refs<'a, 'b, K: Ord>(
|
2023-10-05 18:50:35 +00:00
|
|
|
refs1: impl IntoIterator<Item = (K, &'a RefTarget)>,
|
2023-10-20 22:37:57 +00:00
|
|
|
refs2: impl IntoIterator<Item = (K, &'b RemoteRef)>,
|
|
|
|
) -> impl Iterator<Item = (K, (&'a RefTarget, &'b RemoteRef))> {
|
|
|
|
iter_named_pairs(
|
|
|
|
refs1,
|
|
|
|
refs2,
|
|
|
|
|| RefTarget::absent_ref(),
|
|
|
|
|| RemoteRef::absent_ref(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn iter_named_pairs<K: Ord, V1, V2>(
|
|
|
|
refs1: impl IntoIterator<Item = (K, V1)>,
|
|
|
|
refs2: impl IntoIterator<Item = (K, V2)>,
|
|
|
|
absent_ref1: impl Fn() -> V1,
|
|
|
|
absent_ref2: impl Fn() -> V2,
|
|
|
|
) -> impl Iterator<Item = (K, (V1, V2))> {
|
|
|
|
itertools::merge_join_by(refs1, refs2, |(name1, _), (name2, _)| name1.cmp(name2)).map(
|
|
|
|
move |entry| match entry {
|
2023-10-05 18:50:35 +00:00
|
|
|
EitherOrBoth::Both((name, target1), (_, target2)) => (name, (target1, target2)),
|
2023-10-20 22:37:57 +00:00
|
|
|
EitherOrBoth::Left((name, target1)) => (name, (target1, absent_ref2())),
|
|
|
|
EitherOrBoth::Right((name, target2)) => (name, (absent_ref1(), target2)),
|
|
|
|
},
|
|
|
|
)
|
2023-10-05 18:50:35 +00:00
|
|
|
}
|
|
|
|
|
2021-07-19 06:04:21 +00:00
|
|
|
pub fn merge_ref_targets(
|
2023-02-13 05:57:49 +00:00
|
|
|
index: &dyn Index,
|
2023-07-12 22:20:44 +00:00
|
|
|
left: &RefTarget,
|
|
|
|
base: &RefTarget,
|
|
|
|
right: &RefTarget,
|
|
|
|
) -> RefTarget {
|
2023-07-12 19:17:40 +00:00
|
|
|
if let Some(&resolved) = trivial_merge(&[base], &[left, right]) {
|
|
|
|
return resolved.clone();
|
2023-05-14 13:34:55 +00:00
|
|
|
}
|
2021-07-19 06:04:21 +00:00
|
|
|
|
2023-11-05 02:15:26 +00:00
|
|
|
let mut merge = Merge::from_vec(vec![
|
|
|
|
left.as_merge().clone(),
|
|
|
|
base.as_merge().clone(),
|
|
|
|
right.as_merge().clone(),
|
|
|
|
])
|
2023-07-07 10:57:34 +00:00
|
|
|
.flatten()
|
|
|
|
.simplify();
|
2024-09-01 04:44:05 +00:00
|
|
|
// Suppose left = [A - C + B], base = [B], right = [A], the merge result is
|
|
|
|
// [A - C + A], which can now be trivially resolved.
|
|
|
|
if let Some(resolved) = merge.resolve_trivial() {
|
|
|
|
RefTarget::resolved(resolved.clone())
|
|
|
|
} else {
|
2023-11-06 08:26:41 +00:00
|
|
|
merge_ref_targets_non_trivial(index, &mut merge);
|
2024-09-01 04:44:05 +00:00
|
|
|
// TODO: Maybe better to try resolve_trivial() again, but the result is
|
|
|
|
// unreliable since merge_ref_targets_non_trivial() is order dependent.
|
|
|
|
RefTarget::from_merge(merge)
|
2023-05-14 13:34:55 +00:00
|
|
|
}
|
2023-07-07 10:57:34 +00:00
|
|
|
}
|
2021-07-19 06:04:21 +00:00
|
|
|
|
2023-10-20 07:55:24 +00:00
|
|
|
pub fn merge_remote_refs(
|
|
|
|
index: &dyn Index,
|
|
|
|
left: &RemoteRef,
|
|
|
|
base: &RemoteRef,
|
|
|
|
right: &RemoteRef,
|
|
|
|
) -> RemoteRef {
|
|
|
|
// Just merge target and state fields separately. Strictly speaking, merging
|
|
|
|
// target-only change and state-only change shouldn't automatically mark the
|
|
|
|
// new target as tracking. However, many faulty merges will end up in local
|
|
|
|
// or remote target conflicts (since fast-forwardable move can be safely
|
|
|
|
// "tracked"), and the conflicts will require user intervention anyway. So
|
|
|
|
// there wouldn't be much reason to handle these merges precisely.
|
|
|
|
let target = merge_ref_targets(index, &left.target, &base.target, &right.target);
|
|
|
|
// Merged state shouldn't conflict atm since we only have two states, but if
|
|
|
|
// it does, keep the original state. The choice is arbitrary.
|
|
|
|
let state = *trivial_merge(&[base.state], &[left.state, right.state]).unwrap_or(&base.state);
|
|
|
|
RemoteRef { target, state }
|
|
|
|
}
|
|
|
|
|
2023-11-06 08:26:41 +00:00
|
|
|
fn merge_ref_targets_non_trivial(index: &dyn Index, conflict: &mut Merge<Option<CommitId>>) {
|
|
|
|
while let Some((remove_index, add_index)) = find_pair_to_remove(index, conflict) {
|
|
|
|
conflict.swap_remove(remove_index, add_index);
|
2023-05-14 13:34:55 +00:00
|
|
|
}
|
2021-07-19 06:04:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn find_pair_to_remove(
|
2023-02-13 05:57:49 +00:00
|
|
|
index: &dyn Index,
|
2023-11-06 08:26:41 +00:00
|
|
|
conflict: &Merge<Option<CommitId>>,
|
2023-07-07 12:11:16 +00:00
|
|
|
) -> Option<(usize, usize)> {
|
2021-07-19 06:04:21 +00:00
|
|
|
// If a "remove" is an ancestor of two different "adds" and one of the
|
|
|
|
// "adds" is an ancestor of the other, then pick the descendant.
|
2023-11-06 08:26:41 +00:00
|
|
|
for (add_index1, add1) in conflict.adds().enumerate() {
|
|
|
|
for (add_index2, add2) in conflict.adds().enumerate().skip(add_index1 + 1) {
|
2023-07-07 12:11:16 +00:00
|
|
|
// TODO: Instead of relying on the list order, maybe ((add1, add2), remove)
|
|
|
|
// combination should be somehow weighted?
|
|
|
|
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,
|
2023-07-07 15:30:48 +00:00
|
|
|
};
|
2023-11-06 08:26:41 +00:00
|
|
|
if let Some(remove_index) = conflict.removes().position(|remove| match remove {
|
2023-07-07 12:11:16 +00:00
|
|
|
Some(id) => index.is_ancestor(id, add_id),
|
|
|
|
None => true, // Absent ref can be considered a root
|
|
|
|
}) {
|
|
|
|
return Some((remove_index, add_index));
|
2021-07-19 06:04:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
|
|
|
}
|
2021-09-11 18:14:22 +00:00
|
|
|
|
2024-02-07 01:30:21 +00:00
|
|
|
/// Pair of local and remote targets.
|
2023-10-06 07:17:26 +00:00
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
2024-02-07 01:30:21 +00:00
|
|
|
pub struct LocalAndRemoteRef<'a> {
|
2023-10-06 07:17:26 +00:00
|
|
|
pub local_target: &'a RefTarget,
|
2023-10-14 14:54:35 +00:00
|
|
|
pub remote_ref: &'a RemoteRef,
|
2023-10-06 07:17:26 +00:00
|
|
|
}
|
|
|
|
|
2022-12-19 02:25:04 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
2021-09-12 00:09:48 +00:00
|
|
|
pub struct BranchPushUpdate {
|
|
|
|
pub old_target: Option<CommitId>,
|
|
|
|
pub new_target: Option<CommitId>,
|
|
|
|
}
|
|
|
|
|
2021-09-11 18:14:22 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
|
|
pub enum BranchPushAction {
|
2021-09-12 00:09:48 +00:00
|
|
|
Update(BranchPushUpdate),
|
2021-09-11 18:14:22 +00:00
|
|
|
AlreadyMatches,
|
|
|
|
LocalConflicted,
|
|
|
|
RemoteConflicted,
|
2023-10-15 17:43:45 +00:00
|
|
|
RemoteUntracked,
|
2021-09-11 18:14:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Figure out what changes (if any) need to be made to the remote when pushing
|
2024-08-21 19:59:15 +00:00
|
|
|
/// this bookmark.
|
|
|
|
pub fn classify_bookmark_push_action(targets: LocalAndRemoteRef) -> BranchPushAction {
|
2023-10-06 08:24:03 +00:00
|
|
|
let local_target = targets.local_target;
|
2023-10-14 14:54:35 +00:00
|
|
|
let remote_target = targets.remote_ref.tracking_target();
|
2023-07-11 17:48:55 +00:00
|
|
|
if local_target == remote_target {
|
|
|
|
BranchPushAction::AlreadyMatches
|
2023-07-19 12:31:49 +00:00
|
|
|
} else if local_target.has_conflict() {
|
2023-07-11 17:48:55 +00:00
|
|
|
BranchPushAction::LocalConflicted
|
2023-07-19 12:31:49 +00:00
|
|
|
} else if remote_target.has_conflict() {
|
2023-07-11 17:48:55 +00:00
|
|
|
BranchPushAction::RemoteConflicted
|
2023-10-15 17:43:45 +00:00
|
|
|
} else if targets.remote_ref.is_present() && !targets.remote_ref.is_tracking() {
|
|
|
|
BranchPushAction::RemoteUntracked
|
2023-07-11 17:48:55 +00:00
|
|
|
} else {
|
|
|
|
BranchPushAction::Update(BranchPushUpdate {
|
|
|
|
old_target: remote_target.as_normal().cloned(),
|
|
|
|
new_target: local_target.as_normal().cloned(),
|
|
|
|
})
|
2021-09-11 18:14:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2023-10-14 14:54:35 +00:00
|
|
|
use crate::op_store::RemoteRefState;
|
|
|
|
|
2023-10-14 14:54:35 +00:00
|
|
|
fn new_remote_ref(target: RefTarget) -> RemoteRef {
|
|
|
|
RemoteRef {
|
|
|
|
target,
|
|
|
|
state: RemoteRefState::New,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-14 14:54:35 +00:00
|
|
|
fn tracking_remote_ref(target: RefTarget) -> RemoteRef {
|
|
|
|
RemoteRef {
|
|
|
|
target,
|
|
|
|
state: RemoteRefState::Tracking,
|
|
|
|
}
|
|
|
|
}
|
2021-09-11 18:14:22 +00:00
|
|
|
|
|
|
|
#[test]
|
2024-08-21 19:59:15 +00:00
|
|
|
fn test_classify_bookmark_push_action_unchanged() {
|
2021-09-11 18:14:22 +00:00
|
|
|
let commit_id1 = CommitId::from_hex("11");
|
2024-02-07 01:30:21 +00:00
|
|
|
let targets = LocalAndRemoteRef {
|
2023-10-06 08:24:03 +00:00
|
|
|
local_target: &RefTarget::normal(commit_id1.clone()),
|
2023-10-14 14:54:35 +00:00
|
|
|
remote_ref: &tracking_remote_ref(RefTarget::normal(commit_id1)),
|
2021-09-11 18:14:22 +00:00
|
|
|
};
|
|
|
|
assert_eq!(
|
2024-08-21 19:59:15 +00:00
|
|
|
classify_bookmark_push_action(targets),
|
2021-09-11 18:14:22 +00:00
|
|
|
BranchPushAction::AlreadyMatches
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2024-08-21 19:59:15 +00:00
|
|
|
fn test_classify_bookmark_push_action_added() {
|
2021-09-11 18:14:22 +00:00
|
|
|
let commit_id1 = CommitId::from_hex("11");
|
2024-02-07 01:30:21 +00:00
|
|
|
let targets = LocalAndRemoteRef {
|
2023-10-06 08:24:03 +00:00
|
|
|
local_target: &RefTarget::normal(commit_id1.clone()),
|
2023-10-14 14:54:35 +00:00
|
|
|
remote_ref: RemoteRef::absent_ref(),
|
2021-09-11 18:14:22 +00:00
|
|
|
};
|
|
|
|
assert_eq!(
|
2024-08-21 19:59:15 +00:00
|
|
|
classify_bookmark_push_action(targets),
|
2021-09-12 00:09:48 +00:00
|
|
|
BranchPushAction::Update(BranchPushUpdate {
|
2021-09-11 18:14:22 +00:00
|
|
|
old_target: None,
|
|
|
|
new_target: Some(commit_id1),
|
2021-09-12 00:09:48 +00:00
|
|
|
})
|
2021-09-11 18:14:22 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2024-08-21 19:59:15 +00:00
|
|
|
fn test_classify_bookmark_push_action_removed() {
|
2021-09-11 18:14:22 +00:00
|
|
|
let commit_id1 = CommitId::from_hex("11");
|
2024-02-07 01:30:21 +00:00
|
|
|
let targets = LocalAndRemoteRef {
|
2023-10-06 08:24:03 +00:00
|
|
|
local_target: RefTarget::absent_ref(),
|
2023-10-14 14:54:35 +00:00
|
|
|
remote_ref: &tracking_remote_ref(RefTarget::normal(commit_id1.clone())),
|
2021-09-11 18:14:22 +00:00
|
|
|
};
|
|
|
|
assert_eq!(
|
2024-08-21 19:59:15 +00:00
|
|
|
classify_bookmark_push_action(targets),
|
2021-09-12 00:09:48 +00:00
|
|
|
BranchPushAction::Update(BranchPushUpdate {
|
2021-09-11 18:14:22 +00:00
|
|
|
old_target: Some(commit_id1),
|
|
|
|
new_target: None,
|
2021-09-12 00:09:48 +00:00
|
|
|
})
|
2021-09-11 18:14:22 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2024-08-21 19:59:15 +00:00
|
|
|
fn test_classify_bookmark_push_action_updated() {
|
2021-09-11 18:14:22 +00:00
|
|
|
let commit_id1 = CommitId::from_hex("11");
|
|
|
|
let commit_id2 = CommitId::from_hex("22");
|
2024-02-07 01:30:21 +00:00
|
|
|
let targets = LocalAndRemoteRef {
|
2023-10-06 08:24:03 +00:00
|
|
|
local_target: &RefTarget::normal(commit_id2.clone()),
|
2023-10-14 14:54:35 +00:00
|
|
|
remote_ref: &tracking_remote_ref(RefTarget::normal(commit_id1.clone())),
|
2021-09-11 18:14:22 +00:00
|
|
|
};
|
|
|
|
assert_eq!(
|
2024-08-21 19:59:15 +00:00
|
|
|
classify_bookmark_push_action(targets),
|
2021-09-12 00:09:48 +00:00
|
|
|
BranchPushAction::Update(BranchPushUpdate {
|
2021-09-11 18:14:22 +00:00
|
|
|
old_target: Some(commit_id1),
|
|
|
|
new_target: Some(commit_id2),
|
2021-09-12 00:09:48 +00:00
|
|
|
})
|
2021-09-11 18:14:22 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-10-14 14:54:35 +00:00
|
|
|
#[test]
|
2024-08-21 19:59:15 +00:00
|
|
|
fn test_classify_bookmark_push_action_removed_untracked() {
|
|
|
|
// This is not RemoteUntracked error since non-tracking remote bookmarks
|
|
|
|
// have no relation to local bookmarks, and there's nothing to push.
|
2023-10-14 14:54:35 +00:00
|
|
|
let commit_id1 = CommitId::from_hex("11");
|
2024-02-07 01:30:21 +00:00
|
|
|
let targets = LocalAndRemoteRef {
|
2023-10-14 14:54:35 +00:00
|
|
|
local_target: RefTarget::absent_ref(),
|
|
|
|
remote_ref: &new_remote_ref(RefTarget::normal(commit_id1.clone())),
|
|
|
|
};
|
|
|
|
assert_eq!(
|
2024-08-21 19:59:15 +00:00
|
|
|
classify_bookmark_push_action(targets),
|
2023-10-14 14:54:35 +00:00
|
|
|
BranchPushAction::AlreadyMatches
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2024-08-21 19:59:15 +00:00
|
|
|
fn test_classify_bookmark_push_action_updated_untracked() {
|
2023-10-14 14:54:35 +00:00
|
|
|
let commit_id1 = CommitId::from_hex("11");
|
|
|
|
let commit_id2 = CommitId::from_hex("22");
|
2024-02-07 01:30:21 +00:00
|
|
|
let targets = LocalAndRemoteRef {
|
2023-10-14 14:54:35 +00:00
|
|
|
local_target: &RefTarget::normal(commit_id2.clone()),
|
|
|
|
remote_ref: &new_remote_ref(RefTarget::normal(commit_id1.clone())),
|
|
|
|
};
|
|
|
|
assert_eq!(
|
2024-08-21 19:59:15 +00:00
|
|
|
classify_bookmark_push_action(targets),
|
2023-10-15 17:43:45 +00:00
|
|
|
BranchPushAction::RemoteUntracked
|
2023-10-14 14:54:35 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-09-11 18:14:22 +00:00
|
|
|
#[test]
|
2024-08-21 19:59:15 +00:00
|
|
|
fn test_classify_bookmark_push_action_local_conflicted() {
|
2021-09-11 18:14:22 +00:00
|
|
|
let commit_id1 = CommitId::from_hex("11");
|
|
|
|
let commit_id2 = CommitId::from_hex("22");
|
2024-02-07 01:30:21 +00:00
|
|
|
let targets = LocalAndRemoteRef {
|
2023-10-06 08:24:03 +00:00
|
|
|
local_target: &RefTarget::from_legacy_form([], [commit_id1.clone(), commit_id2]),
|
2023-10-14 14:54:35 +00:00
|
|
|
remote_ref: &tracking_remote_ref(RefTarget::normal(commit_id1)),
|
2021-09-11 18:14:22 +00:00
|
|
|
};
|
|
|
|
assert_eq!(
|
2024-08-21 19:59:15 +00:00
|
|
|
classify_bookmark_push_action(targets),
|
2021-09-11 18:14:22 +00:00
|
|
|
BranchPushAction::LocalConflicted
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2024-08-21 19:59:15 +00:00
|
|
|
fn test_classify_bookmark_push_action_remote_conflicted() {
|
2021-09-11 18:14:22 +00:00
|
|
|
let commit_id1 = CommitId::from_hex("11");
|
|
|
|
let commit_id2 = CommitId::from_hex("22");
|
2024-02-07 01:30:21 +00:00
|
|
|
let targets = LocalAndRemoteRef {
|
2023-10-06 08:24:03 +00:00
|
|
|
local_target: &RefTarget::normal(commit_id1.clone()),
|
2023-10-14 14:54:35 +00:00
|
|
|
remote_ref: &tracking_remote_ref(RefTarget::from_legacy_form(
|
|
|
|
[],
|
|
|
|
[commit_id1, commit_id2],
|
|
|
|
)),
|
2021-09-11 18:14:22 +00:00
|
|
|
};
|
|
|
|
assert_eq!(
|
2024-08-21 19:59:15 +00:00
|
|
|
classify_bookmark_push_action(targets),
|
2021-09-11 18:14:22 +00:00
|
|
|
BranchPushAction::RemoteConflicted
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|