| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | |
|
| 8 | |
|
| 9 | |
|
| 10 | |
|
| 11 | |
|
| 12 | |
|
| 13 | | use crate::hash::ChangeHash; |
| 14 | | use crate::objects::{AtomRange, AtomRef, Change, Edit, OpRef, TreeOp}; |
| 15 | | use crate::state::State; |
| 16 | | use std::collections::{BTreeSet, HashMap}; |
| 17 | |
|
| 18 | |
|
| 19 | | #[derive(Clone, Debug, Default, PartialEq, Eq)] |
| 20 | | pub struct Correspondence { |
| 21 | | |
| 22 | | |
| 23 | | pub atoms: HashMap<(crate::ids::NodeId, u32), u32>, |
| 24 | | |
| 25 | | pub ops: HashMap<u32, u32>, |
| 26 | | } |
| 27 | |
|
| 28 | | impl Correspondence { |
| 29 | | |
| 30 | 45× | pub fn identity(change: &Change) -> Correspondence { |
| 31 | 45× | let mut atoms = HashMap::new(); |
| 32 | 45× | let mut per_node: HashMap<crate::ids::NodeId, u32> = HashMap::new(); |
| 33 | 47× | for e in &change.edits { |
| 34 | 47× | if let Edit::Insert { node, lines, .. } = e { |
| 35 | 42× | let idx = per_node.entry(*node).or_default(); |
| 36 | 46× | for _ in lines.iter() { |
| 37 | 46× | atoms.insert((*node, *idx), *idx); |
| 38 | 46× | *idx += 1; |
| 39 | 46× | } |
| 40 | 5× | } |
| 41 | | } |
| 42 | 45× | let ops = (0..change.tree_ops.len() as u32).map(|i| (i, i)).collect(); |
| 43 | 45× | Correspondence { atoms, ops } |
| 44 | 45× | } |
| 45 | |
|
| 46 | | |
| 47 | | |
| 48 | | |
| 49 | 1× | pub fn by_content(old: &Change, new: &Change) -> Correspondence { |
| 50 | 2× | let lines_of = |c: &Change| -> HashMap<crate::ids::NodeId, Vec<Vec<u8>>> { |
| 51 | 2× | let mut m: HashMap<crate::ids::NodeId, Vec<Vec<u8>>> = HashMap::new(); |
| 52 | 2× | for e in &c.edits { |
| 53 | 2× | if let Edit::Insert { node, lines, .. } = e { |
| 54 | 2× | m.entry(*node) |
| 55 | 2× | .or_default() |
| 56 | 2× | .extend(lines.iter().map(<[u8]>::to_vec)); |
| 57 | 2× | } |
| 58 | | } |
| 59 | 2× | m |
| 60 | 2× | }; |
| 61 | 1× | let (a, b) = (lines_of(old), lines_of(new)); |
| 62 | 1× | let mut atoms = HashMap::new(); |
| 63 | 1× | for (node, olds) in &a { |
| 64 | 1× | let Some(news) = b.get(node) else { continue }; |
| 65 | 1× | let mut j = 0usize; |
| 66 | 1× | for (i, line) in olds.iter().enumerate() { |
| 67 | 1× | while j < news.len() && news[j] != *line { |
| 68 | 0× | j += 1; |
| 69 | 0× | } |
| 70 | 1× | if j < news.len() { |
| 71 | 1× | atoms.insert((*node, i as u32), j as u32); |
| 72 | 1× | j += 1; |
| 73 | 1× | } |
| 74 | | } |
| 75 | | } |
| 76 | 1× | let mut ops = HashMap::new(); |
| 77 | 1× | for (i, op) in old.tree_ops.iter().enumerate() { |
| 78 | 0× | if let Some(j) = new.tree_ops.iter().position(|n| { |
| 79 | 0× | n.node() == op.node() && std::mem::discriminant(n) == std::mem::discriminant(op) |
| 80 | 0× | }) { |
| 81 | 0× | ops.insert(i as u32, j as u32); |
| 82 | 0× | } |
| 83 | | } |
| 84 | 1× | Correspondence { atoms, ops } |
| 85 | 1× | } |
| 86 | | } |
| 87 | |
|
| 88 | | #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] |
| 89 | | pub enum RewriteError { |
| 90 | | #[error("change {0} is not in the state")] |
| 91 | | Missing(ChangeHash), |
| 92 | | } |
| 93 | |
|
| 94 | |
|
| 95 | | #[derive(Clone, Debug, Default)] |
| 96 | | pub struct Propagation { |
| 97 | | |
| 98 | | pub changes: Vec<(ChangeHash, Change)>, |
| 99 | | |
| 100 | | pub notes: Vec<String>, |
| 101 | | } |
| 102 | |
|
| 103 | |
|
| 104 | | struct Chain { |
| 105 | | node: crate::ids::NodeId, |
| 106 | | first: u32, |
| 107 | | last: u32, |
| 108 | | after: AtomRef, |
| 109 | | before: AtomRef, |
| 110 | | } |
| 111 | |
|
| 112 | 18× | fn chains_of(c: &Change) -> Vec<Chain> { |
| 113 | 18× | let mut per_node: HashMap<crate::ids::NodeId, u32> = HashMap::new(); |
| 114 | 18× | let mut out = Vec::new(); |
| 115 | 24× | for e in &c.edits { |
| 116 | | if let Edit::Insert { |
| 117 | 19× | node, |
| 118 | 19× | after, |
| 119 | 19× | before, |
| 120 | 19× | lines, |
| 121 | 24× | } = e |
| 122 | 19× | { |
| 123 | 19× | let idx = per_node.entry(*node).or_default(); |
| 124 | 19× | out.push(Chain { |
| 125 | 19× | node: *node, |
| 126 | 19× | first: *idx, |
| 127 | 19× | last: *idx + lines.len() as u32 - 1, |
| 128 | 19× | after: *after, |
| 129 | 19× | before: *before, |
| 130 | 19× | }); |
| 131 | 19× | *idx += lines.len() as u32; |
| 132 | 19× | } |
| 133 | | } |
| 134 | 18× | out |
| 135 | 18× | } |
| 136 | |
|
| 137 | |
|
| 138 | | #[derive(Clone, Debug)] |
| 139 | | enum Replacement { |
| 140 | | Change(ChangeHash), |
| 141 | | |
| 142 | | Removed(Vec<ChangeHash>), |
| 143 | | } |
| 144 | |
|
| 145 | |
|
| 146 | |
|
| 147 | |
|
| 148 | |
|
| 149 | |
|
| 150 | |
|
| 151 | |
|
| 152 | 19× | pub fn propagate( |
| 153 | 19× | state: &State, |
| 154 | 19× | old: ChangeHash, |
| 155 | 19× | new: Option<&Change>, |
| 156 | 19× | corr: &Correspondence, |
| 157 | 19× | load: &dyn Fn(&ChangeHash) -> Option<Change>, |
| 158 | 19× | ) -> Result<Propagation, RewriteError> { |
| 159 | 19× | let old_meta = state.get(&old).ok_or(RewriteError::Missing(old))?; |
| 160 | 18× | let old_full = load(&old); |
| 161 | 18× | let mut mapping: HashMap<ChangeHash, Replacement> = HashMap::new(); |
| 162 | 18× | mapping.insert( |
| 163 | 18× | old, |
| 164 | 18× | match new { |
| 165 | 12× | Some(n) => Replacement::Change(n.hash()), |
| 166 | 6× | None => Replacement::Removed(old_meta.deps.clone()), |
| 167 | | }, |
| 168 | | ); |
| 169 | 18× | let mut corrs: HashMap<ChangeHash, Correspondence> = HashMap::new(); |
| 170 | 18× | corrs.insert(old, corr.clone()); |
| 171 | | |
| 172 | 18× | let chains: HashMap<ChangeHash, Vec<Chain>> = old_full |
| 173 | 18× | .as_ref() |
| 174 | 18× | .map(|c| (old, chains_of(c))) |
| 175 | 18× | .into_iter() |
| 176 | 18× | .collect(); |
| 177 | 18× | let mut vanished_nodes: BTreeSet<crate::ids::NodeId> = BTreeSet::new(); |
| 178 | 18× | if let Some(c) = &old_full { |
| 179 | 26× | for (i, op) in c.tree_ops.iter().enumerate() { |
| 180 | 26× | if let TreeOp::AddNode { node, .. } = op { |
| 181 | 26× | let survives = new.is_some() && corr.ops.contains_key(&(i as u32)); |
| 182 | 26× | if !survives { |
| 183 | 13× | vanished_nodes.insert(*node); |
| 184 | 13× | } |
| 185 | 0× | } |
| 186 | | } |
| 187 | 0× | } |
| 188 | 18× | let mut notes = Vec::new(); |
| 189 | |
|
| 190 | | |
| 191 | | |
| 192 | 18× | let mut dependents: Vec<ChangeHash> = state |
| 193 | 18× | .changes() |
| 194 | 72× | .filter(|(h, _)| **h != old && state.closure(h).contains(&old)) |
| 195 | 18× | .map(|(h, _)| *h) |
| 196 | 18× | .collect(); |
| 197 | 68× | dependents.sort_by_key(|h| (state.closure(h).len(), *h)); |
| 198 | |
|
| 199 | 18× | let mut out = Vec::new(); |
| 200 | 37× | for h in dependents { |
| 201 | 37× | let c = load(&h).ok_or(RewriteError::Missing(h))?; |
| 202 | 37× | let rewritten = rewrite_one( |
| 203 | 37× | &h, |
| 204 | 37× | &c, |
| 205 | 37× | &mapping, |
| 206 | 37× | &corrs, |
| 207 | 37× | &chains, |
| 208 | 37× | &vanished_nodes, |
| 209 | 37× | &mut notes, |
| 210 | | ); |
| 211 | 37× | let rh = rewritten.hash(); |
| 212 | 37× | corrs.insert(h, Correspondence::identity(&rewritten)); |
| 213 | 37× | mapping.insert(h, Replacement::Change(rh)); |
| 214 | 37× | out.push((h, rewritten)); |
| 215 | | } |
| 216 | 18× | Ok(Propagation { |
| 217 | 18× | changes: out, |
| 218 | 18× | notes, |
| 219 | 18× | }) |
| 220 | 19× | } |
| 221 | |
|
| 222 | |
|
| 223 | |
|
| 224 | |
|
| 225 | 2× | fn reanchor( |
| 226 | 2× | change: &ChangeHash, |
| 227 | 2× | index: u32, |
| 228 | 2× | want_after: bool, |
| 229 | 2× | mapping: &HashMap<ChangeHash, Replacement>, |
| 230 | 2× | corrs: &HashMap<ChangeHash, Correspondence>, |
| 231 | 2× | chains: &HashMap<ChangeHash, Vec<Chain>>, |
| 232 | 2× | ) -> AtomRef { |
| 233 | 2× | let corr = corrs.get(change); |
| 234 | 2× | let chain = chains |
| 235 | 2× | .get(change) |
| 236 | 2× | .and_then(|cs| cs.iter().find(|ch| ch.first <= index && index <= ch.last)); |
| 237 | 2× | if let (Some(Replacement::Change(nh)), Some(corr), Some(ch)) = |
| 238 | 2× | (mapping.get(change), corr, chain) |
| 239 | | { |
| 240 | 2× | let candidates: Box<dyn Iterator<Item = u32>> = if want_after { |
| 241 | 2× | Box::new((ch.first..index).rev()) |
| 242 | | } else { |
| 243 | 0× | Box::new(index + 1..=ch.last) |
| 244 | | }; |
| 245 | 2× | for j in candidates { |
| 246 | 2× | if let Some(nj) = corr.atoms.get(&(ch.node, j)) { |
| 247 | 1× | return AtomRef::Atom { |
| 248 | 1× | change: *nh, |
| 249 | 1× | index: *nj, |
| 250 | 1× | }; |
| 251 | 1× | } |
| 252 | | } |
| 253 | 0× | } |
| 254 | 1× | match chain { |
| 255 | 1× | Some(ch) => { |
| 256 | 1× | if want_after { |
| 257 | 1× | ch.after |
| 258 | | } else { |
| 259 | 0× | ch.before |
| 260 | | } |
| 261 | | } |
| 262 | | None => { |
| 263 | 0× | if want_after { |
| 264 | 0× | AtomRef::Start |
| 265 | | } else { |
| 266 | 0× | AtomRef::End |
| 267 | | } |
| 268 | | } |
| 269 | | } |
| 270 | 2× | } |
| 271 | |
|
| 272 | 37× | fn rewrite_one( |
| 273 | 37× | hash: &ChangeHash, |
| 274 | 37× | c: &Change, |
| 275 | 37× | mapping: &HashMap<ChangeHash, Replacement>, |
| 276 | 37× | corrs: &HashMap<ChangeHash, Correspondence>, |
| 277 | 37× | chains: &HashMap<ChangeHash, Vec<Chain>>, |
| 278 | 37× | vanished_nodes: &BTreeSet<crate::ids::NodeId>, |
| 279 | 37× | notes: &mut Vec<String>, |
| 280 | 37× | ) -> Change { |
| 281 | 37× | let short = hash.short(12); |
| 282 | | |
| 283 | 37× | let map_anchor = |r: AtomRef, |
| 284 | | node: &crate::ids::NodeId, |
| 285 | | want_after: bool, |
| 286 | | notes: &mut Vec<String>| |
| 287 | 62× | -> AtomRef { |
| 288 | 62× | match r { |
| 289 | 21× | AtomRef::Atom { change, index } => match mapping.get(&change) { |
| 290 | 20× | Some(Replacement::Change(nh)) => match corrs[&change].atoms.get(&(*node, index)) { |
| 291 | 18× | Some(idx) => AtomRef::Atom { |
| 292 | 18× | change: *nh, |
| 293 | 18× | index: *idx, |
| 294 | 18× | }, |
| 295 | | None => { |
| 296 | 2× | notes.push(format!("change {short}: an anchor line of the rewritten change vanished; re-anchored to its nearest neighbour")); |
| 297 | 2× | reanchor(&change, index, want_after, mapping, corrs, chains) |
| 298 | | } |
| 299 | | }, |
| 300 | | Some(Replacement::Removed(_)) => { |
| 301 | 0× | notes.push(format!("change {short}: an anchor line of the removed change vanished; re-anchored to its neighbours")); |
| 302 | 0× | reanchor(&change, index, want_after, mapping, corrs, chains) |
| 303 | | } |
| 304 | 1× | None => r, |
| 305 | | }, |
| 306 | 41× | other => other, |
| 307 | | } |
| 308 | 62× | }; |
| 309 | | |
| 310 | 37× | let map_op = |r: OpRef| -> Option<OpRef> { |
| 311 | 1× | match mapping.get(&r.change) { |
| 312 | 1× | Some(Replacement::Change(nh)) => corrs[&r.change].ops.get(&r.index).map(|idx| OpRef { |
| 313 | 1× | change: *nh, |
| 314 | 1× | index: *idx, |
| 315 | 1× | }), |
| 316 | 0× | Some(Replacement::Removed(_)) => None, |
| 317 | 0× | None => Some(r), |
| 318 | | } |
| 319 | 1× | }; |
| 320 | 37× | let mut out = c.clone(); |
| 321 | 37× | let mut deps: BTreeSet<ChangeHash> = BTreeSet::new(); |
| 322 | 55× | for d in &c.deps { |
| 323 | 55× | match mapping.get(d) { |
| 324 | 32× | Some(Replacement::Change(nh)) => { |
| 325 | 32× | deps.insert(*nh); |
| 326 | 32× | } |
| 327 | 7× | Some(Replacement::Removed(inherited)) => deps.extend(inherited.iter().copied()), |
| 328 | 16× | None => { |
| 329 | 16× | deps.insert(*d); |
| 330 | 16× | } |
| 331 | | } |
| 332 | | } |
| 333 | 37× | out.deps = deps.into_iter().collect(); |
| 334 | 37× | let mut ops = Vec::new(); |
| 335 | 37× | for op in &out.tree_ops { |
| 336 | 13× | let node = op.node(); |
| 337 | 13× | if vanished_nodes.contains(&node) |
| 338 | 11× | || matches!(op, TreeOp::AddNode { parent, .. } if vanished_nodes.contains(parent)) |
| 339 | | { |
| 340 | 1× | notes.push(format!("change {short}: an operation on a file that vanished with the rewritten change was dropped")); |
| 341 | 1× | continue; |
| 342 | 12× | } |
| 343 | 12× | match op { |
| 344 | 0× | TreeOp::RemoveNode { node, placement } => match map_op(*placement) { |
| 345 | 0× | Some(p) => ops.push(TreeOp::RemoveNode { |
| 346 | 0× | node: *node, |
| 347 | 0× | placement: p, |
| 348 | 0× | }), |
| 349 | 0× | None => notes.push(format!( |
| 350 | | "change {short}: removal of a placement that vanished was dropped" |
| 351 | | )), |
| 352 | | }, |
| 353 | | TreeOp::Move { |
| 354 | 0× | node, |
| 355 | 0× | old, |
| 356 | 0× | parent, |
| 357 | 0× | name, |
| 358 | 0× | } => match map_op(*old) { |
| 359 | 0× | Some(o) => ops.push(TreeOp::Move { |
| 360 | 0× | node: *node, |
| 361 | 0× | old: o, |
| 362 | 0× | parent: *parent, |
| 363 | 0× | name: name.clone(), |
| 364 | 0× | }), |
| 365 | 0× | None => notes.push(format!( |
| 366 | | "change {short}: a move of {name} lost its origin and was dropped" |
| 367 | | )), |
| 368 | | }, |
| 369 | 1× | TreeOp::SetMode { node, old, mode } => match map_op(*old) { |
| 370 | 1× | Some(o) => ops.push(TreeOp::SetMode { |
| 371 | 1× | node: *node, |
| 372 | 1× | old: o, |
| 373 | 1× | mode: *mode, |
| 374 | 1× | }), |
| 375 | 0× | None => notes.push(format!( |
| 376 | | "change {short}: a mode change lost its origin and was dropped" |
| 377 | | )), |
| 378 | | }, |
| 379 | 11× | add @ TreeOp::AddNode { .. } => ops.push(add.clone()), |
| 380 | | } |
| 381 | | } |
| 382 | 37× | out.tree_ops = ops; |
| 383 | 37× | let mut edits = Vec::new(); |
| 384 | 37× | for e in &out.edits { |
| 385 | 37× | if vanished_nodes.contains(&e.node()) { |
| 386 | 4× | notes.push(format!("change {short}: an edit of a file that vanished with the rewritten change was dropped")); |
| 387 | 4× | continue; |
| 388 | 33× | } |
| 389 | 33× | match e { |
| 390 | | Edit::Insert { |
| 391 | 31× | node, |
| 392 | 31× | after, |
| 393 | 31× | before, |
| 394 | 31× | lines, |
| 395 | 31× | } => edits.push(Edit::Insert { |
| 396 | 31× | node: *node, |
| 397 | 31× | after: map_anchor(*after, node, true, notes), |
| 398 | 31× | before: map_anchor(*before, node, false, notes), |
| 399 | 31× | lines: lines.clone(), |
| 400 | 31× | }), |
| 401 | 2× | Edit::Delete { node, atoms } => { |
| 402 | 2× | let mut mapped: Vec<(ChangeHash, u32)> = Vec::new(); |
| 403 | 2× | let mut dropped = 0; |
| 404 | 2× | for r in atoms.iter() { |
| 405 | 2× | for i in r.from..r.to { |
| 406 | 2× | match mapping.get(&r.change) { |
| 407 | 2× | Some(Replacement::Change(nh)) => { |
| 408 | 2× | match corrs[&r.change].atoms.get(&(*node, i)) { |
| 409 | 2× | Some(idx) => mapped.push((*nh, *idx)), |
| 410 | 0× | None => dropped += 1, |
| 411 | | } |
| 412 | | } |
| 413 | 0× | Some(Replacement::Removed(_)) => dropped += 1, |
| 414 | 0× | None => mapped.push((r.change, i)), |
| 415 | | } |
| 416 | | } |
| 417 | | } |
| 418 | 2× | if dropped > 0 { |
| 419 | 0× | notes.push(format!( |
| 420 | 0× | "change {short}: {dropped} deletion(s) of lines that vanished were dropped" |
| 421 | 0× | )); |
| 422 | 2× | } |
| 423 | 2× | let mut ranges: Vec<AtomRange> = Vec::new(); |
| 424 | 2× | for (ch, i) in mapped { |
| 425 | 2× | match ranges.last_mut() { |
| 426 | 0× | Some(r) if r.change == ch && r.to == i => r.to = i + 1, |
| 427 | 2× | _ => ranges.push(AtomRange { |
| 428 | 2× | change: ch, |
| 429 | 2× | from: i, |
| 430 | 2× | to: i + 1, |
| 431 | 2× | }), |
| 432 | | } |
| 433 | | } |
| 434 | 2× | if !ranges.is_empty() { |
| 435 | 2× | edits.push(Edit::Delete { |
| 436 | 2× | node: *node, |
| 437 | 2× | atoms: ranges, |
| 438 | 2× | }); |
| 439 | 2× | } |
| 440 | | } |
| 441 | 0× | other => edits.push(other.clone()), |
| 442 | | } |
| 443 | | } |
| 444 | 37× | out.edits = edits; |
| 445 | 37× | out.normalize(); |
| 446 | 37× | out |
| 447 | 37× | } |
| 448 | |
|
| 449 | | #[cfg(test)] |
| 450 | | mod tests { |
| 451 | | use super::*; |
| 452 | | use crate::hash::ObjectRef; |
| 453 | | use crate::ids::{ChangeId, NodeId}; |
| 454 | | use crate::objects::{ChangeKind, Hlc, Mode, NodeKind}; |
| 455 | | use crate::state::FileContent; |
| 456 | |
|
| 457 | 9× | fn change(deps: Vec<ChangeHash>, tree_ops: Vec<TreeOp>, edits: Vec<Edit>, msg: &str) -> Change { |
| 458 | 9× | let mut c = Change { |
| 459 | 9× | version: crate::objects::CHANGE_VERSION, |
| 460 | 9× | change_id: ChangeId::random(), |
| 461 | 9× | deps, |
| 462 | 9× | author: ObjectRef::ZERO, |
| 463 | 9× | hlc: Hlc { |
| 464 | 9× | wall_ms: 1, |
| 465 | 9× | logical: 0, |
| 466 | 9× | }, |
| 467 | 9× | message: msg.into(), |
| 468 | 9× | tree_ops, |
| 469 | 9× | edits, |
| 470 | 9× | kind: ChangeKind::Normal, |
| 471 | 9× | }; |
| 472 | 9× | c.normalize(); |
| 473 | 9× | c |
| 474 | 9× | } |
| 475 | |
|
| 476 | | #[test] |
| 477 | 1× | fn describe_propagates_through_dependents() { |
| 478 | 1× | let mut st = State::new(); |
| 479 | 1× | let f = NodeId::random(); |
| 480 | 1× | let c1 = change( |
| 481 | 1× | vec![], |
| 482 | 1× | vec![TreeOp::AddNode { |
| 483 | 1× | node: f, |
| 484 | 1× | kind: NodeKind::File, |
| 485 | 1× | mode: Mode::TEXT, |
| 486 | 1× | parent: NodeId::ROOT, |
| 487 | 1× | name: "f".into(), |
| 488 | 1× | }], |
| 489 | 1× | vec![Edit::Insert { |
| 490 | 1× | node: f, |
| 491 | 1× | after: AtomRef::Start, |
| 492 | 1× | before: AtomRef::End, |
| 493 | 1× | lines: vec![b"a\n".to_vec(), b"b\n".to_vec()].into(), |
| 494 | 1× | }], |
| 495 | 1× | "one", |
| 496 | | ); |
| 497 | 1× | let h1 = st.add(c1.clone()).unwrap(); |
| 498 | 1× | let c2 = change( |
| 499 | 1× | vec![h1], |
| 500 | 1× | vec![], |
| 501 | 1× | vec![ |
| 502 | 1× | Edit::Delete { |
| 503 | 1× | node: f, |
| 504 | 1× | atoms: vec![AtomRange { |
| 505 | 1× | change: h1, |
| 506 | 1× | from: 0, |
| 507 | 1× | to: 1, |
| 508 | 1× | }], |
| 509 | 1× | }, |
| 510 | 1× | Edit::Insert { |
| 511 | 1× | node: f, |
| 512 | 1× | after: AtomRef::Atom { |
| 513 | 1× | change: h1, |
| 514 | 1× | index: 1, |
| 515 | 1× | }, |
| 516 | 1× | before: AtomRef::End, |
| 517 | 1× | lines: vec![b"c\n".to_vec()].into(), |
| 518 | 1× | }, |
| 519 | | ], |
| 520 | 1× | "two", |
| 521 | | ); |
| 522 | 1× | let h2 = st.add(c2.clone()).unwrap(); |
| 523 | 1× | let c3 = change( |
| 524 | 1× | vec![h2], |
| 525 | 1× | vec![TreeOp::SetMode { |
| 526 | 1× | node: f, |
| 527 | 1× | old: OpRef { |
| 528 | 1× | change: h1, |
| 529 | 1× | index: 0, |
| 530 | 1× | }, |
| 531 | 1× | mode: Mode { |
| 532 | 1× | exec: true, |
| 533 | 1× | opaque: false, |
| 534 | 1× | }, |
| 535 | 1× | }], |
| 536 | 1× | vec![Edit::Insert { |
| 537 | 1× | node: f, |
| 538 | 1× | after: AtomRef::Atom { |
| 539 | 1× | change: h2, |
| 540 | 1× | index: 0, |
| 541 | 1× | }, |
| 542 | 1× | before: AtomRef::End, |
| 543 | 1× | lines: vec![b"d\n".to_vec()].into(), |
| 544 | 1× | }], |
| 545 | 1× | "three", |
| 546 | | ); |
| 547 | 1× | let h3 = st.add(c3.clone()).unwrap(); |
| 548 | 1× | let before = st.materialize(); |
| 549 | |
|
| 550 | | |
| 551 | 1× | let mut n1 = c1.clone(); |
| 552 | 1× | n1.message = "one, described".into(); |
| 553 | 1× | let all: HashMap<ChangeHash, Change> = |
| 554 | 1× | [(h1, c1.clone()), (h2, c2.clone()), (h3, c3.clone())] |
| 555 | 1× | .into_iter() |
| 556 | 1× | .collect(); |
| 557 | 3× | let load = |h: &ChangeHash| all.get(h).cloned(); |
| 558 | 1× | let rewritten = propagate(&st, h1, Some(&n1), &Correspondence::identity(&c1), &load) |
| 559 | 1× | .unwrap() |
| 560 | 1× | .changes; |
| 561 | 1× | assert_eq!( |
| 562 | 1× | rewritten.iter().map(|(h, _)| *h).collect::<Vec<_>>(), |
| 563 | 1× | vec![h2, h3] |
| 564 | | ); |
| 565 | 1× | let mut st2 = State::new(); |
| 566 | 1× | st2.add(n1.clone()).unwrap(); |
| 567 | 2× | for (_, c) in &rewritten { |
| 568 | 2× | st2.add(c.clone()).unwrap(); |
| 569 | 2× | } |
| 570 | 1× | assert_eq!(st2.len(), 3); |
| 571 | 1× | let after = st2.materialize(); |
| 572 | 1× | assert_eq!(after.files["f"].mode, before.files["f"].mode); |
| 573 | 1× | match (&after.files["f"].content, &before.files["f"].content) { |
| 574 | 1× | (FileContent::Text { bytes: a, .. }, FileContent::Text { bytes: b, .. }) => { |
| 575 | 1× | assert_eq!(a, b) |
| 576 | | } |
| 577 | 0× | _ => panic!(), |
| 578 | | } |
| 579 | 1× | assert!( |
| 580 | 1× | matches!(&after.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"b\nc\nd\n") |
| 581 | | ); |
| 582 | | |
| 583 | 1× | assert_eq!(rewritten[0].1.change_id, c2.change_id); |
| 584 | 1× | assert_eq!(rewritten[0].1.deps, vec![n1.hash()]); |
| 585 | 1× | assert_ne!(rewritten[0].1.hash(), h2); |
| 586 | 1× | assert_eq!(rewritten[1].1.deps, vec![rewritten[0].1.hash()]); |
| 587 | 1× | } |
| 588 | |
|
| 589 | | #[test] |
| 590 | 1× | fn missing_counterpart_is_reanchored() { |
| 591 | 1× | let mut st = State::new(); |
| 592 | 1× | let f = NodeId::random(); |
| 593 | 1× | let c1 = change( |
| 594 | 1× | vec![], |
| 595 | 1× | vec![TreeOp::AddNode { |
| 596 | 1× | node: f, |
| 597 | 1× | kind: NodeKind::File, |
| 598 | 1× | mode: Mode::TEXT, |
| 599 | 1× | parent: NodeId::ROOT, |
| 600 | 1× | name: "f".into(), |
| 601 | 1× | }], |
| 602 | 1× | vec![Edit::Insert { |
| 603 | 1× | node: f, |
| 604 | 1× | after: AtomRef::Start, |
| 605 | 1× | before: AtomRef::End, |
| 606 | 1× | lines: vec![b"a\n".to_vec(), b"b\n".to_vec()].into(), |
| 607 | 1× | }], |
| 608 | 1× | "one", |
| 609 | | ); |
| 610 | 1× | let h1 = st.add(c1.clone()).unwrap(); |
| 611 | 1× | let c2 = change( |
| 612 | 1× | vec![h1], |
| 613 | 1× | vec![], |
| 614 | 1× | vec![Edit::Insert { |
| 615 | 1× | node: f, |
| 616 | 1× | after: AtomRef::Atom { |
| 617 | 1× | change: h1, |
| 618 | 1× | index: 1, |
| 619 | 1× | }, |
| 620 | 1× | before: AtomRef::End, |
| 621 | 1× | lines: vec![b"c\n".to_vec()].into(), |
| 622 | 1× | }], |
| 623 | 1× | "two", |
| 624 | | ); |
| 625 | 1× | let h2 = st.add(c2.clone()).unwrap(); |
| 626 | 1× | let all: HashMap<ChangeHash, Change> = [(h1, c1.clone()), (h2, c2)].into_iter().collect(); |
| 627 | 6× | let load = |h: &ChangeHash| all.get(h).cloned(); |
| 628 | | |
| 629 | | |
| 630 | 1× | let mut n1 = c1.clone(); |
| 631 | 1× | n1.edits = vec![Edit::Insert { |
| 632 | 1× | node: f, |
| 633 | 1× | after: AtomRef::Start, |
| 634 | 1× | before: AtomRef::End, |
| 635 | 1× | lines: vec![b"a\n".to_vec()].into(), |
| 636 | 1× | }]; |
| 637 | 1× | let corr = Correspondence { |
| 638 | 1× | atoms: [((f, 0), 0)].into_iter().collect(), |
| 639 | 1× | ops: [(0, 0)].into_iter().collect(), |
| 640 | 1× | }; |
| 641 | 1× | let p = propagate(&st, h1, Some(&n1), &corr, &load).unwrap(); |
| 642 | 1× | assert_eq!(p.notes.len(), 1, "{:?}", p.notes); |
| 643 | 1× | let n1h = n1.hash(); |
| 644 | 1× | assert!( |
| 645 | 1× | matches!(p.changes[0].1.edits[0], Edit::Insert { after: AtomRef::Atom { change, index: 0 }, before: AtomRef::End, .. } if change == n1h) |
| 646 | | ); |
| 647 | 1× | let mut st2 = State::new(); |
| 648 | 1× | st2.add(n1.clone()).unwrap(); |
| 649 | 1× | st2.add(p.changes[0].1.clone()).unwrap(); |
| 650 | 1× | let m = st2.materialize(); |
| 651 | 1× | assert!(!m.is_conflicted()); |
| 652 | 1× | assert!( |
| 653 | 1× | matches!(&m.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"a\nc\n") |
| 654 | | ); |
| 655 | 1× | assert_eq!( |
| 656 | 1× | propagate(&st, ObjectRef([9; 32]), Some(&n1), &corr, &load).unwrap_err(), |
| 657 | | RewriteError::Missing(ObjectRef([9; 32])) |
| 658 | | ); |
| 659 | |
|
| 660 | | |
| 661 | 1× | let p = propagate(&st, h1, None, &Correspondence::default(), &load).unwrap(); |
| 662 | 1× | assert_eq!(p.notes.len(), 1, "{:?}", p.notes); |
| 663 | 1× | assert!(p.changes[0].1.edits.is_empty() && p.changes[0].1.deps.is_empty()); |
| 664 | |
|
| 665 | | |
| 666 | | |
| 667 | 1× | let mut n2 = c1.clone(); |
| 668 | 1× | n2.edits = vec![]; |
| 669 | 1× | let corr = Correspondence { |
| 670 | 1× | atoms: HashMap::new(), |
| 671 | 1× | ops: [(0, 0)].into_iter().collect(), |
| 672 | 1× | }; |
| 673 | 1× | let p = propagate(&st, h1, Some(&n2), &corr, &load).unwrap(); |
| 674 | 1× | assert!(matches!( |
| 675 | 1× | p.changes[0].1.edits[0], |
| 676 | | Edit::Insert { |
| 677 | | after: AtomRef::Start, |
| 678 | | before: AtomRef::End, |
| 679 | | .. |
| 680 | | } |
| 681 | | )); |
| 682 | 1× | let mut st3 = State::new(); |
| 683 | 1× | st3.add(n2.clone()).unwrap(); |
| 684 | 1× | st3.add(p.changes[0].1.clone()).unwrap(); |
| 685 | 1× | let m = st3.materialize(); |
| 686 | 1× | assert!(!m.is_conflicted()); |
| 687 | 1× | assert!( |
| 688 | 1× | matches!(&m.files["f"].content, FileContent::Text { bytes, .. } if bytes == b"c\n") |
| 689 | | ); |
| 690 | 1× | } |
| 691 | |
|
| 692 | | #[test] |
| 693 | 1× | fn removal_rewires_dependencies_or_fails_on_references() { |
| 694 | 1× | let mut st = State::new(); |
| 695 | 1× | let f = NodeId::random(); |
| 696 | 1× | let g = NodeId::random(); |
| 697 | 1× | let c1 = change( |
| 698 | 1× | vec![], |
| 699 | 1× | vec![TreeOp::AddNode { |
| 700 | 1× | node: f, |
| 701 | 1× | kind: NodeKind::File, |
| 702 | 1× | mode: Mode::TEXT, |
| 703 | 1× | parent: NodeId::ROOT, |
| 704 | 1× | name: "f".into(), |
| 705 | 1× | }], |
| 706 | 1× | vec![], |
| 707 | 1× | "one", |
| 708 | | ); |
| 709 | 1× | let h1 = st.add(c1.clone()).unwrap(); |
| 710 | | |
| 711 | 1× | let c2 = change( |
| 712 | 1× | vec![h1], |
| 713 | 1× | vec![TreeOp::AddNode { |
| 714 | 1× | node: g, |
| 715 | 1× | kind: NodeKind::File, |
| 716 | 1× | mode: Mode::TEXT, |
| 717 | 1× | parent: NodeId::ROOT, |
| 718 | 1× | name: "g".into(), |
| 719 | 1× | }], |
| 720 | 1× | vec![], |
| 721 | 1× | "two", |
| 722 | | ); |
| 723 | 1× | let h2 = st.add(c2.clone()).unwrap(); |
| 724 | 1× | let c3 = change( |
| 725 | 1× | vec![h2], |
| 726 | 1× | vec![], |
| 727 | 1× | vec![Edit::Insert { |
| 728 | 1× | node: g, |
| 729 | 1× | after: AtomRef::Start, |
| 730 | 1× | before: AtomRef::End, |
| 731 | 1× | lines: vec![b"x\n".to_vec()].into(), |
| 732 | 1× | }], |
| 733 | 1× | "three", |
| 734 | | ); |
| 735 | 1× | let h3 = st.add(c3.clone()).unwrap(); |
| 736 | 1× | let mut all: HashMap<ChangeHash, Change> = |
| 737 | 1× | [(h1, c1), (h2, c2), (h3, c3)].into_iter().collect(); |
| 738 | 1× | let load_all = all.clone(); |
| 739 | 5× | let load = move |h: &ChangeHash| load_all.get(h).cloned(); |
| 740 | | |
| 741 | 1× | let out = propagate(&st, h2, None, &Correspondence::default(), &load) |
| 742 | 1× | .unwrap() |
| 743 | 1× | .changes; |
| 744 | 1× | assert_eq!(out.len(), 1); |
| 745 | 1× | assert_eq!(out[0].0, h3); |
| 746 | 1× | assert_eq!(out[0].1.deps, vec![h1]); |
| 747 | | |
| 748 | | |
| 749 | 1× | let out = propagate(&st, h1, None, &Correspondence::default(), &load) |
| 750 | 1× | .unwrap() |
| 751 | 1× | .changes; |
| 752 | 1× | assert!(out.iter().any(|(h, c)| *h == h2 && c.deps.is_empty())); |
| 753 | | |
| 754 | | |
| 755 | 1× | let c4 = change( |
| 756 | 1× | vec![h2], |
| 757 | 1× | vec![TreeOp::RemoveNode { |
| 758 | 1× | node: g, |
| 759 | 1× | placement: OpRef { |
| 760 | 1× | change: h2, |
| 761 | 1× | index: 0, |
| 762 | 1× | }, |
| 763 | 1× | }], |
| 764 | 1× | vec![], |
| 765 | 1× | "four", |
| 766 | | ); |
| 767 | 1× | let h4 = st.add(c4.clone()).unwrap(); |
| 768 | 1× | all.insert(h4, c4); |
| 769 | 3× | let load2 = |h: &ChangeHash| all.get(h).cloned(); |
| 770 | 1× | let p = propagate(&st, h2, None, &Correspondence::default(), &load2).unwrap(); |
| 771 | 1× | assert_eq!(p.notes.len(), 2, "{:?}", p.notes); |
| 772 | 2× | for (_, c) in &p.changes { |
| 773 | 2× | assert!(c.edits.is_empty() && c.tree_ops.is_empty()); |
| 774 | | } |
| 775 | 1× | } |
| 776 | | } |