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