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