Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 1 | //! Three-way merges: text line by line, JSON by key, and whole trees. |
| 2 | //! |
| 3 | //! The orientation is the same everywhere: `ours` is main and `theirs` is |
| 4 | //! the change being landed or synced. On the server ([`Mode::Land`]) any |
| 5 | //! conflict stops the landing. In a workspace ([`Mode::Sync`]) text conflicts |
| 6 | //! are written into the file with markers, and conflicts that can't be marked |
| 7 | //! (binary files, a file deleted on one side) keep the change's side and say |
| 8 | //! so. |
| 9 |
| 10 | use std::collections::BTreeMap; |
| 11 |
| 12 | use serde_json::Value; |
| 13 |
| 14 | use crate::chunk; |
| 15 | use crate::diff::{Hunk, hunks, is_binary, lines}; |
| 16 | use crate::object::{Kind, Object}; |
| 17 | use crate::tree::{self, EntryKind, TreeEntry}; |
| 18 | use crate::{Id, Layered, MemSource, Result, Source}; |
| 19 |
| 20 | /// The result of a text merge. |
| 21 | pub struct TextMerge { |
| 22 | pub content: Vec<u8>, |
| 23 | pub conflicts: usize, |
| 24 | } |
| 25 |
| 26 | /// Line-by-line three-way merge. Changes on both sides to the same or |
| 27 | /// touching lines are a conflict unless they're identical. |
| 28 | pub fn text(base: &[u8], ours: &[u8], theirs: &[u8], ours_label: &str, theirs_label: &str) -> TextMerge { |
| 29 | let b = lines(base); |
| 30 | let o = lines(ours); |
| 31 | let t = lines(theirs); |
| 32 | let ho = hunks(base, ours); |
| 33 | let ht = hunks(base, theirs); |
| 34 | let mut out = Vec::with_capacity(ours.len().max(theirs.len())); |
| 35 | let mut conflicts = 0; |
| 36 | let (mut i, mut j) = (0, 0); |
| 37 | let mut pos: u32 = 0; |
| 38 | while i < ho.len() || j < ht.len() { |
| 39 | // Start a group at the earliest hunk. |
| 40 | let take_ours = j >= ht.len() || (i < ho.len() && ho[i].before.start <= ht[j].before.start); |
| 41 | let first = if take_ours { &ho[i] } else { &ht[j] }; |
| 42 | let lo = first.before.start; |
| 43 | let mut hi = first.before.end; |
| 44 | let (gi, gj) = (i, j); |
| 45 | if take_ours { |
| 46 | i += 1; |
| 47 | } else { |
| 48 | j += 1; |
| 49 | } |
| 50 | // Absorb every hunk that overlaps or touches the group. |
| 51 | loop { |
| 52 | if i < ho.len() && ho[i].before.start <= hi { |
| 53 | hi = hi.max(ho[i].before.end); |
| 54 | i += 1; |
| 55 | } else if j < ht.len() && ht[j].before.start <= hi { |
| 56 | hi = hi.max(ht[j].before.end); |
| 57 | j += 1; |
| 58 | } else { |
| 59 | break; |
| 60 | } |
| 61 | } |
| 62 | for line in &b[pos as usize..lo as usize] { |
| 63 | out.extend_from_slice(line); |
| 64 | } |
| 65 | let ours_hunks = &ho[gi..i]; |
| 66 | let theirs_hunks = &ht[gj..j]; |
| 67 | let side = |hs: &[Hunk], other: &[&[u8]]| -> Vec<u8> { |
| 68 | let mut text = Vec::new(); |
| 69 | let mut p = lo; |
| 70 | for h in hs { |
| 71 | for line in &b[p as usize..h.before.start as usize] { |
| 72 | text.extend_from_slice(line); |
| 73 | } |
| 74 | for line in &other[h.after.start as usize..h.after.end as usize] { |
| 75 | text.extend_from_slice(line); |
| 76 | } |
| 77 | p = h.before.end; |
| 78 | } |
| 79 | for line in &b[p as usize..hi as usize] { |
| 80 | text.extend_from_slice(line); |
| 81 | } |
| 82 | text |
| 83 | }; |
| 84 | if theirs_hunks.is_empty() { |
| 85 | out.extend_from_slice(&side(ours_hunks, &o)); |
| 86 | } else if ours_hunks.is_empty() { |
| 87 | out.extend_from_slice(&side(theirs_hunks, &t)); |
| 88 | } else { |
| 89 | let a = side(ours_hunks, &o); |
| 90 | let c = side(theirs_hunks, &t); |
| 91 | if a == c { |
| 92 | out.extend_from_slice(&a); |
| 93 | } else { |
| 94 | conflicts += 1; |
| 95 | if !out.is_empty() && !out.ends_with(b"\n") { |
| 96 | out.push(b'\n'); |
| 97 | } |
| 98 | out.extend_from_slice(format!("<<<<<<< {ours_label}\n").as_bytes()); |
| 99 | out.extend_from_slice(&a); |
| 100 | if !a.is_empty() && !a.ends_with(b"\n") { |
| 101 | out.push(b'\n'); |
| 102 | } |
| 103 | out.extend_from_slice(b"=======\n"); |
| 104 | out.extend_from_slice(&c); |
| 105 | if !c.is_empty() && !c.ends_with(b"\n") { |
| 106 | out.push(b'\n'); |
| 107 | } |
| 108 | out.extend_from_slice(format!(">>>>>>> {theirs_label}\n").as_bytes()); |
| 109 | } |
| 110 | } |
| 111 | pos = hi; |
| 112 | } |
| 113 | for line in &b[pos as usize..] { |
| 114 | out.extend_from_slice(line); |
| 115 | } |
| 116 | TextMerge { content: out, conflicts } |
| 117 | } |
| 118 |
| 119 | /// Merges JSON objects key by key. None when the files aren't all JSON |
| 120 | /// objects or both sides changed the same key differently. |
| 121 | pub fn json(base: Option<&[u8]>, ours: &[u8], theirs: &[u8]) -> Option<Vec<u8>> { |
| 122 | let parse = |b: &[u8]| serde_json::from_slice::<Value>(b).ok(); |
| 123 | let o = parse(ours)?; |
| 124 | let t = parse(theirs)?; |
| 125 | let b = match base { |
| 126 | Some(b) => parse(b)?, |
| 127 | None => Value::Object(Default::default()), |
| 128 | }; |
| 129 | if !(o.is_object() && t.is_object() && b.is_object()) { |
| 130 | return None; |
| 131 | } |
| 132 | let merged = json_value(Some(&b), Some(&o), Some(&t))??; |
| 133 | // Write it back in ours' style. |
| 134 | let text = String::from_utf8_lossy(ours); |
| 135 | let indent = text |
| 136 | .lines() |
| 137 | .skip(1) |
| 138 | .find(|l| l.starts_with([' ', '\t'])) |
| 139 | .map(|l| l.chars().take_while(|c| *c == ' ' || *c == '\t').collect::<String>()) |
| 140 | .unwrap_or_else(|| " ".into()); |
| 141 | let mut out = Vec::new(); |
| 142 | let formatter = serde_json::ser::PrettyFormatter::with_indent(indent.as_bytes()); |
| 143 | let mut ser = serde_json::Serializer::with_formatter(&mut out, formatter); |
| 144 | serde::Serialize::serialize(&merged, &mut ser).ok()?; |
| 145 | if ours.ends_with(b"\n") { |
| 146 | out.push(b'\n'); |
| 147 | } |
| 148 | Some(out) |
| 149 | } |
| 150 |
| 151 | /// Some(None): the key is gone. None: a conflict. |
| 152 | fn json_value(b: Option<&Value>, o: Option<&Value>, t: Option<&Value>) -> Option<Option<Value>> { |
| 153 | if o == t { |
| 154 | return Some(o.cloned()); |
| 155 | } |
| 156 | if b == o { |
| 157 | return Some(t.cloned()); |
| 158 | } |
| 159 | if b == t { |
| 160 | return Some(o.cloned()); |
| 161 | } |
| 162 | let (Some(Value::Object(om)), Some(Value::Object(tm))) = (o, t) else { return None }; |
| 163 | let empty = serde_json::Map::new(); |
| 164 | let bm = match b { |
| 165 | Some(Value::Object(m)) => m, |
| 166 | None => &empty, |
| 167 | _ => return None, |
| 168 | }; |
| 169 | let mut out = serde_json::Map::new(); |
| 170 | for (k, ov) in om { |
| 171 | if let Some(v) = json_value(bm.get(k), Some(ov), tm.get(k))? { |
| 172 | out.insert(k.clone(), v); |
| 173 | } |
| 174 | } |
| 175 | // Keys only theirs has, placed after the key they follow in theirs. |
| 176 | let keys: Vec<&String> = tm.keys().collect(); |
| 177 | for (idx, k) in keys.iter().enumerate() { |
| 178 | if om.contains_key(*k) || bm.contains_key(*k) { |
| 179 | if !om.contains_key(*k) { |
| 180 | // Deleted on ours: handled above only for keys ours has. |
| 181 | if let Some(v) = json_value(bm.get(*k), None, tm.get(*k))? { |
| 182 | out.insert((*k).clone(), v); |
| 183 | } |
| 184 | } |
| 185 | continue; |
| 186 | } |
| 187 | let value = tm[*k].clone(); |
| 188 | let after = keys[..idx].iter().rev().find(|p| out.contains_key(**p)).map(|p| (*p).clone()); |
| 189 | insert_after(&mut out, after.as_deref(), (*k).clone(), value); |
| 190 | } |
| 191 | Some(Some(Value::Object(out))) |
| 192 | } |
| 193 |
| 194 | fn insert_after(map: &mut serde_json::Map<String, Value>, after: Option<&str>, key: String, value: Value) { |
| 195 | let Some(after) = after else { |
| 196 | // Theirs put it first. |
| 197 | let rest = std::mem::take(map); |
| 198 | map.insert(key, value); |
| 199 | map.extend(rest); |
| 200 | return; |
| 201 | }; |
| 202 | let rest: Vec<(String, Value)> = std::mem::take(map).into_iter().collect(); |
| 203 | for (k, v) in rest { |
| 204 | let hit = k == after; |
| 205 | map.insert(k, v); |
| 206 | if hit { |
| 207 | map.insert(key.clone(), value.clone()); |
| 208 | } |
| 209 | } |
| 210 | } |
| 211 |
| 212 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 213 | pub enum Mode { |
| 214 | /// Landing on the server: any conflict stops it. |
| 215 | Land, |
| 216 | /// Syncing a workspace: conflicts are written into files. |
| 217 | Sync, |
| 218 | } |
| 219 |
| 220 | /// A conflict the merge found. |
| 221 | #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] |
| 222 | pub struct Conflict { |
| 223 | pub path: String, |
| 224 | /// text, binary, deleted, type. |
| 225 | pub kind: String, |
| 226 | } |
| 227 |
| 228 | /// Something sync resolved on its own that the person should know about. |
| 229 | #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] |
| 230 | pub struct Note { |
| 231 | pub path: String, |
| 232 | pub message: String, |
| 233 | } |
| 234 |
| 235 | pub struct TreeMerge { |
| 236 | pub tree: Id, |
| 237 | /// Objects the merge created (merged blobs and trees). |
| 238 | pub objects: MemSource, |
| 239 | pub conflicts: Vec<Conflict>, |
| 240 | pub notes: Vec<Note>, |
| 241 | } |
| 242 |
| 243 | impl TreeMerge { |
| 244 | pub fn clean(&self) -> bool { |
| 245 | self.conflicts.is_empty() |
| 246 | } |
| 247 | } |
| 248 |
| 249 | /// Merges trees: `base` is where the change started, `ours` is main and |
| 250 | /// `theirs` is the change. |
| 251 | pub fn trees(src: &dyn Source, base: Option<&Id>, ours: &Id, theirs: &Id, mode: Mode, labels: (&str, &str)) -> Result<TreeMerge> { |
| 252 | let mut m = Merger { src, mode, labels, objects: MemSource::default(), conflicts: Vec::new(), notes: Vec::new() }; |
| 253 | let empty = tree::empty_id(); |
| 254 | m.objects.put(tree::empty()); |
| 255 | let base = base.copied().unwrap_or(empty); |
| 256 | let tree = m.merge_tree(&base, ours, theirs, "")?; |
| 257 | Ok(TreeMerge { tree, objects: m.objects, conflicts: m.conflicts, notes: m.notes }) |
| 258 | } |
| 259 |
| 260 | struct Merger<'a> { |
| 261 | src: &'a dyn Source, |
| 262 | mode: Mode, |
| 263 | labels: (&'a str, &'a str), |
| 264 | objects: MemSource, |
| 265 | conflicts: Vec<Conflict>, |
| 266 | notes: Vec<Note>, |
| 267 | } |
| 268 |
| 269 | impl Merger<'_> { |
| 270 | fn read(&self, id: &Id) -> Result<Vec<TreeEntry>> { |
| 271 | let src = Layered { first: &self.objects, second: self.src }; |
| 272 | tree::read(&src, id) |
| 273 | } |
| 274 |
| 275 | fn file(&self, id: &Id) -> Result<Object> { |
| 276 | let src = Layered { first: &self.objects, second: self.src }; |
| 277 | src.must(id) |
| 278 | } |
| 279 |
| 280 | fn merge_tree(&mut self, base: &Id, ours: &Id, theirs: &Id, prefix: &str) -> Result<Id> { |
| 281 | if ours == theirs || base == theirs { |
| 282 | return Ok(*ours); |
| 283 | } |
| 284 | if base == ours { |
| 285 | return Ok(*theirs); |
| 286 | } |
| 287 | let map = |es: Vec<TreeEntry>| es.into_iter().map(|e| (e.name.clone(), e)).collect::<BTreeMap<_, _>>(); |
| 288 | let b = map(self.read(base)?); |
| 289 | let o = map(self.read(ours)?); |
| 290 | let t = map(self.read(theirs)?); |
| 291 | let mut names: Vec<&String> = b.keys().chain(o.keys()).chain(t.keys()).collect(); |
| 292 | names.sort(); |
| 293 | names.dedup(); |
| 294 | let mut out = Vec::new(); |
| 295 | for name in names { |
| 296 | let path = if prefix.is_empty() { name.clone() } else { format!("{prefix}/{name}") }; |
| 297 | if let Some(e) = self.merge_entry(b.get(name), o.get(name), t.get(name), &path)? { |
| 298 | out.push(TreeEntry { name: name.clone(), kind: e.0, id: e.1 }); |
| 299 | } |
| 300 | } |
| 301 | Ok(self.objects.put(Object::new(Kind::Tree, tree::encode(&out)))) |
| 302 | } |
| 303 |
| 304 | fn merge_entry(&mut self, b: Option<&TreeEntry>, o: Option<&TreeEntry>, t: Option<&TreeEntry>, path: &str) -> Result<Option<(EntryKind, Id)>> { |
| 305 | let key = |e: Option<&TreeEntry>| e.map(|e| (e.kind, e.id)); |
| 306 | let (kb, ko, kt) = (key(b), key(o), key(t)); |
| 307 | if ko == kt || kb == kt { |
| 308 | return Ok(ko); |
| 309 | } |
| 310 | if kb == ko { |
| 311 | return Ok(kt); |
| 312 | } |
| 313 | let is_tree = |e: Option<&TreeEntry>| e.is_some_and(|e| e.kind == EntryKind::Tree); |
| 314 | let is_file = |e: Option<&TreeEntry>| e.is_some_and(|e| e.kind != EntryKind::Tree); |
| 315 | match (o, t) { |
| 316 | (Some(oe), Some(te)) if is_tree(o) && is_tree(t) => { |
| 317 | let base = if is_tree(b) { b.unwrap().id } else { tree::empty_id() }; |
| 318 | Ok(Some((EntryKind::Tree, self.merge_tree(&base, &oe.id, &te.id, path)?))) |
| 319 | } |
| 320 | (Some(oe), Some(te)) if is_file(o) && is_file(t) => self.merge_file(b.filter(|e| e.kind != EntryKind::Tree), oe, te, path), |
| 321 | (None, Some(te)) => { |
| 322 | // Main deleted what the change edited. |
| 323 | self.unmarkable(path, "deleted", Some(te), format!("main deleted {path}; kept your version")) |
| 324 | } |
| 325 | (Some(_), None) => { |
| 326 | // The change deleted what main edited. |
| 327 | self.unmarkable(path, "deleted", None, format!("you deleted {path}, which main also changed; `vcs restore {path} --from main` brings main's back")) |
| 328 | } |
| 329 | _ => { |
| 330 | // A file on one side, a folder on the other. |
| 331 | self.unmarkable(path, "type", t, format!("{path} is a file on one side and a folder on the other; kept yours")) |
| 332 | } |
| 333 | } |
| 334 | } |
| 335 |
| 336 | /// A conflict that can't be written with markers: landing stops; sync |
| 337 | /// keeps the change's side (`keep`) and says so. |
| 338 | fn unmarkable(&mut self, path: &str, kind: &'static str, keep: Option<&TreeEntry>, note: String) -> Result<Option<(EntryKind, Id)>> { |
| 339 | self.conflicts.push(Conflict { path: path.to_string(), kind: kind.to_string() }); |
| 340 | if self.mode == Mode::Sync { |
| 341 | self.conflicts.pop(); |
| 342 | self.notes.push(Note { path: path.to_string(), message: note }); |
| 343 | } |
| 344 | Ok(keep.map(|e| (e.kind, e.id))) |
| 345 | } |
| 346 |
| 347 | fn merge_file(&mut self, b: Option<&TreeEntry>, o: &TreeEntry, t: &TreeEntry, path: &str) -> Result<Option<(EntryKind, Id)>> { |
| 348 | // The kind (exec bit, link) merges on its own. |
| 349 | let kind = if o.kind == t.kind { |
| 350 | o.kind |
| 351 | } else if b.is_some_and(|b| b.kind == o.kind) { |
| 352 | t.kind |
| 353 | } else if b.is_some_and(|b| b.kind == t.kind) { |
| 354 | o.kind |
| 355 | } else { |
| 356 | t.kind |
| 357 | }; |
| 358 | if o.id == t.id { |
| 359 | return Ok(Some((kind, o.id))); |
| 360 | } |
| 361 | if b.is_some_and(|b| b.id == o.id) { |
| 362 | return Ok(Some((kind, t.id))); |
| 363 | } |
| 364 | if b.is_some_and(|b| b.id == t.id) { |
| 365 | return Ok(Some((kind, o.id))); |
| 366 | } |
| 367 | let oo = self.file(&o.id)?; |
| 368 | let to = self.file(&t.id)?; |
| 369 | let bo = match b { |
| 370 | Some(b) => Some(self.file(&b.id)?), |
| 371 | None => None, |
| 372 | }; |
| 373 | let text = |obj: &Object| obj.kind == Kind::Blob && !is_binary(&obj.data); |
| 374 | let all_text = text(&oo) && text(&to) && bo.as_ref().is_none_or(text) && o.kind != EntryKind::Link && t.kind != EntryKind::Link; |
| 375 | if !all_text { |
| 376 | return self.unmarkable(path, "binary", Some(t), format!("{path} changed on main and in your change and can't be merged line by line; kept yours, `vcs restore {path} --from main` takes main's")); |
| 377 | } |
| 378 | let base_bytes: &[u8] = bo.as_ref().map(|b| b.data.as_slice()).unwrap_or(b""); |
| 379 | let merged = text_merge(base_bytes, &oo.data, &to.data, self.labels.0, self.labels.1); |
| 380 | if merged.conflicts == 0 { |
| 381 | let id = self.put_file(merged.content); |
| 382 | return Ok(Some((kind, id))); |
| 383 | } |
| 384 | if path.ends_with(".json") |
| 385 | && let Some(content) = json(bo.as_ref().map(|b| b.data.as_slice()), &oo.data, &to.data) |
| 386 | { |
| 387 | let id = self.put_file(content); |
| 388 | return Ok(Some((kind, id))); |
| 389 | } |
| 390 | self.conflicts.push(Conflict { path: path.to_string(), kind: "text".into() }); |
| 391 | match self.mode { |
| 392 | Mode::Land => Ok(Some((kind, t.id))), |
| 393 | Mode::Sync => { |
| 394 | let id = self.put_file(merged.content); |
| 395 | Ok(Some((kind, id))) |
| 396 | } |
| 397 | } |
| 398 | } |
| 399 |
| 400 | fn put_file(&mut self, content: Vec<u8>) -> Id { |
| 401 | let objects = chunk::file_objects(&content); |
| 402 | let id = objects[0].id(); |
| 403 | for o in objects { |
| 404 | self.objects.put(o); |
| 405 | } |
| 406 | id |
| 407 | } |
| 408 | } |
| 409 |
| 410 | fn text_merge(base: &[u8], ours: &[u8], theirs: &[u8], ol: &str, tl: &str) -> TextMerge { |
| 411 | text(base, ours, theirs, ol, tl) |
| 412 | } |
| 413 |
| 414 | #[cfg(test)] |
| 415 | mod tests { |
| 416 | use super::*; |
| 417 | use crate::tree::{Entry, Files, build, flatten}; |
| 418 |
| 419 | #[test] |
| 420 | fn clean_text_merge() { |
| 421 | let base = b"a\nb\nc\nd\ne\nf\n"; |
| 422 | let ours = b"A\nb\nc\nd\ne\nf\n"; |
| 423 | let theirs = b"a\nb\nc\nd\ne\nF\n"; |
| 424 | let m = text(base, ours, theirs, "main", "change"); |
| 425 | assert_eq!(m.conflicts, 0); |
| 426 | assert_eq!(m.content, b"A\nb\nc\nd\ne\nF\n"); |
| 427 | } |
| 428 |
| 429 | #[test] |
| 430 | fn conflicting_text_merge() { |
| 431 | let base = b"a\nb\nc\n"; |
| 432 | let ours = b"a\nB1\nc\n"; |
| 433 | let theirs = b"a\nB2\nc\n"; |
| 434 | let m = text(base, ours, theirs, "main 4", "your change bcdf"); |
| 435 | assert_eq!(m.conflicts, 1); |
| 436 | assert_eq!(String::from_utf8(m.content).unwrap(), "a\n<<<<<<< main 4\nB1\n=======\nB2\n>>>>>>> your change bcdf\nc\n"); |
| 437 | } |
| 438 |
| 439 | #[test] |
| 440 | fn same_change_both_sides() { |
| 441 | let m = text(b"a\nb\n", b"a\nX\n", b"a\nX\n", "o", "t"); |
| 442 | assert_eq!(m.conflicts, 0); |
| 443 | assert_eq!(m.content, b"a\nX\n"); |
| 444 | } |
| 445 |
| 446 | #[test] |
| 447 | fn json_keys_merge() { |
| 448 | let base = b"{\n \"a\": \"1\"\n}\n"; |
| 449 | let ours = b"{\n \"a\": \"1\",\n \"b\": \"2\"\n}\n"; |
| 450 | let theirs = b"{\n \"a\": \"1\",\n \"c\": \"3\"\n}\n"; |
| 451 | // Line by line this is a conflict (both added after "a"). |
| 452 | assert_eq!(text(base, ours, theirs, "o", "t").conflicts, 1); |
| 453 | let merged = json(Some(base), ours, theirs).unwrap(); |
| 454 | let v: Value = serde_json::from_slice(&merged).unwrap(); |
| 455 | assert_eq!(v["b"], "2"); |
| 456 | assert_eq!(v["c"], "3"); |
| 457 | assert!(json(Some(base), b"{\"a\": \"x\"}", b"{\"a\": \"y\"}").is_none()); |
| 458 | } |
| 459 |
| 460 | fn put_files(src: &mut MemSource, list: &[(&str, &str)]) -> Id { |
| 461 | let files: Files = list |
| 462 | .iter() |
| 463 | .map(|(p, c)| (p.to_string(), Entry { kind: EntryKind::File, id: src.put(Object::blob(c.as_bytes().to_vec())) })) |
| 464 | .collect(); |
| 465 | let (root, trees) = build(&files); |
| 466 | for t in trees { |
| 467 | src.put(t); |
| 468 | } |
| 469 | root |
| 470 | } |
| 471 |
| 472 | #[test] |
| 473 | fn tree_merges() { |
| 474 | let mut src = MemSource::default(); |
| 475 | let base = put_files(&mut src, &[("a.txt", "a\nb\nc\n"), ("src/x.rs", "x\n"), ("keys.json", "{\n \"a\": 1\n}\n")]); |
| 476 | let ours = put_files(&mut src, &[("a.txt", "A\nb\nc\n"), ("src/x.rs", "x\n"), ("src/new.rs", "n\n"), ("keys.json", "{\n \"a\": 1,\n \"b\": 2\n}\n")]); |
| 477 | let theirs = put_files(&mut src, &[("a.txt", "a\nb\nC\n"), ("src/x.rs", "x2\n"), ("keys.json", "{\n \"a\": 1,\n \"c\": 3\n}\n")]); |
| 478 | let m = trees(&src, Some(&base), &ours, &theirs, Mode::Land, ("main", "change")).unwrap(); |
| 479 | assert!(m.clean(), "{:?}", m.conflicts); |
| 480 | let layered = Layered { first: &m.objects, second: &src }; |
| 481 | let files = flatten(&layered, &m.tree).unwrap(); |
| 482 | let read = |p: &str| String::from_utf8(layered.must(&files[p].id).unwrap().data).unwrap(); |
| 483 | assert_eq!(read("a.txt"), "A\nb\nC\n"); |
| 484 | assert_eq!(read("src/x.rs"), "x2\n"); |
| 485 | assert_eq!(read("src/new.rs"), "n\n"); |
| 486 | let keys: Value = serde_json::from_str(&read("keys.json")).unwrap(); |
| 487 | assert_eq!((keys["b"].as_i64(), keys["c"].as_i64()), (Some(2), Some(3))); |
| 488 | } |
| 489 |
| 490 | #[test] |
| 491 | fn tree_conflicts() { |
| 492 | let mut src = MemSource::default(); |
| 493 | let base = put_files(&mut src, &[("a.txt", "a\n"), ("gone.txt", "g\n")]); |
| 494 | let ours = put_files(&mut src, &[("a.txt", "main\n")]); |
| 495 | let theirs = put_files(&mut src, &[("a.txt", "change\n"), ("gone.txt", "edited\n")]); |
| 496 | let land = trees(&src, Some(&base), &ours, &theirs, Mode::Land, ("main", "change")).unwrap(); |
| 497 | let mut paths: Vec<_> = land.conflicts.iter().map(|c| (c.path.as_str(), c.kind.as_str())).collect(); |
| 498 | paths.sort(); |
| 499 | assert_eq!(paths, vec![("a.txt", "text"), ("gone.txt", "deleted")]); |
| 500 | let sync = trees(&src, Some(&base), &ours, &theirs, Mode::Sync, ("main 2", "your change x")).unwrap(); |
| 501 | assert_eq!(sync.conflicts.len(), 1); |
| 502 | assert_eq!(sync.notes.len(), 1); |
| 503 | let layered = Layered { first: &sync.objects, second: &src }; |
| 504 | let files = flatten(&layered, &sync.tree).unwrap(); |
| 505 | let a = String::from_utf8(layered.must(&files["a.txt"].id).unwrap().data).unwrap(); |
| 506 | assert!(a.contains("<<<<<<< main 2")); |
| 507 | assert!(files.contains_key("gone.txt")); |
| 508 | } |
| 509 |
| 510 | #[test] |
| 511 | fn fast_paths() { |
| 512 | let mut src = MemSource::default(); |
| 513 | let base = put_files(&mut src, &[("a.txt", "a\n")]); |
| 514 | let theirs = put_files(&mut src, &[("a.txt", "b\n")]); |
| 515 | let m = trees(&src, Some(&base), &base, &theirs, Mode::Land, ("main", "change")).unwrap(); |
| 516 | assert_eq!(m.tree, theirs); |
| 517 | } |
| 518 | } |
| 519 |