Skip to main content

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::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
32/// Compares `refs1` and `refs2` targets, yields entry if they differ.
33///
34/// `refs1` and `refs2` must be sorted by `K`.
35pub 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
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(refs1, refs2, || &ABSENT_REMOTE_REF, || &ABSENT_REMOTE_REF)
51        .filter(|(_, (ref1, ref2))| ref1 != ref2)
52}
53
54/// Iterates local `refs1` and remote `refs2` pairs by name.
55///
56/// `refs1` and `refs2` must be sorted by `K`.
57pub 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
64/// Compares `ids1` and `ids2` commit ids, yields entry if they differ.
65///
66/// `ids1` and `ids2` must be sorted by `K`.
67pub 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    // Suppose left = [A - C + B], base = [B], right = [A], the merge result is
109    // [A - C + A], which can now be trivially resolved.
110    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        // TODO: Maybe better to try resolve_trivial() again, but the result is
115        // unreliable since merge_ref_targets_non_trivial() is order dependent.
116        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    // Just merge target and state fields separately. Strictly speaking, merging
127    // target-only change and state-only change shouldn't automatically mark the
128    // new target as tracking. However, many faulty merges will end up in local
129    // or remote target conflicts (since fast-forwardable move can be safely
130    // "tracked"), and the conflicts will require user intervention anyway. So
131    // there wouldn't be much reason to handle these merges precisely.
132    let target = merge_ref_targets(index, &left.target, &base.target, &right.target).await?;
133    // Merged state shouldn't conflict atm since we only have two states, but if
134    // it does, keep the original state. The choice is arbitrary.
135    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    // If a "remove" is an ancestor of two different "adds" and one of the
155    // "adds" is an ancestor of the other, then pick the descendant.
156    for (add_index1, add1) in conflict.adds().enumerate() {
157        for (add_index2, add2) in conflict.adds().enumerate().skip(add_index1 + 1) {
158            // TODO: Instead of relying on the list order, maybe ((add1, add2), remove)
159            // combination should be somehow weighted?
160            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), // Absent ref can be considered a root
170                })
171                .await?
172            {
173                return Ok(Some((remove_index, add_index)));
174            }
175        }
176    }
177
178    Ok(None)
179}
180
181/// Pair of local and remote targets.
182#[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
197/// Figure out what changes (if any) need to be made to the remote when pushing
198/// this ref.
199pub 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        // This is not RemoteUntracked error since non-tracking remote refs
293        // have no relation to local refs, and there's nothing to push.
294        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}