jj_lib/
refs.rs

1// Copyright 2021 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![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
27/// Compares `refs1` and `refs2` targets, yields entry if they differ.
28///
29/// `refs1` and `refs2` must be sorted by `K`.
30pub 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
43/// Compares remote `refs1` and `refs2` pairs, yields entry if they differ.
44///
45/// `refs1` and `refs2` must be sorted by `K`.
46pub 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
59/// Iterates local `refs1` and remote `refs2` pairs by name.
60///
61/// `refs1` and `refs2` must be sorted by `K`.
62pub 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
74/// Compares `ids1` and `ids2` commit ids, yields entry if they differ.
75///
76/// `ids1` and `ids2` must be sorted by `K`.
77pub 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    // Suppose left = [A - C + B], base = [B], right = [A], the merge result is
123    // [A - C + A], which can now be trivially resolved.
124    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        // TODO: Maybe better to try resolve_trivial() again, but the result is
129        // unreliable since merge_ref_targets_non_trivial() is order dependent.
130        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    // Just merge target and state fields separately. Strictly speaking, merging
141    // target-only change and state-only change shouldn't automatically mark the
142    // new target as tracking. However, many faulty merges will end up in local
143    // or remote target conflicts (since fast-forwardable move can be safely
144    // "tracked"), and the conflicts will require user intervention anyway. So
145    // there wouldn't be much reason to handle these merges precisely.
146    let target = merge_ref_targets(index, &left.target, &base.target, &right.target);
147    // Merged state shouldn't conflict atm since we only have two states, but if
148    // it does, keep the original state. The choice is arbitrary.
149    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    // If a "remove" is an ancestor of two different "adds" and one of the
165    // "adds" is an ancestor of the other, then pick the descendant.
166    for (add_index1, add1) in conflict.adds().enumerate() {
167        for (add_index2, add2) in conflict.adds().enumerate().skip(add_index1 + 1) {
168            // TODO: Instead of relying on the list order, maybe ((add1, add2), remove)
169            // combination should be somehow weighted?
170            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, // Absent ref can be considered a root
179            }) {
180                return Some((remove_index, add_index));
181            }
182        }
183    }
184
185    None
186}
187
188/// Pair of local and remote targets.
189#[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
210/// Figure out what changes (if any) need to be made to the remote when pushing
211/// this bookmark.
212pub 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        // This is not RemoteUntracked error since non-tracking remote bookmarks
315        // have no relation to local bookmarks, and there's nothing to push.
316        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}