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