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