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::RefTarget;
28use crate::op_store::RemoteRef;
29
30/// Compares `refs1` and `refs2` targets, yields entry if they differ.
31///
32/// `refs1` and `refs2` must be sorted by `K`.
33pub 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
46/// Compares remote `refs1` and `refs2` pairs, yields entry if they differ.
47///
48/// `refs1` and `refs2` must be sorted by `K`.
49pub 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
62/// Iterates local `refs1` and remote `refs2` pairs by name.
63///
64/// `refs1` and `refs2` must be sorted by `K`.
65pub 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
77/// Compares `ids1` and `ids2` commit ids, yields entry if they differ.
78///
79/// `ids1` and `ids2` must be sorted by `K`.
80pub 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 async 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    // Suppose left = [A - C + B], base = [B], right = [A], the merge result is
126    // [A - C + A], which can now be trivially resolved.
127    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).await?;
131        // TODO: Maybe better to try resolve_trivial() again, but the result is
132        // unreliable since merge_ref_targets_non_trivial() is order dependent.
133        Ok(RefTarget::from_merge(merge))
134    }
135}
136
137pub async fn merge_remote_refs(
138    index: &dyn Index,
139    left: &RemoteRef,
140    base: &RemoteRef,
141    right: &RemoteRef,
142) -> IndexResult<RemoteRef> {
143    // Just merge target and state fields separately. Strictly speaking, merging
144    // target-only change and state-only change shouldn't automatically mark the
145    // new target as tracking. However, many faulty merges will end up in local
146    // or remote target conflicts (since fast-forwardable move can be safely
147    // "tracked"), and the conflicts will require user intervention anyway. So
148    // there wouldn't be much reason to handle these merges precisely.
149    let target = merge_ref_targets(index, &left.target, &base.target, &right.target).await?;
150    // Merged state shouldn't conflict atm since we only have two states, but if
151    // it does, keep the original state. The choice is arbitrary.
152    let state = *trivial_merge(&[left.state, base.state, right.state], SameChange::Accept)
153        .unwrap_or(&base.state);
154    Ok(RemoteRef { target, state })
155}
156
157async fn 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).await? {
162        conflict.swap_remove(remove_index, add_index);
163    }
164    Ok(())
165}
166
167async fn find_pair_to_remove(
168    index: &dyn Index,
169    conflict: &Merge<Option<CommitId>>,
170) -> IndexResult<Option<(usize, usize)>> {
171    // If a "remove" is an ancestor of two different "adds" and one of the
172    // "adds" is an ancestor of the other, then pick the descendant.
173    for (add_index1, add1) in conflict.adds().enumerate() {
174        for (add_index2, add2) in conflict.adds().enumerate().skip(add_index1 + 1) {
175            // TODO: Instead of relying on the list order, maybe ((add1, add2), remove)
176            // combination should be somehow weighted?
177            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).await? => (add_index1, id1),
180                (Some(id1), Some(id2)) if index.is_ancestor(id2, id1).await? => (add_index2, id2),
181                _ => continue,
182            };
183            if let Some(remove_index) =
184                fallible_position(conflict.removes(), async |remove| match remove {
185                    Some(id) => index.is_ancestor(id, add_id).await,
186                    None => Ok(true), // Absent ref can be considered a root
187                })
188                .await?
189            {
190                return Ok(Some((remove_index, add_index)));
191            }
192        }
193    }
194
195    Ok(None)
196}
197
198/// Pair of local and remote targets.
199#[derive(Clone, Copy, Debug, Eq, PartialEq)]
200pub struct LocalAndRemoteRef<'a> {
201    pub local_target: &'a RefTarget,
202    pub remote_ref: &'a RemoteRef,
203}
204
205#[derive(Debug, PartialEq, Eq, Clone)]
206pub enum RefPushAction {
207    Update(Diff<Option<CommitId>>),
208    AlreadyMatches,
209    LocalConflicted,
210    RemoteConflicted,
211    RemoteUntracked,
212}
213
214/// Figure out what changes (if any) need to be made to the remote when pushing
215/// this ref.
216pub fn classify_ref_push_action(targets: LocalAndRemoteRef) -> RefPushAction {
217    let local_target = targets.local_target;
218    let remote_target = targets.remote_ref.tracked_target();
219    if local_target == remote_target {
220        RefPushAction::AlreadyMatches
221    } else if local_target.has_conflict() {
222        RefPushAction::LocalConflicted
223    } else if remote_target.has_conflict() {
224        RefPushAction::RemoteConflicted
225    } else if targets.remote_ref.is_present() && !targets.remote_ref.is_tracked() {
226        RefPushAction::RemoteUntracked
227    } else {
228        RefPushAction::Update(Diff::new(
229            remote_target.as_normal().cloned(),
230            local_target.as_normal().cloned(),
231        ))
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use crate::op_store::RemoteRefState;
239
240    fn new_remote_ref(target: RefTarget) -> RemoteRef {
241        RemoteRef {
242            target,
243            state: RemoteRefState::New,
244        }
245    }
246
247    fn tracked_remote_ref(target: RefTarget) -> RemoteRef {
248        RemoteRef {
249            target,
250            state: RemoteRefState::Tracked,
251        }
252    }
253
254    #[test]
255    fn test_classify_ref_push_action_unchanged() {
256        let commit_id1 = CommitId::from_hex("11");
257        let targets = LocalAndRemoteRef {
258            local_target: &RefTarget::normal(commit_id1.clone()),
259            remote_ref: &tracked_remote_ref(RefTarget::normal(commit_id1)),
260        };
261        assert_eq!(
262            classify_ref_push_action(targets),
263            RefPushAction::AlreadyMatches
264        );
265    }
266
267    #[test]
268    fn test_classify_ref_push_action_added() {
269        let commit_id1 = CommitId::from_hex("11");
270        let targets = LocalAndRemoteRef {
271            local_target: &RefTarget::normal(commit_id1.clone()),
272            remote_ref: RemoteRef::absent_ref(),
273        };
274        assert_eq!(
275            classify_ref_push_action(targets),
276            RefPushAction::Update(Diff::new(None, Some(commit_id1)))
277        );
278    }
279
280    #[test]
281    fn test_classify_ref_push_action_removed() {
282        let commit_id1 = CommitId::from_hex("11");
283        let targets = LocalAndRemoteRef {
284            local_target: RefTarget::absent_ref(),
285            remote_ref: &tracked_remote_ref(RefTarget::normal(commit_id1.clone())),
286        };
287        assert_eq!(
288            classify_ref_push_action(targets),
289            RefPushAction::Update(Diff::new(Some(commit_id1), None))
290        );
291    }
292
293    #[test]
294    fn test_classify_ref_push_action_updated() {
295        let commit_id1 = CommitId::from_hex("11");
296        let commit_id2 = CommitId::from_hex("22");
297        let targets = LocalAndRemoteRef {
298            local_target: &RefTarget::normal(commit_id2.clone()),
299            remote_ref: &tracked_remote_ref(RefTarget::normal(commit_id1.clone())),
300        };
301        assert_eq!(
302            classify_ref_push_action(targets),
303            RefPushAction::Update(Diff::new(Some(commit_id1), Some(commit_id2)))
304        );
305    }
306
307    #[test]
308    fn test_classify_ref_push_action_removed_untracked() {
309        // This is not RemoteUntracked error since non-tracking remote refs
310        // have no relation to local refs, and there's nothing to push.
311        let commit_id1 = CommitId::from_hex("11");
312        let targets = LocalAndRemoteRef {
313            local_target: RefTarget::absent_ref(),
314            remote_ref: &new_remote_ref(RefTarget::normal(commit_id1.clone())),
315        };
316        assert_eq!(
317            classify_ref_push_action(targets),
318            RefPushAction::AlreadyMatches
319        );
320    }
321
322    #[test]
323    fn test_classify_ref_push_action_updated_untracked() {
324        let commit_id1 = CommitId::from_hex("11");
325        let commit_id2 = CommitId::from_hex("22");
326        let targets = LocalAndRemoteRef {
327            local_target: &RefTarget::normal(commit_id2.clone()),
328            remote_ref: &new_remote_ref(RefTarget::normal(commit_id1.clone())),
329        };
330        assert_eq!(
331            classify_ref_push_action(targets),
332            RefPushAction::RemoteUntracked
333        );
334    }
335
336    #[test]
337    fn test_classify_ref_push_action_local_conflicted() {
338        let commit_id1 = CommitId::from_hex("11");
339        let commit_id2 = CommitId::from_hex("22");
340        let targets = LocalAndRemoteRef {
341            local_target: &RefTarget::from_legacy_form([], [commit_id1.clone(), commit_id2]),
342            remote_ref: &tracked_remote_ref(RefTarget::normal(commit_id1)),
343        };
344        assert_eq!(
345            classify_ref_push_action(targets),
346            RefPushAction::LocalConflicted
347        );
348    }
349
350    #[test]
351    fn test_classify_ref_push_action_remote_conflicted() {
352        let commit_id1 = CommitId::from_hex("11");
353        let commit_id2 = CommitId::from_hex("22");
354        let targets = LocalAndRemoteRef {
355            local_target: &RefTarget::normal(commit_id1.clone()),
356            remote_ref: &tracked_remote_ref(RefTarget::from_legacy_form(
357                [],
358                [commit_id1, commit_id2],
359            )),
360        };
361        assert_eq!(
362            classify_ref_push_action(targets),
363            RefPushAction::RemoteConflicted
364        );
365    }
366}