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