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#![allow(missing_docs)]
16
17use std::fmt;
18use std::fmt::Display;
19
20use itertools::EitherOrBoth;
21
22use crate::backend::CommitId;
23use crate::index::Index;
24use crate::merge::trivial_merge;
25use crate::merge::Merge;
26use crate::op_store::RefTarget;
27use crate::op_store::RemoteRef;
28use crate::revset;
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
77fn iter_named_pairs<K: Ord, V1, V2>(
78    refs1: impl IntoIterator<Item = (K, V1)>,
79    refs2: impl IntoIterator<Item = (K, V2)>,
80    absent_ref1: impl Fn() -> V1,
81    absent_ref2: impl Fn() -> V2,
82) -> impl Iterator<Item = (K, (V1, V2))> {
83    itertools::merge_join_by(refs1, refs2, |(name1, _), (name2, _)| name1.cmp(name2)).map(
84        move |entry| match entry {
85            EitherOrBoth::Both((name, target1), (_, target2)) => (name, (target1, target2)),
86            EitherOrBoth::Left((name, target1)) => (name, (target1, absent_ref2())),
87            EitherOrBoth::Right((name, target2)) => (name, (absent_ref1(), target2)),
88        },
89    )
90}
91
92pub fn merge_ref_targets(
93    index: &dyn Index,
94    left: &RefTarget,
95    base: &RefTarget,
96    right: &RefTarget,
97) -> RefTarget {
98    if let Some(&resolved) = trivial_merge(&[left, base, right]) {
99        return resolved.clone();
100    }
101
102    let mut merge = Merge::from_vec(vec![
103        left.as_merge().clone(),
104        base.as_merge().clone(),
105        right.as_merge().clone(),
106    ])
107    .flatten()
108    .simplify();
109    // Suppose left = [A - C + B], base = [B], right = [A], the merge result is
110    // [A - C + A], which can now be trivially resolved.
111    if let Some(resolved) = merge.resolve_trivial() {
112        RefTarget::resolved(resolved.clone())
113    } else {
114        merge_ref_targets_non_trivial(index, &mut merge);
115        // TODO: Maybe better to try resolve_trivial() again, but the result is
116        // unreliable since merge_ref_targets_non_trivial() is order dependent.
117        RefTarget::from_merge(merge)
118    }
119}
120
121pub fn merge_remote_refs(
122    index: &dyn Index,
123    left: &RemoteRef,
124    base: &RemoteRef,
125    right: &RemoteRef,
126) -> RemoteRef {
127    // Just merge target and state fields separately. Strictly speaking, merging
128    // target-only change and state-only change shouldn't automatically mark the
129    // new target as tracking. However, many faulty merges will end up in local
130    // or remote target conflicts (since fast-forwardable move can be safely
131    // "tracked"), and the conflicts will require user intervention anyway. So
132    // there wouldn't be much reason to handle these merges precisely.
133    let target = merge_ref_targets(index, &left.target, &base.target, &right.target);
134    // Merged state shouldn't conflict atm since we only have two states, but if
135    // it does, keep the original state. The choice is arbitrary.
136    let state = *trivial_merge(&[left.state, base.state, right.state]).unwrap_or(&base.state);
137    RemoteRef { target, state }
138}
139
140fn merge_ref_targets_non_trivial(index: &dyn Index, conflict: &mut Merge<Option<CommitId>>) {
141    while let Some((remove_index, add_index)) = find_pair_to_remove(index, conflict) {
142        conflict.swap_remove(remove_index, add_index);
143    }
144}
145
146fn find_pair_to_remove(
147    index: &dyn Index,
148    conflict: &Merge<Option<CommitId>>,
149) -> Option<(usize, usize)> {
150    // If a "remove" is an ancestor of two different "adds" and one of the
151    // "adds" is an ancestor of the other, then pick the descendant.
152    for (add_index1, add1) in conflict.adds().enumerate() {
153        for (add_index2, add2) in conflict.adds().enumerate().skip(add_index1 + 1) {
154            // TODO: Instead of relying on the list order, maybe ((add1, add2), remove)
155            // combination should be somehow weighted?
156            let (add_index, add_id) = match (add1, add2) {
157                (Some(id1), Some(id2)) if id1 == id2 => (add_index1, id1),
158                (Some(id1), Some(id2)) if index.is_ancestor(id1, id2) => (add_index1, id1),
159                (Some(id1), Some(id2)) if index.is_ancestor(id2, id1) => (add_index2, id2),
160                _ => continue,
161            };
162            if let Some(remove_index) = conflict.removes().position(|remove| match remove {
163                Some(id) => index.is_ancestor(id, add_id),
164                None => true, // Absent ref can be considered a root
165            }) {
166                return Some((remove_index, add_index));
167            }
168        }
169    }
170
171    None
172}
173
174/// Owned remote bookmark or tag name.
175///
176/// This type can be displayed in `{name}@{remote}` form, with quoting and
177/// escaping if necessary.
178#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
179pub struct RemoteRefSymbolBuf {
180    /// Local name.
181    pub name: String,
182    /// Remote name.
183    pub remote: String,
184}
185
186impl RemoteRefSymbolBuf {
187    /// Converts to reference type.
188    pub fn as_ref(&self) -> RemoteRefSymbol<'_> {
189        RemoteRefSymbol {
190            name: &self.name,
191            remote: &self.remote,
192        }
193    }
194}
195
196/// Borrowed remote bookmark or tag name.
197///
198/// This type can be displayed in `{name}@{remote}` form, with quoting and
199/// escaping if necessary.
200#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
201pub struct RemoteRefSymbol<'a> {
202    /// Local name.
203    pub name: &'a str,
204    /// Remote name.
205    pub remote: &'a str,
206}
207
208impl RemoteRefSymbol<'_> {
209    /// Converts to owned type.
210    pub fn to_owned(self) -> RemoteRefSymbolBuf {
211        RemoteRefSymbolBuf {
212            name: self.name.to_owned(),
213            remote: self.remote.to_owned(),
214        }
215    }
216}
217
218impl From<RemoteRefSymbol<'_>> for RemoteRefSymbolBuf {
219    fn from(value: RemoteRefSymbol<'_>) -> Self {
220        value.to_owned()
221    }
222}
223
224impl PartialEq<RemoteRefSymbol<'_>> for RemoteRefSymbolBuf {
225    fn eq(&self, other: &RemoteRefSymbol) -> bool {
226        self.as_ref() == *other
227    }
228}
229
230impl PartialEq<RemoteRefSymbol<'_>> for &RemoteRefSymbolBuf {
231    fn eq(&self, other: &RemoteRefSymbol) -> bool {
232        self.as_ref() == *other
233    }
234}
235
236impl PartialEq<RemoteRefSymbolBuf> for RemoteRefSymbol<'_> {
237    fn eq(&self, other: &RemoteRefSymbolBuf) -> bool {
238        *self == other.as_ref()
239    }
240}
241
242impl PartialEq<&RemoteRefSymbolBuf> for RemoteRefSymbol<'_> {
243    fn eq(&self, other: &&RemoteRefSymbolBuf) -> bool {
244        *self == other.as_ref()
245    }
246}
247
248impl Display for RemoteRefSymbolBuf {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        Display::fmt(&self.as_ref(), f)
251    }
252}
253
254impl Display for RemoteRefSymbol<'_> {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        let RemoteRefSymbol { name, remote } = self;
257        f.pad(&revset::format_remote_symbol(name, remote))
258    }
259}
260
261/// Pair of local and remote targets.
262#[derive(Clone, Copy, Debug, Eq, PartialEq)]
263pub struct LocalAndRemoteRef<'a> {
264    pub local_target: &'a RefTarget,
265    pub remote_ref: &'a RemoteRef,
266}
267
268#[derive(Debug, PartialEq, Eq, Clone, Hash)]
269pub struct BookmarkPushUpdate {
270    pub old_target: Option<CommitId>,
271    pub new_target: Option<CommitId>,
272}
273
274#[derive(Debug, PartialEq, Eq, Clone)]
275pub enum BookmarkPushAction {
276    Update(BookmarkPushUpdate),
277    AlreadyMatches,
278    LocalConflicted,
279    RemoteConflicted,
280    RemoteUntracked,
281}
282
283/// Figure out what changes (if any) need to be made to the remote when pushing
284/// this bookmark.
285pub fn classify_bookmark_push_action(targets: LocalAndRemoteRef) -> BookmarkPushAction {
286    let local_target = targets.local_target;
287    let remote_target = targets.remote_ref.tracking_target();
288    if local_target == remote_target {
289        BookmarkPushAction::AlreadyMatches
290    } else if local_target.has_conflict() {
291        BookmarkPushAction::LocalConflicted
292    } else if remote_target.has_conflict() {
293        BookmarkPushAction::RemoteConflicted
294    } else if targets.remote_ref.is_present() && !targets.remote_ref.is_tracking() {
295        BookmarkPushAction::RemoteUntracked
296    } else {
297        BookmarkPushAction::Update(BookmarkPushUpdate {
298            old_target: remote_target.as_normal().cloned(),
299            new_target: local_target.as_normal().cloned(),
300        })
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307    use crate::op_store::RemoteRefState;
308
309    fn new_remote_ref(target: RefTarget) -> RemoteRef {
310        RemoteRef {
311            target,
312            state: RemoteRefState::New,
313        }
314    }
315
316    fn tracking_remote_ref(target: RefTarget) -> RemoteRef {
317        RemoteRef {
318            target,
319            state: RemoteRefState::Tracking,
320        }
321    }
322
323    #[test]
324    fn test_classify_bookmark_push_action_unchanged() {
325        let commit_id1 = CommitId::from_hex("11");
326        let targets = LocalAndRemoteRef {
327            local_target: &RefTarget::normal(commit_id1.clone()),
328            remote_ref: &tracking_remote_ref(RefTarget::normal(commit_id1)),
329        };
330        assert_eq!(
331            classify_bookmark_push_action(targets),
332            BookmarkPushAction::AlreadyMatches
333        );
334    }
335
336    #[test]
337    fn test_classify_bookmark_push_action_added() {
338        let commit_id1 = CommitId::from_hex("11");
339        let targets = LocalAndRemoteRef {
340            local_target: &RefTarget::normal(commit_id1.clone()),
341            remote_ref: RemoteRef::absent_ref(),
342        };
343        assert_eq!(
344            classify_bookmark_push_action(targets),
345            BookmarkPushAction::Update(BookmarkPushUpdate {
346                old_target: None,
347                new_target: Some(commit_id1),
348            })
349        );
350    }
351
352    #[test]
353    fn test_classify_bookmark_push_action_removed() {
354        let commit_id1 = CommitId::from_hex("11");
355        let targets = LocalAndRemoteRef {
356            local_target: RefTarget::absent_ref(),
357            remote_ref: &tracking_remote_ref(RefTarget::normal(commit_id1.clone())),
358        };
359        assert_eq!(
360            classify_bookmark_push_action(targets),
361            BookmarkPushAction::Update(BookmarkPushUpdate {
362                old_target: Some(commit_id1),
363                new_target: None,
364            })
365        );
366    }
367
368    #[test]
369    fn test_classify_bookmark_push_action_updated() {
370        let commit_id1 = CommitId::from_hex("11");
371        let commit_id2 = CommitId::from_hex("22");
372        let targets = LocalAndRemoteRef {
373            local_target: &RefTarget::normal(commit_id2.clone()),
374            remote_ref: &tracking_remote_ref(RefTarget::normal(commit_id1.clone())),
375        };
376        assert_eq!(
377            classify_bookmark_push_action(targets),
378            BookmarkPushAction::Update(BookmarkPushUpdate {
379                old_target: Some(commit_id1),
380                new_target: Some(commit_id2),
381            })
382        );
383    }
384
385    #[test]
386    fn test_classify_bookmark_push_action_removed_untracked() {
387        // This is not RemoteUntracked error since non-tracking remote bookmarks
388        // have no relation to local bookmarks, and there's nothing to push.
389        let commit_id1 = CommitId::from_hex("11");
390        let targets = LocalAndRemoteRef {
391            local_target: RefTarget::absent_ref(),
392            remote_ref: &new_remote_ref(RefTarget::normal(commit_id1.clone())),
393        };
394        assert_eq!(
395            classify_bookmark_push_action(targets),
396            BookmarkPushAction::AlreadyMatches
397        );
398    }
399
400    #[test]
401    fn test_classify_bookmark_push_action_updated_untracked() {
402        let commit_id1 = CommitId::from_hex("11");
403        let commit_id2 = CommitId::from_hex("22");
404        let targets = LocalAndRemoteRef {
405            local_target: &RefTarget::normal(commit_id2.clone()),
406            remote_ref: &new_remote_ref(RefTarget::normal(commit_id1.clone())),
407        };
408        assert_eq!(
409            classify_bookmark_push_action(targets),
410            BookmarkPushAction::RemoteUntracked
411        );
412    }
413
414    #[test]
415    fn test_classify_bookmark_push_action_local_conflicted() {
416        let commit_id1 = CommitId::from_hex("11");
417        let commit_id2 = CommitId::from_hex("22");
418        let targets = LocalAndRemoteRef {
419            local_target: &RefTarget::from_legacy_form([], [commit_id1.clone(), commit_id2]),
420            remote_ref: &tracking_remote_ref(RefTarget::normal(commit_id1)),
421        };
422        assert_eq!(
423            classify_bookmark_push_action(targets),
424            BookmarkPushAction::LocalConflicted
425        );
426    }
427
428    #[test]
429    fn test_classify_bookmark_push_action_remote_conflicted() {
430        let commit_id1 = CommitId::from_hex("11");
431        let commit_id2 = CommitId::from_hex("22");
432        let targets = LocalAndRemoteRef {
433            local_target: &RefTarget::normal(commit_id1.clone()),
434            remote_ref: &tracking_remote_ref(RefTarget::from_legacy_form(
435                [],
436                [commit_id1, commit_id2],
437            )),
438        };
439        assert_eq!(
440            classify_bookmark_push_action(targets),
441            BookmarkPushAction::RemoteConflicted
442        );
443    }
444}