just playing with tangled
at globpattern 460 lines 16 kB view raw
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 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 fn merge_ref_targets( 109 index: &dyn Index, 110 left: &RefTarget, 111 base: &RefTarget, 112 right: &RefTarget, 113) -> RefTarget { 114 if let Some(&resolved) = trivial_merge(&[left, base, right]) { 115 return 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() { 128 RefTarget::resolved(resolved.clone()) 129 } else { 130 merge_ref_targets_non_trivial(index, &mut merge); 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 RefTarget::from_merge(merge) 134 } 135} 136 137pub fn merge_remote_refs( 138 index: &dyn Index, 139 left: &RemoteRef, 140 base: &RemoteRef, 141 right: &RemoteRef, 142) -> 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); 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]).unwrap_or(&base.state); 153 RemoteRef { target, state } 154} 155 156fn merge_ref_targets_non_trivial(index: &dyn Index, conflict: &mut Merge<Option<CommitId>>) { 157 while let Some((remove_index, add_index)) = find_pair_to_remove(index, conflict) { 158 conflict.swap_remove(remove_index, add_index); 159 } 160} 161 162fn find_pair_to_remove( 163 index: &dyn Index, 164 conflict: &Merge<Option<CommitId>>, 165) -> Option<(usize, usize)> { 166 // If a "remove" is an ancestor of two different "adds" and one of the 167 // "adds" is an ancestor of the other, then pick the descendant. 168 for (add_index1, add1) in conflict.adds().enumerate() { 169 for (add_index2, add2) in conflict.adds().enumerate().skip(add_index1 + 1) { 170 // TODO: Instead of relying on the list order, maybe ((add1, add2), remove) 171 // combination should be somehow weighted? 172 let (add_index, add_id) = match (add1, add2) { 173 (Some(id1), Some(id2)) if id1 == id2 => (add_index1, id1), 174 (Some(id1), Some(id2)) if index.is_ancestor(id1, id2) => (add_index1, id1), 175 (Some(id1), Some(id2)) if index.is_ancestor(id2, id1) => (add_index2, id2), 176 _ => continue, 177 }; 178 if let Some(remove_index) = conflict.removes().position(|remove| match remove { 179 Some(id) => index.is_ancestor(id, add_id), 180 None => true, // Absent ref can be considered a root 181 }) { 182 return Some((remove_index, add_index)); 183 } 184 } 185 } 186 187 None 188} 189 190/// Owned remote bookmark or tag name. 191/// 192/// This type can be displayed in `{name}@{remote}` form, with quoting and 193/// escaping if necessary. 194#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 195pub struct RemoteRefSymbolBuf { 196 /// Local name. 197 pub name: String, 198 /// Remote name. 199 pub remote: String, 200} 201 202impl RemoteRefSymbolBuf { 203 /// Converts to reference type. 204 pub fn as_ref(&self) -> RemoteRefSymbol<'_> { 205 RemoteRefSymbol { 206 name: &self.name, 207 remote: &self.remote, 208 } 209 } 210} 211 212/// Borrowed remote bookmark or tag name. 213/// 214/// This type can be displayed in `{name}@{remote}` form, with quoting and 215/// escaping if necessary. 216#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 217pub struct RemoteRefSymbol<'a> { 218 /// Local name. 219 pub name: &'a str, 220 /// Remote name. 221 pub remote: &'a str, 222} 223 224impl RemoteRefSymbol<'_> { 225 /// Converts to owned type. 226 pub fn to_owned(self) -> RemoteRefSymbolBuf { 227 RemoteRefSymbolBuf { 228 name: self.name.to_owned(), 229 remote: self.remote.to_owned(), 230 } 231 } 232} 233 234impl From<RemoteRefSymbol<'_>> for RemoteRefSymbolBuf { 235 fn from(value: RemoteRefSymbol<'_>) -> Self { 236 value.to_owned() 237 } 238} 239 240impl PartialEq<RemoteRefSymbol<'_>> for RemoteRefSymbolBuf { 241 fn eq(&self, other: &RemoteRefSymbol) -> bool { 242 self.as_ref() == *other 243 } 244} 245 246impl PartialEq<RemoteRefSymbol<'_>> for &RemoteRefSymbolBuf { 247 fn eq(&self, other: &RemoteRefSymbol) -> bool { 248 self.as_ref() == *other 249 } 250} 251 252impl PartialEq<RemoteRefSymbolBuf> for RemoteRefSymbol<'_> { 253 fn eq(&self, other: &RemoteRefSymbolBuf) -> bool { 254 *self == other.as_ref() 255 } 256} 257 258impl PartialEq<&RemoteRefSymbolBuf> for RemoteRefSymbol<'_> { 259 fn eq(&self, other: &&RemoteRefSymbolBuf) -> bool { 260 *self == other.as_ref() 261 } 262} 263 264impl Display for RemoteRefSymbolBuf { 265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 266 Display::fmt(&self.as_ref(), f) 267 } 268} 269 270impl Display for RemoteRefSymbol<'_> { 271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 272 let RemoteRefSymbol { name, remote } = self; 273 f.pad(&revset::format_remote_symbol(name, remote)) 274 } 275} 276 277/// Pair of local and remote targets. 278#[derive(Clone, Copy, Debug, Eq, PartialEq)] 279pub struct LocalAndRemoteRef<'a> { 280 pub local_target: &'a RefTarget, 281 pub remote_ref: &'a RemoteRef, 282} 283 284#[derive(Debug, PartialEq, Eq, Clone, Hash)] 285pub struct BookmarkPushUpdate { 286 pub old_target: Option<CommitId>, 287 pub new_target: Option<CommitId>, 288} 289 290#[derive(Debug, PartialEq, Eq, Clone)] 291pub enum BookmarkPushAction { 292 Update(BookmarkPushUpdate), 293 AlreadyMatches, 294 LocalConflicted, 295 RemoteConflicted, 296 RemoteUntracked, 297} 298 299/// Figure out what changes (if any) need to be made to the remote when pushing 300/// this bookmark. 301pub fn classify_bookmark_push_action(targets: LocalAndRemoteRef) -> BookmarkPushAction { 302 let local_target = targets.local_target; 303 let remote_target = targets.remote_ref.tracking_target(); 304 if local_target == remote_target { 305 BookmarkPushAction::AlreadyMatches 306 } else if local_target.has_conflict() { 307 BookmarkPushAction::LocalConflicted 308 } else if remote_target.has_conflict() { 309 BookmarkPushAction::RemoteConflicted 310 } else if targets.remote_ref.is_present() && !targets.remote_ref.is_tracking() { 311 BookmarkPushAction::RemoteUntracked 312 } else { 313 BookmarkPushAction::Update(BookmarkPushUpdate { 314 old_target: remote_target.as_normal().cloned(), 315 new_target: local_target.as_normal().cloned(), 316 }) 317 } 318} 319 320#[cfg(test)] 321mod tests { 322 use super::*; 323 use crate::op_store::RemoteRefState; 324 325 fn new_remote_ref(target: RefTarget) -> RemoteRef { 326 RemoteRef { 327 target, 328 state: RemoteRefState::New, 329 } 330 } 331 332 fn tracking_remote_ref(target: RefTarget) -> RemoteRef { 333 RemoteRef { 334 target, 335 state: RemoteRefState::Tracking, 336 } 337 } 338 339 #[test] 340 fn test_classify_bookmark_push_action_unchanged() { 341 let commit_id1 = CommitId::from_hex("11"); 342 let targets = LocalAndRemoteRef { 343 local_target: &RefTarget::normal(commit_id1.clone()), 344 remote_ref: &tracking_remote_ref(RefTarget::normal(commit_id1)), 345 }; 346 assert_eq!( 347 classify_bookmark_push_action(targets), 348 BookmarkPushAction::AlreadyMatches 349 ); 350 } 351 352 #[test] 353 fn test_classify_bookmark_push_action_added() { 354 let commit_id1 = CommitId::from_hex("11"); 355 let targets = LocalAndRemoteRef { 356 local_target: &RefTarget::normal(commit_id1.clone()), 357 remote_ref: RemoteRef::absent_ref(), 358 }; 359 assert_eq!( 360 classify_bookmark_push_action(targets), 361 BookmarkPushAction::Update(BookmarkPushUpdate { 362 old_target: None, 363 new_target: Some(commit_id1), 364 }) 365 ); 366 } 367 368 #[test] 369 fn test_classify_bookmark_push_action_removed() { 370 let commit_id1 = CommitId::from_hex("11"); 371 let targets = LocalAndRemoteRef { 372 local_target: RefTarget::absent_ref(), 373 remote_ref: &tracking_remote_ref(RefTarget::normal(commit_id1.clone())), 374 }; 375 assert_eq!( 376 classify_bookmark_push_action(targets), 377 BookmarkPushAction::Update(BookmarkPushUpdate { 378 old_target: Some(commit_id1), 379 new_target: None, 380 }) 381 ); 382 } 383 384 #[test] 385 fn test_classify_bookmark_push_action_updated() { 386 let commit_id1 = CommitId::from_hex("11"); 387 let commit_id2 = CommitId::from_hex("22"); 388 let targets = LocalAndRemoteRef { 389 local_target: &RefTarget::normal(commit_id2.clone()), 390 remote_ref: &tracking_remote_ref(RefTarget::normal(commit_id1.clone())), 391 }; 392 assert_eq!( 393 classify_bookmark_push_action(targets), 394 BookmarkPushAction::Update(BookmarkPushUpdate { 395 old_target: Some(commit_id1), 396 new_target: Some(commit_id2), 397 }) 398 ); 399 } 400 401 #[test] 402 fn test_classify_bookmark_push_action_removed_untracked() { 403 // This is not RemoteUntracked error since non-tracking remote bookmarks 404 // have no relation to local bookmarks, and there's nothing to push. 405 let commit_id1 = CommitId::from_hex("11"); 406 let targets = LocalAndRemoteRef { 407 local_target: RefTarget::absent_ref(), 408 remote_ref: &new_remote_ref(RefTarget::normal(commit_id1.clone())), 409 }; 410 assert_eq!( 411 classify_bookmark_push_action(targets), 412 BookmarkPushAction::AlreadyMatches 413 ); 414 } 415 416 #[test] 417 fn test_classify_bookmark_push_action_updated_untracked() { 418 let commit_id1 = CommitId::from_hex("11"); 419 let commit_id2 = CommitId::from_hex("22"); 420 let targets = LocalAndRemoteRef { 421 local_target: &RefTarget::normal(commit_id2.clone()), 422 remote_ref: &new_remote_ref(RefTarget::normal(commit_id1.clone())), 423 }; 424 assert_eq!( 425 classify_bookmark_push_action(targets), 426 BookmarkPushAction::RemoteUntracked 427 ); 428 } 429 430 #[test] 431 fn test_classify_bookmark_push_action_local_conflicted() { 432 let commit_id1 = CommitId::from_hex("11"); 433 let commit_id2 = CommitId::from_hex("22"); 434 let targets = LocalAndRemoteRef { 435 local_target: &RefTarget::from_legacy_form([], [commit_id1.clone(), commit_id2]), 436 remote_ref: &tracking_remote_ref(RefTarget::normal(commit_id1)), 437 }; 438 assert_eq!( 439 classify_bookmark_push_action(targets), 440 BookmarkPushAction::LocalConflicted 441 ); 442 } 443 444 #[test] 445 fn test_classify_bookmark_push_action_remote_conflicted() { 446 let commit_id1 = CommitId::from_hex("11"); 447 let commit_id2 = CommitId::from_hex("22"); 448 let targets = LocalAndRemoteRef { 449 local_target: &RefTarget::normal(commit_id1.clone()), 450 remote_ref: &tracking_remote_ref(RefTarget::from_legacy_form( 451 [], 452 [commit_id1, commit_id2], 453 )), 454 }; 455 assert_eq!( 456 classify_bookmark_push_action(targets), 457 BookmarkPushAction::RemoteConflicted 458 ); 459 } 460}