1#![allow(missing_docs)]
16
17use itertools::EitherOrBoth;
18
19use crate::backend::CommitId;
20use crate::index::Index;
21use crate::merge::trivial_merge;
22use crate::merge::Merge;
23use crate::op_store::RefTarget;
24use crate::op_store::RemoteRef;
25
26pub fn diff_named_ref_targets<'a, 'b, K: Ord>(
30 refs1: impl IntoIterator<Item = (K, &'a RefTarget)>,
31 refs2: impl IntoIterator<Item = (K, &'b RefTarget)>,
32) -> impl Iterator<Item = (K, (&'a RefTarget, &'b RefTarget))> {
33 iter_named_pairs(
34 refs1,
35 refs2,
36 || RefTarget::absent_ref(),
37 || RefTarget::absent_ref(),
38 )
39 .filter(|(_, (target1, target2))| target1 != target2)
40}
41
42pub fn diff_named_remote_refs<'a, 'b, K: Ord>(
46 refs1: impl IntoIterator<Item = (K, &'a RemoteRef)>,
47 refs2: impl IntoIterator<Item = (K, &'b RemoteRef)>,
48) -> impl Iterator<Item = (K, (&'a RemoteRef, &'b RemoteRef))> {
49 iter_named_pairs(
50 refs1,
51 refs2,
52 || RemoteRef::absent_ref(),
53 || RemoteRef::absent_ref(),
54 )
55 .filter(|(_, (ref1, ref2))| ref1 != ref2)
56}
57
58pub fn iter_named_local_remote_refs<'a, 'b, K: Ord>(
62 refs1: impl IntoIterator<Item = (K, &'a RefTarget)>,
63 refs2: impl IntoIterator<Item = (K, &'b RemoteRef)>,
64) -> impl Iterator<Item = (K, (&'a RefTarget, &'b RemoteRef))> {
65 iter_named_pairs(
66 refs1,
67 refs2,
68 || RefTarget::absent_ref(),
69 || RemoteRef::absent_ref(),
70 )
71}
72
73pub fn diff_named_commit_ids<'a, 'b, K: Ord>(
77 ids1: impl IntoIterator<Item = (K, &'a CommitId)>,
78 ids2: impl IntoIterator<Item = (K, &'b CommitId)>,
79) -> impl Iterator<Item = (K, (Option<&'a CommitId>, Option<&'b CommitId>))> {
80 iter_named_pairs(
81 ids1.into_iter().map(|(k, v)| (k, Some(v))),
82 ids2.into_iter().map(|(k, v)| (k, Some(v))),
83 || None,
84 || None,
85 )
86 .filter(|(_, (target1, target2))| target1 != target2)
87}
88
89fn iter_named_pairs<K: Ord, V1, V2>(
90 refs1: impl IntoIterator<Item = (K, V1)>,
91 refs2: impl IntoIterator<Item = (K, V2)>,
92 absent_ref1: impl Fn() -> V1,
93 absent_ref2: impl Fn() -> V2,
94) -> impl Iterator<Item = (K, (V1, V2))> {
95 itertools::merge_join_by(refs1, refs2, |(name1, _), (name2, _)| name1.cmp(name2)).map(
96 move |entry| match entry {
97 EitherOrBoth::Both((name, target1), (_, target2)) => (name, (target1, target2)),
98 EitherOrBoth::Left((name, target1)) => (name, (target1, absent_ref2())),
99 EitherOrBoth::Right((name, target2)) => (name, (absent_ref1(), target2)),
100 },
101 )
102}
103
104pub fn merge_ref_targets(
105 index: &dyn Index,
106 left: &RefTarget,
107 base: &RefTarget,
108 right: &RefTarget,
109) -> RefTarget {
110 if let Some(&resolved) = trivial_merge(&[left, base, right]) {
111 return resolved.clone();
112 }
113
114 let mut merge = Merge::from_vec(vec![
115 left.as_merge().clone(),
116 base.as_merge().clone(),
117 right.as_merge().clone(),
118 ])
119 .flatten()
120 .simplify();
121 if let Some(resolved) = merge.resolve_trivial() {
124 RefTarget::resolved(resolved.clone())
125 } else {
126 merge_ref_targets_non_trivial(index, &mut merge);
127 RefTarget::from_merge(merge)
130 }
131}
132
133pub fn merge_remote_refs(
134 index: &dyn Index,
135 left: &RemoteRef,
136 base: &RemoteRef,
137 right: &RemoteRef,
138) -> RemoteRef {
139 let target = merge_ref_targets(index, &left.target, &base.target, &right.target);
146 let state = *trivial_merge(&[left.state, base.state, right.state]).unwrap_or(&base.state);
149 RemoteRef { target, state }
150}
151
152fn merge_ref_targets_non_trivial(index: &dyn Index, conflict: &mut Merge<Option<CommitId>>) {
153 while let Some((remove_index, add_index)) = find_pair_to_remove(index, conflict) {
154 conflict.swap_remove(remove_index, add_index);
155 }
156}
157
158fn find_pair_to_remove(
159 index: &dyn Index,
160 conflict: &Merge<Option<CommitId>>,
161) -> Option<(usize, usize)> {
162 for (add_index1, add1) in conflict.adds().enumerate() {
165 for (add_index2, add2) in conflict.adds().enumerate().skip(add_index1 + 1) {
166 let (add_index, add_id) = match (add1, add2) {
169 (Some(id1), Some(id2)) if id1 == id2 => (add_index1, id1),
170 (Some(id1), Some(id2)) if index.is_ancestor(id1, id2) => (add_index1, id1),
171 (Some(id1), Some(id2)) if index.is_ancestor(id2, id1) => (add_index2, id2),
172 _ => continue,
173 };
174 if let Some(remove_index) = conflict.removes().position(|remove| match remove {
175 Some(id) => index.is_ancestor(id, add_id),
176 None => true, }) {
178 return Some((remove_index, add_index));
179 }
180 }
181 }
182
183 None
184}
185
186#[derive(Clone, Copy, Debug, Eq, PartialEq)]
188pub struct LocalAndRemoteRef<'a> {
189 pub local_target: &'a RefTarget,
190 pub remote_ref: &'a RemoteRef,
191}
192
193#[derive(Debug, PartialEq, Eq, Clone, Hash)]
194pub struct BookmarkPushUpdate {
195 pub old_target: Option<CommitId>,
196 pub new_target: Option<CommitId>,
197}
198
199#[derive(Debug, PartialEq, Eq, Clone)]
200pub enum BookmarkPushAction {
201 Update(BookmarkPushUpdate),
202 AlreadyMatches,
203 LocalConflicted,
204 RemoteConflicted,
205 RemoteUntracked,
206}
207
208pub fn classify_bookmark_push_action(targets: LocalAndRemoteRef) -> BookmarkPushAction {
211 let local_target = targets.local_target;
212 let remote_target = targets.remote_ref.tracked_target();
213 if local_target == remote_target {
214 BookmarkPushAction::AlreadyMatches
215 } else if local_target.has_conflict() {
216 BookmarkPushAction::LocalConflicted
217 } else if remote_target.has_conflict() {
218 BookmarkPushAction::RemoteConflicted
219 } else if targets.remote_ref.is_present() && !targets.remote_ref.is_tracked() {
220 BookmarkPushAction::RemoteUntracked
221 } else {
222 BookmarkPushAction::Update(BookmarkPushUpdate {
223 old_target: remote_target.as_normal().cloned(),
224 new_target: local_target.as_normal().cloned(),
225 })
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use crate::op_store::RemoteRefState;
233
234 fn new_remote_ref(target: RefTarget) -> RemoteRef {
235 RemoteRef {
236 target,
237 state: RemoteRefState::New,
238 }
239 }
240
241 fn tracked_remote_ref(target: RefTarget) -> RemoteRef {
242 RemoteRef {
243 target,
244 state: RemoteRefState::Tracked,
245 }
246 }
247
248 #[test]
249 fn test_classify_bookmark_push_action_unchanged() {
250 let commit_id1 = CommitId::from_hex("11");
251 let targets = LocalAndRemoteRef {
252 local_target: &RefTarget::normal(commit_id1.clone()),
253 remote_ref: &tracked_remote_ref(RefTarget::normal(commit_id1)),
254 };
255 assert_eq!(
256 classify_bookmark_push_action(targets),
257 BookmarkPushAction::AlreadyMatches
258 );
259 }
260
261 #[test]
262 fn test_classify_bookmark_push_action_added() {
263 let commit_id1 = CommitId::from_hex("11");
264 let targets = LocalAndRemoteRef {
265 local_target: &RefTarget::normal(commit_id1.clone()),
266 remote_ref: RemoteRef::absent_ref(),
267 };
268 assert_eq!(
269 classify_bookmark_push_action(targets),
270 BookmarkPushAction::Update(BookmarkPushUpdate {
271 old_target: None,
272 new_target: Some(commit_id1),
273 })
274 );
275 }
276
277 #[test]
278 fn test_classify_bookmark_push_action_removed() {
279 let commit_id1 = CommitId::from_hex("11");
280 let targets = LocalAndRemoteRef {
281 local_target: RefTarget::absent_ref(),
282 remote_ref: &tracked_remote_ref(RefTarget::normal(commit_id1.clone())),
283 };
284 assert_eq!(
285 classify_bookmark_push_action(targets),
286 BookmarkPushAction::Update(BookmarkPushUpdate {
287 old_target: Some(commit_id1),
288 new_target: None,
289 })
290 );
291 }
292
293 #[test]
294 fn test_classify_bookmark_push_action_updated() {
295 let commit_id1 = CommitId::from_hex("11");
296 let commit_id2 = CommitId::from_hex("22");
297 let targets = LocalAndRemoteRef {
298 local_target: &RefTarget::normal(commit_id2.clone()),
299 remote_ref: &tracked_remote_ref(RefTarget::normal(commit_id1.clone())),
300 };
301 assert_eq!(
302 classify_bookmark_push_action(targets),
303 BookmarkPushAction::Update(BookmarkPushUpdate {
304 old_target: Some(commit_id1),
305 new_target: Some(commit_id2),
306 })
307 );
308 }
309
310 #[test]
311 fn test_classify_bookmark_push_action_removed_untracked() {
312 let commit_id1 = CommitId::from_hex("11");
315 let targets = LocalAndRemoteRef {
316 local_target: RefTarget::absent_ref(),
317 remote_ref: &new_remote_ref(RefTarget::normal(commit_id1.clone())),
318 };
319 assert_eq!(
320 classify_bookmark_push_action(targets),
321 BookmarkPushAction::AlreadyMatches
322 );
323 }
324
325 #[test]
326 fn test_classify_bookmark_push_action_updated_untracked() {
327 let commit_id1 = CommitId::from_hex("11");
328 let commit_id2 = CommitId::from_hex("22");
329 let targets = LocalAndRemoteRef {
330 local_target: &RefTarget::normal(commit_id2.clone()),
331 remote_ref: &new_remote_ref(RefTarget::normal(commit_id1.clone())),
332 };
333 assert_eq!(
334 classify_bookmark_push_action(targets),
335 BookmarkPushAction::RemoteUntracked
336 );
337 }
338
339 #[test]
340 fn test_classify_bookmark_push_action_local_conflicted() {
341 let commit_id1 = CommitId::from_hex("11");
342 let commit_id2 = CommitId::from_hex("22");
343 let targets = LocalAndRemoteRef {
344 local_target: &RefTarget::from_legacy_form([], [commit_id1.clone(), commit_id2]),
345 remote_ref: &tracked_remote_ref(RefTarget::normal(commit_id1)),
346 };
347 assert_eq!(
348 classify_bookmark_push_action(targets),
349 BookmarkPushAction::LocalConflicted
350 );
351 }
352
353 #[test]
354 fn test_classify_bookmark_push_action_remote_conflicted() {
355 let commit_id1 = CommitId::from_hex("11");
356 let commit_id2 = CommitId::from_hex("22");
357 let targets = LocalAndRemoteRef {
358 local_target: &RefTarget::normal(commit_id1.clone()),
359 remote_ref: &tracked_remote_ref(RefTarget::from_legacy_form(
360 [],
361 [commit_id1, commit_id2],
362 )),
363 };
364 assert_eq!(
365 classify_bookmark_push_action(targets),
366 BookmarkPushAction::RemoteConflicted
367 );
368 }
369}