1use serde::{Deserialize, Serialize};
2use toml_edit::{Array, ArrayOfTables, DocumentMut, InlineTable, Item, Table, Value};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "kebab-case")]
6pub enum FileFormat {
7 Toml,
8 Json,
9 Yaml,
10 Text,
11}
12
13impl FileFormat {
14 pub fn from_path(path: &str) -> Self {
15 let lower = path.trim().to_ascii_lowercase();
16 if lower.ends_with(".toml") {
17 Self::Toml
18 } else if lower.ends_with(".json") {
19 Self::Json
20 } else if lower.ends_with(".yml") || lower.ends_with(".yaml") {
21 Self::Yaml
22 } else {
23 Self::Text
24 }
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct StructuralConflict {
30 pub key_path: String,
31 pub base: String,
32 pub ours: String,
33 pub theirs: String,
34}
35
36fn format_conflicts(format_label: &str, conflicts: &[StructuralConflict]) -> String {
37 let mut out = format!("# Structural Merge Conflict ({format_label})\n");
38 for c in conflicts {
39 out.push_str(&format!(
40 "<<<<<<< ours: {}\n{}\n||||||| base: {}\n{}\n=======\n{}\n>>>>>>> theirs: {}\n",
41 c.key_path, c.ours, c.key_path, c.base, c.theirs, c.key_path
42 ));
43 }
44 out
45}
46
47pub fn three_way_merge_by_path(
48 target_path: &str,
49 base: &str,
50 ours: &str,
51 theirs: &str,
52) -> Result<String, String> {
53 match FileFormat::from_path(target_path) {
54 FileFormat::Toml => {
55 if let Some(res) = try_merge_toml_structural(base, ours, theirs) {
56 res
57 } else {
58 diffy::merge(base, ours, theirs)
59 }
60 }
61 FileFormat::Json => {
62 if let Some(res) = try_merge_json_structural(base, ours, theirs) {
63 res
64 } else {
65 diffy::merge(base, ours, theirs)
66 }
67 }
68 FileFormat::Yaml => {
69 if let Some(res) = try_merge_yaml_structural(base, ours, theirs) {
70 res
71 } else {
72 diffy::merge(base, ours, theirs)
73 }
74 }
75 FileFormat::Text => diffy::merge(base, ours, theirs),
76 }
77}
78
79pub fn merge_toml_structural(base: &str, ours: &str, theirs: &str) -> Result<String, String> {
80 match try_merge_toml_structural(base, ours, theirs) {
81 Some(result) => result,
82 None => Err("at least one input document is not valid TOML".to_string()),
83 }
84}
85
86fn try_merge_toml_structural(
87 base: &str,
88 ours: &str,
89 theirs: &str,
90) -> Option<Result<String, String>> {
91 let base_doc = base.parse::<DocumentMut>().ok()?;
92 let mut ours_doc = ours.parse::<DocumentMut>().ok()?;
93 let theirs_doc = theirs.parse::<DocumentMut>().ok()?;
94
95 let mut conflicts = Vec::new();
96 merge_toml_tables(
97 Some(base_doc.as_table()),
98 ours_doc.as_table_mut(),
99 theirs_doc.as_table(),
100 "",
101 &mut conflicts,
102 );
103
104 if conflicts.is_empty() {
105 Some(Ok(ours_doc.to_string()))
106 } else {
107 Some(Err(format_conflicts("TOML", &conflicts)))
108 }
109}
110
111fn collect_table_keys(base: Option<&Table>, ours: &Table, theirs: &Table) -> Vec<String> {
112 let mut keys = Vec::new();
113 if let Some(b) = base {
114 for (k, item) in b.iter() {
115 if !item.is_none() && !keys.iter().any(|existing| existing == k) {
116 keys.push(k.to_string());
117 }
118 }
119 }
120 for (k, item) in ours.iter() {
121 if !item.is_none() && !keys.iter().any(|existing| existing == k) {
122 keys.push(k.to_string());
123 }
124 }
125 for (k, item) in theirs.iter() {
126 if !item.is_none() && !keys.iter().any(|existing| existing == k) {
127 keys.push(k.to_string());
128 }
129 }
130 keys
131}
132
133fn item_as_table_clone(item: &Item) -> Option<Table> {
134 match item {
135 Item::Table(t) => Some(t.clone()),
136 Item::Value(Value::InlineTable(it)) => Some(it.clone().into_table()),
137 _ => None,
138 }
139}
140
141fn item_is_table_like(item: &Item) -> bool {
142 matches!(item, Item::Table(_) | Item::Value(Value::InlineTable(_)))
143}
144
145fn merge_toml_tables(
146 base: Option<&Table>,
147 ours: &mut Table,
148 theirs: &Table,
149 prefix: &str,
150 conflicts: &mut Vec<StructuralConflict>,
151) {
152 let keys = collect_table_keys(base, ours, theirs);
153
154 for k in keys {
155 let key_path = if prefix.is_empty() {
156 k.clone()
157 } else {
158 format!("{prefix}.{k}")
159 };
160
161 let b_item = base.and_then(|t| t.get(&k)).filter(|i| !i.is_none());
162 let o_item = ours.get(&k).filter(|i| !i.is_none()).cloned();
163 let t_item = theirs.get(&k).filter(|i| !i.is_none());
164
165 let both_table_like = match (o_item.as_ref(), t_item) {
166 (Some(o), Some(t)) => {
167 item_is_table_like(o)
168 && item_is_table_like(t)
169 && b_item.is_none_or(item_is_table_like)
170 }
171 _ => false,
172 };
173
174 if both_table_like {
175 match ours.get_mut(&k) {
176 Some(Item::Table(o_subtable)) => {
177 let b_sub = b_item.and_then(item_as_table_clone);
178 if let Some(t_sub) = t_item.and_then(item_as_table_clone) {
179 merge_toml_tables(b_sub.as_ref(), o_subtable, &t_sub, &key_path, conflicts);
180 }
181 continue;
182 }
183 Some(Item::Value(Value::InlineTable(o_inline))) => {
184 if let Some(Item::Value(Value::InlineTable(t_inline))) = t_item {
185 let b_inline = match b_item {
186 Some(Item::Value(Value::InlineTable(bi))) => Some(bi),
187 _ => None,
188 };
189 merge_toml_inline_tables(
190 b_inline, o_inline, t_inline, &key_path, conflicts,
191 );
192 continue;
193 } else if let Some(t_sub) = t_item.and_then(item_as_table_clone) {
194 let b_sub = b_item.and_then(item_as_table_clone);
195 let mut promoted = o_inline.clone().into_table();
196 merge_toml_tables(
197 b_sub.as_ref(),
198 &mut promoted,
199 &t_sub,
200 &key_path,
201 conflicts,
202 );
203 ours.insert(&k, Item::Table(promoted));
204 continue;
205 }
206 }
207 _ => {}
208 }
209 }
210
211 let b_eq_t = opt_toml_items_equal(b_item, t_item);
212 let b_eq_o = opt_toml_items_equal(b_item, o_item.as_ref());
213 let o_eq_t = opt_toml_items_equal(o_item.as_ref(), t_item);
214
215 if b_eq_t || o_eq_t {
216 } else if b_eq_o {
218 match t_item {
219 Some(t_val) => {
220 apply_theirs_item_to_ours_table(ours, theirs, &k, t_val);
221 }
222 None => {
223 ours.remove(&k);
224 }
225 }
226 } else {
227 conflicts.push(StructuralConflict {
228 key_path,
229 base: format_opt_toml_item(b_item),
230 ours: format_opt_toml_item(o_item.as_ref()),
231 theirs: format_opt_toml_item(t_item),
232 });
233 }
234 }
235}
236
237fn apply_theirs_item_to_ours_table(ours: &mut Table, theirs: &Table, key: &str, t_val: &Item) {
238 if let (Some(Item::Value(existing_val)), Item::Value(new_val)) = (ours.get_mut(key), t_val) {
239 let prefix = existing_val.decor().prefix().cloned();
240 let suffix = existing_val.decor().suffix().cloned();
241 let mut updated = new_val.clone();
242 if let Some(p) = prefix {
243 updated.decor_mut().set_prefix(p);
244 }
245 if let Some(s) = suffix {
246 updated.decor_mut().set_suffix(s);
247 }
248 *existing_val = updated;
249 } else if !ours.contains_key(key) {
250 if let Some((theirs_key, _)) = theirs.get_key_value(key) {
251 ours.insert_formatted(theirs_key, t_val.clone());
252 } else {
253 ours.insert(key, t_val.clone());
254 }
255 } else {
256 ours.insert(key, t_val.clone());
257 }
258}
259
260fn merge_toml_inline_tables(
261 base: Option<&InlineTable>,
262 ours: &mut InlineTable,
263 theirs: &InlineTable,
264 prefix: &str,
265 conflicts: &mut Vec<StructuralConflict>,
266) {
267 let mut keys = Vec::new();
268 if let Some(b) = base {
269 for (k, _) in b.iter() {
270 if !keys.iter().any(|existing| existing == k) {
271 keys.push(k.to_string());
272 }
273 }
274 }
275 for (k, _) in ours.iter() {
276 if !keys.iter().any(|existing| existing == k) {
277 keys.push(k.to_string());
278 }
279 }
280 for (k, _) in theirs.iter() {
281 if !keys.iter().any(|existing| existing == k) {
282 keys.push(k.to_string());
283 }
284 }
285
286 for k in keys {
287 let key_path = format!("{prefix}.{k}");
288 let b_val = base.and_then(|t| t.get(&k));
289 let o_val = ours.get(&k).cloned();
290 let t_val = theirs.get(&k);
291
292 if let (Some(Value::InlineTable(_)), Some(Value::InlineTable(t_sub))) =
293 (o_val.as_ref(), t_val)
294 && b_val.is_none_or(|v| matches!(v, Value::InlineTable(_)))
295 && let Some(Value::InlineTable(o_sub)) = ours.get_mut(&k)
296 {
297 let b_sub = match b_val {
298 Some(Value::InlineTable(bi)) => Some(bi),
299 _ => None,
300 };
301 merge_toml_inline_tables(b_sub, o_sub, t_sub, &key_path, conflicts);
302 continue;
303 }
304
305 let b_eq_t = opt_toml_values_equal(b_val, t_val);
306 let b_eq_o = opt_toml_values_equal(b_val, o_val.as_ref());
307 let o_eq_t = opt_toml_values_equal(o_val.as_ref(), t_val);
308
309 if b_eq_t || o_eq_t {
310 } else if b_eq_o {
312 match t_val {
313 Some(tv) => {
314 ours.insert(&k, tv.clone());
315 }
316 None => {
317 ours.remove(&k);
318 }
319 }
320 } else {
321 conflicts.push(StructuralConflict {
322 key_path,
323 base: b_val
324 .map(|v| v.to_string().trim().to_string())
325 .unwrap_or_else(|| "<none>".to_string()),
326 ours: o_val
327 .as_ref()
328 .map(|v| v.to_string().trim().to_string())
329 .unwrap_or_else(|| "<deleted>".to_string()),
330 theirs: t_val
331 .map(|v| v.to_string().trim().to_string())
332 .unwrap_or_else(|| "<deleted>".to_string()),
333 });
334 }
335 }
336}
337
338fn opt_toml_items_equal(a: Option<&Item>, b: Option<&Item>) -> bool {
339 match (a, b) {
340 (None, None) => true,
341 (Some(ia), Some(ib)) => toml_items_equal(ia, ib),
342 _ => false,
343 }
344}
345
346fn toml_items_equal(a: &Item, b: &Item) -> bool {
347 match (a, b) {
348 (Item::None, Item::None) => true,
349 (Item::Value(va), Item::Value(vb)) => toml_values_equal(va, vb),
350 (Item::Table(ta), Item::Table(tb)) => toml_tables_equal(ta, tb),
351 (Item::Table(ta), Item::Value(Value::InlineTable(ib)))
352 | (Item::Value(Value::InlineTable(ib)), Item::Table(ta)) => {
353 toml_tables_equal(ta, &ib.clone().into_table())
354 }
355 (Item::ArrayOfTables(aa), Item::ArrayOfTables(ab)) => toml_array_of_tables_equal(aa, ab),
356 _ => false,
357 }
358}
359
360fn opt_toml_values_equal(a: Option<&Value>, b: Option<&Value>) -> bool {
361 match (a, b) {
362 (None, None) => true,
363 (Some(va), Some(vb)) => toml_values_equal(va, vb),
364 _ => false,
365 }
366}
367
368fn toml_values_equal(a: &Value, b: &Value) -> bool {
369 match (a, b) {
370 (Value::String(sa), Value::String(sb)) => sa.value() == sb.value(),
371 (Value::Integer(ia), Value::Integer(ib)) => ia.value() == ib.value(),
372 (Value::Float(fa), Value::Float(fb)) => fa.value().to_bits() == fb.value().to_bits(),
373 (Value::Boolean(ba), Value::Boolean(bb)) => ba.value() == bb.value(),
374 (Value::Datetime(da), Value::Datetime(db)) => da.value() == db.value(),
375 (Value::Array(aa), Value::Array(ab)) => toml_arrays_equal(aa, ab),
376 (Value::InlineTable(ta), Value::InlineTable(tb)) => toml_inline_tables_equal(ta, tb),
377 _ => false,
378 }
379}
380
381fn toml_arrays_equal(a: &Array, b: &Array) -> bool {
382 a.len() == b.len()
383 && a.iter()
384 .zip(b.iter())
385 .all(|(va, vb)| toml_values_equal(va, vb))
386}
387
388fn toml_inline_tables_equal(a: &InlineTable, b: &InlineTable) -> bool {
389 if a.len() != b.len() {
390 return false;
391 }
392 a.iter()
393 .all(|(k, va)| b.get(k).is_some_and(|vb| toml_values_equal(va, vb)))
394}
395
396fn toml_tables_equal(a: &Table, b: &Table) -> bool {
397 let a_keys: Vec<_> = a.iter().filter(|(_, i)| !i.is_none()).collect();
398 let b_keys: Vec<_> = b.iter().filter(|(_, i)| !i.is_none()).collect();
399 if a_keys.len() != b_keys.len() {
400 return false;
401 }
402 a_keys.iter().all(|(k, ia)| {
403 b.get(k)
404 .filter(|ib| !ib.is_none())
405 .is_some_and(|ib| toml_items_equal(ia, ib))
406 })
407}
408
409fn toml_array_of_tables_equal(a: &ArrayOfTables, b: &ArrayOfTables) -> bool {
410 a.len() == b.len()
411 && a.iter()
412 .zip(b.iter())
413 .all(|(ta, tb)| toml_tables_equal(ta, tb))
414}
415
416fn format_opt_toml_item(item: Option<&Item>) -> String {
417 match item {
418 None | Some(Item::None) => "<none>".to_string(),
419 Some(Item::Value(v)) => v.to_string().trim().to_string(),
420 Some(Item::Table(t)) => t.to_string().trim().to_string(),
421 Some(Item::ArrayOfTables(a)) => format!("[({} array of tables)]", a.len()),
422 }
423}
424
425pub fn merge_json_structural(base: &str, ours: &str, theirs: &str) -> Result<String, String> {
426 match try_merge_json_structural(base, ours, theirs) {
427 Some(result) => result,
428 None => Err("at least one input document is not valid JSON".to_string()),
429 }
430}
431
432fn try_merge_json_structural(
433 base: &str,
434 ours: &str,
435 theirs: &str,
436) -> Option<Result<String, String>> {
437 let base_val: serde_json::Value = serde_json::from_str(base).ok()?;
438 let ours_val: serde_json::Value = serde_json::from_str(ours).ok()?;
439 let theirs_val: serde_json::Value = serde_json::from_str(theirs).ok()?;
440
441 let mut conflicts = Vec::new();
442 let merged_val = merge_json_values(Some(&base_val), &ours_val, &theirs_val, "", &mut conflicts);
443
444 if conflicts.is_empty() {
445 let mut pretty = serde_json::to_string_pretty(&merged_val).ok()?;
446 pretty.push('\n');
447 Some(Ok(pretty))
448 } else {
449 Some(Err(format_conflicts("JSON", &conflicts)))
450 }
451}
452
453fn merge_json_values(
454 base: Option<&serde_json::Value>,
455 ours: &serde_json::Value,
456 theirs: &serde_json::Value,
457 prefix: &str,
458 conflicts: &mut Vec<StructuralConflict>,
459) -> serde_json::Value {
460 if let (serde_json::Value::Object(o_map), serde_json::Value::Object(t_map)) = (ours, theirs)
461 && base.is_none_or(|b| b.is_object())
462 {
463 let b_map = base.and_then(|b| b.as_object());
464 let mut keys = Vec::new();
465 if let Some(bm) = b_map {
466 for k in bm.keys() {
467 if !keys.contains(k) {
468 keys.push(k.clone());
469 }
470 }
471 }
472 for k in o_map.keys() {
473 if !keys.contains(k) {
474 keys.push(k.clone());
475 }
476 }
477 for k in t_map.keys() {
478 if !keys.contains(k) {
479 keys.push(k.clone());
480 }
481 }
482
483 let mut merged = serde_json::Map::new();
484 for k in keys {
485 let key_path = if prefix.is_empty() {
486 k.clone()
487 } else {
488 format!("{prefix}.{k}")
489 };
490
491 let b_v = b_map.and_then(|m| m.get(&k));
492 let o_v = o_map.get(&k);
493 let t_v = t_map.get(&k);
494
495 if let (Some(o_sub), Some(t_sub)) = (o_v, t_v)
496 && o_sub.is_object()
497 && t_sub.is_object()
498 && b_v.is_none_or(|bv| bv.is_object())
499 {
500 let sub = merge_json_values(b_v, o_sub, t_sub, &key_path, conflicts);
501 merged.insert(k, sub);
502 continue;
503 }
504
505 if b_v == t_v {
506 if let Some(ov) = o_v {
507 merged.insert(k, ov.clone());
508 }
509 } else if b_v == o_v {
510 if let Some(tv) = t_v {
511 merged.insert(k, tv.clone());
512 }
513 } else if o_v == t_v {
514 if let Some(ov) = o_v {
515 merged.insert(k, ov.clone());
516 }
517 } else {
518 conflicts.push(StructuralConflict {
519 key_path,
520 base: format_opt_json(b_v),
521 ours: format_opt_json(o_v),
522 theirs: format_opt_json(t_v),
523 });
524 }
525 }
526
527 return serde_json::Value::Object(merged);
528 }
529
530 if base == Some(theirs) {
531 ours.clone()
532 } else if base == Some(ours) {
533 theirs.clone()
534 } else if ours == theirs {
535 ours.clone()
536 } else {
537 let key_path = if prefix.is_empty() {
538 "$".to_string()
539 } else {
540 prefix.to_string()
541 };
542 conflicts.push(StructuralConflict {
543 key_path,
544 base: format_opt_json(base),
545 ours: format_opt_json(Some(ours)),
546 theirs: format_opt_json(Some(theirs)),
547 });
548 ours.clone()
549 }
550}
551
552fn format_opt_json(val: Option<&serde_json::Value>) -> String {
553 match val {
554 None => "<none>".to_string(),
555 Some(v) => serde_json::to_string(v).unwrap_or_else(|_| v.to_string()),
556 }
557}
558
559pub fn merge_yaml_structural(base: &str, ours: &str, theirs: &str) -> Result<String, String> {
560 match try_merge_yaml_structural(base, ours, theirs) {
561 Some(result) => result,
562 None => Err("at least one input document is not valid block YAML".to_string()),
563 }
564}
565
566#[derive(Debug, Clone, PartialEq, Eq)]
567struct YamlMap {
568 entries: Vec<YamlEntry>,
569 trailing_lines: Vec<String>,
570}
571
572#[derive(Debug, Clone, PartialEq, Eq)]
573struct YamlEntry {
574 leading_trivia: Vec<String>,
575 key: String,
576 header_line: String,
577 body: YamlNode,
578}
579
580#[derive(Debug, Clone, PartialEq, Eq)]
581enum YamlNode {
582 Scalar(String),
583 Mapping(YamlMap),
584 Sequence(Vec<YamlSeqItem>),
585 RawBlock(Vec<String>),
586}
587
588#[derive(Debug, Clone, PartialEq, Eq)]
589struct YamlSeqItem {
590 leading_trivia: Vec<String>,
591 lines: Vec<String>,
592 canonical: String,
593}
594
595fn try_merge_yaml_structural(
596 base: &str,
597 ours: &str,
598 theirs: &str,
599) -> Option<Result<String, String>> {
600 let base_map = parse_yaml_map_root(base)?;
601 let ours_map = parse_yaml_map_root(ours)?;
602 let theirs_map = parse_yaml_map_root(theirs)?;
603
604 let mut conflicts = Vec::new();
605 let merged_map = merge_yaml_maps(Some(&base_map), &ours_map, &theirs_map, "", &mut conflicts);
606
607 if conflicts.is_empty() {
608 let mut rendered = String::new();
609 emit_yaml_map(&merged_map, &mut rendered);
610 if !rendered.ends_with('\n') && !rendered.is_empty() {
611 rendered.push('\n');
612 }
613 Some(Ok(rendered))
614 } else {
615 Some(Err(format_conflicts("YAML", &conflicts)))
616 }
617}
618
619fn parse_yaml_map_root(input: &str) -> Option<YamlMap> {
620 let lines: Vec<String> = input.lines().map(str::to_string).collect();
621 if lines.is_empty() {
622 return None;
623 }
624 parse_yaml_map_slice(&lines, 0)
625}
626
627fn line_indent(line: &str) -> usize {
628 line.bytes().take_while(|b| *b == b' ').count()
629}
630
631fn is_trivia_line(line: &str) -> bool {
632 let trimmed = line.trim();
633 trimmed.is_empty() || trimmed.starts_with('#') || trimmed == "---"
634}
635
636fn split_yaml_key_value(trimmed: &str) -> Option<(&str, &str)> {
637 if trimmed.starts_with("- ") || trimmed == "-" || trimmed.starts_with('#') {
638 return None;
639 }
640 let colon_idx = trimmed.find(':')?;
641 let key = trimmed[..colon_idx].trim();
642 if key.is_empty() || key.contains(' ') || key.contains('\t') {
643 return None;
644 }
645 let after = &trimmed[colon_idx + 1..];
646 if !after.is_empty() && !after.starts_with(' ') && !after.starts_with('\t') {
647 return None;
648 }
649 let val_no_comment = strip_yaml_inline_comment(after.trim());
650 Some((key, val_no_comment))
651}
652
653fn strip_yaml_inline_comment(val: &str) -> &str {
654 let mut in_single = false;
655 let mut in_double = false;
656 let bytes = val.as_bytes();
657 for i in 0..bytes.len() {
658 match bytes[i] {
659 b'\'' if !in_double => in_single = !in_single,
660 b'"' if !in_single => in_double = !in_double,
661 b'#' if !in_single && !in_double && (i == 0 || bytes[i - 1] == b' ') => {
662 return val[..i].trim_end();
663 }
664 _ => {}
665 }
666 }
667 val.trim()
668}
669
670fn parse_yaml_map_slice(lines: &[String], expected_indent: usize) -> Option<YamlMap> {
671 let mut entries = Vec::new();
672 let mut pending_trivia = Vec::new();
673 let mut idx = 0;
674
675 while idx < lines.len() {
676 let line = &lines[idx];
677 if is_trivia_line(line) {
678 pending_trivia.push(line.clone());
679 idx += 1;
680 continue;
681 }
682 if line.starts_with('\t') {
683 return None;
684 }
685
686 let indent = line_indent(line);
687 if indent != expected_indent {
688 return None;
689 }
690
691 let trimmed = &line[indent..];
692 let (key, inline_val) = split_yaml_key_value(trimmed)?;
693 let header_line = line.clone();
694 let leading_trivia = std::mem::take(&mut pending_trivia);
695 idx += 1;
696
697 let child_start = idx;
698 while idx < lines.len() {
699 let next_line = &lines[idx];
700 if is_trivia_line(next_line) {
701 let mut lookahead = idx + 1;
702 let mut has_deeper_child = false;
703 while lookahead < lines.len() {
704 if !is_trivia_line(&lines[lookahead]) {
705 if line_indent(&lines[lookahead]) > expected_indent {
706 has_deeper_child = true;
707 }
708 break;
709 }
710 lookahead += 1;
711 }
712 if has_deeper_child {
713 idx += 1;
714 continue;
715 } else {
716 break;
717 }
718 }
719 if line_indent(next_line) > expected_indent {
720 idx += 1;
721 } else {
722 break;
723 }
724 }
725
726 let child_lines = &lines[child_start..idx];
727 let body = if child_lines.is_empty() {
728 YamlNode::Scalar(inline_val.to_string())
729 } else if inline_val == "|" || inline_val == ">" || inline_val == "|-" || inline_val == ">-"
730 {
731 YamlNode::RawBlock(child_lines.to_vec())
732 } else {
733 let first_non_trivia = child_lines.iter().find(|l| !is_trivia_line(l))?;
734 let child_indent = line_indent(first_non_trivia);
735 let child_trimmed = &first_non_trivia[child_indent..];
736 if child_trimmed.starts_with("- ") || child_trimmed == "-" {
737 let seq = parse_yaml_sequence_slice(child_lines, child_indent)?;
738 YamlNode::Sequence(seq)
739 } else if let Some(sub_map) = parse_yaml_map_slice(child_lines, child_indent) {
740 YamlNode::Mapping(sub_map)
741 } else {
742 YamlNode::RawBlock(child_lines.to_vec())
743 }
744 };
745
746 entries.push(YamlEntry {
747 leading_trivia,
748 key: key.to_string(),
749 header_line,
750 body,
751 });
752 }
753
754 if entries.is_empty() {
755 return None;
756 }
757
758 Some(YamlMap {
759 entries,
760 trailing_lines: pending_trivia,
761 })
762}
763
764fn parse_yaml_sequence_slice(lines: &[String], seq_indent: usize) -> Option<Vec<YamlSeqItem>> {
765 let mut items = Vec::new();
766 let mut pending_trivia = Vec::new();
767 let mut idx = 0;
768
769 while idx < lines.len() {
770 let line = &lines[idx];
771 if is_trivia_line(line) {
772 pending_trivia.push(line.clone());
773 idx += 1;
774 continue;
775 }
776 let indent = line_indent(line);
777 if indent != seq_indent {
778 return None;
779 }
780 let trimmed = &line[indent..];
781 if !trimmed.starts_with("- ") && trimmed != "-" {
782 return None;
783 }
784
785 let leading_trivia = std::mem::take(&mut pending_trivia);
786 let mut item_lines = vec![line.clone()];
787 idx += 1;
788
789 while idx < lines.len() {
790 let next_line = &lines[idx];
791 if is_trivia_line(next_line) {
792 let mut lookahead = idx + 1;
793 let mut continues_item = false;
794 while lookahead < lines.len() {
795 if !is_trivia_line(&lines[lookahead]) {
796 if line_indent(&lines[lookahead]) > seq_indent {
797 continues_item = true;
798 }
799 break;
800 }
801 lookahead += 1;
802 }
803 if continues_item {
804 item_lines.push(next_line.clone());
805 idx += 1;
806 continue;
807 } else {
808 break;
809 }
810 }
811 if line_indent(next_line) > seq_indent {
812 item_lines.push(next_line.clone());
813 idx += 1;
814 } else {
815 break;
816 }
817 }
818
819 let canonical = item_lines
820 .iter()
821 .filter(|l| !is_trivia_line(l))
822 .map(|l| strip_yaml_inline_comment(l.trim()))
823 .collect::<Vec<_>>()
824 .join("\n");
825
826 items.push(YamlSeqItem {
827 leading_trivia,
828 lines: item_lines,
829 canonical,
830 });
831 }
832
833 if items.is_empty() { None } else { Some(items) }
834}
835
836fn yaml_entry_semantically_equal(a: &YamlEntry, b: &YamlEntry) -> bool {
837 if a.key != b.key {
838 return false;
839 }
840 yaml_node_semantically_equal(&a.body, &b.body)
841}
842
843fn yaml_node_semantically_equal(a: &YamlNode, b: &YamlNode) -> bool {
844 match (a, b) {
845 (YamlNode::Scalar(sa), YamlNode::Scalar(sb)) => sa == sb,
846 (YamlNode::Mapping(ma), YamlNode::Mapping(mb)) => yaml_maps_semantically_equal(ma, mb),
847 (YamlNode::Sequence(sa), YamlNode::Sequence(sb)) => {
848 sa.len() == sb.len()
849 && sa
850 .iter()
851 .zip(sb.iter())
852 .all(|(ia, ib)| ia.canonical == ib.canonical)
853 }
854 (YamlNode::RawBlock(ra), YamlNode::RawBlock(rb)) => ra == rb,
855 _ => false,
856 }
857}
858
859fn yaml_maps_semantically_equal(a: &YamlMap, b: &YamlMap) -> bool {
860 if a.entries.len() != b.entries.len() {
861 return false;
862 }
863 a.entries.iter().all(|ea| {
864 b.entries
865 .iter()
866 .find(|eb| eb.key == ea.key)
867 .is_some_and(|eb| yaml_entry_semantically_equal(ea, eb))
868 })
869}
870
871fn merge_yaml_maps(
872 base: Option<&YamlMap>,
873 ours: &YamlMap,
874 theirs: &YamlMap,
875 prefix: &str,
876 conflicts: &mut Vec<StructuralConflict>,
877) -> YamlMap {
878 let mut keys = Vec::new();
879 if let Some(b) = base {
880 for e in &b.entries {
881 if !keys.contains(&e.key) {
882 keys.push(e.key.clone());
883 }
884 }
885 }
886 for e in &ours.entries {
887 if !keys.contains(&e.key) {
888 keys.push(e.key.clone());
889 }
890 }
891 for e in &theirs.entries {
892 if !keys.contains(&e.key) {
893 keys.push(e.key.clone());
894 }
895 }
896
897 let mut merged_entries = Vec::new();
898 for k in keys {
899 let key_path = if prefix.is_empty() {
900 k.clone()
901 } else {
902 format!("{prefix}.{k}")
903 };
904
905 let b_e = base.and_then(|m| m.entries.iter().find(|e| e.key == k));
906 let o_e = ours.entries.iter().find(|e| e.key == k);
907 let t_e = theirs.entries.iter().find(|e| e.key == k);
908
909 if let (Some(oe), Some(te)) = (o_e, t_e)
910 && let (YamlNode::Mapping(o_sub), YamlNode::Mapping(t_sub)) = (&oe.body, &te.body)
911 && b_e.is_none_or(|be| matches!(be.body, YamlNode::Mapping(_)))
912 {
913 let b_sub = match b_e.map(|be| &be.body) {
914 Some(YamlNode::Mapping(bm)) => Some(bm),
915 _ => None,
916 };
917 let merged_sub = merge_yaml_maps(b_sub, o_sub, t_sub, &key_path, conflicts);
918 merged_entries.push(YamlEntry {
919 leading_trivia: oe.leading_trivia.clone(),
920 key: k,
921 header_line: oe.header_line.clone(),
922 body: YamlNode::Mapping(merged_sub),
923 });
924 continue;
925 }
926
927 if let (Some(oe), Some(te)) = (o_e, t_e)
928 && let (YamlNode::Sequence(o_seq), YamlNode::Sequence(t_seq)) = (&oe.body, &te.body)
929 && b_e.is_none_or(|be| matches!(be.body, YamlNode::Sequence(_)))
930 {
931 let b_seq = match b_e.map(|be| &be.body) {
932 Some(YamlNode::Sequence(bs)) => Some(bs.as_slice()),
933 _ => None,
934 };
935 let merged_seq = merge_yaml_sequences(b_seq, o_seq, t_seq);
936 merged_entries.push(YamlEntry {
937 leading_trivia: oe.leading_trivia.clone(),
938 key: k,
939 header_line: oe.header_line.clone(),
940 body: YamlNode::Sequence(merged_seq),
941 });
942 continue;
943 }
944
945 let b_eq_t = opt_yaml_entries_equal(b_e, t_e);
946 let b_eq_o = opt_yaml_entries_equal(b_e, o_e);
947 let o_eq_t = opt_yaml_entries_equal(o_e, t_e);
948
949 if b_eq_t {
950 if let Some(oe) = o_e {
951 merged_entries.push(oe.clone());
952 }
953 } else if b_eq_o {
954 if let Some(te) = t_e {
955 let mut adopted = te.clone();
956 if let Some(oe) = o_e
957 && !oe.leading_trivia.is_empty()
958 && adopted.leading_trivia.is_empty()
959 {
960 adopted.leading_trivia = oe.leading_trivia.clone();
961 }
962 merged_entries.push(adopted);
963 }
964 } else if o_eq_t {
965 if let Some(oe) = o_e {
966 merged_entries.push(oe.clone());
967 }
968 } else {
969 conflicts.push(StructuralConflict {
970 key_path,
971 base: format_opt_yaml_entry(b_e),
972 ours: format_opt_yaml_entry(o_e),
973 theirs: format_opt_yaml_entry(t_e),
974 });
975 }
976 }
977
978 YamlMap {
979 entries: merged_entries,
980 trailing_lines: ours.trailing_lines.clone(),
981 }
982}
983
984fn merge_yaml_sequences(
985 base: Option<&[YamlSeqItem]>,
986 ours: &[YamlSeqItem],
987 theirs: &[YamlSeqItem],
988) -> Vec<YamlSeqItem> {
989 let base_set: Vec<&str> = base
990 .unwrap_or(&[])
991 .iter()
992 .map(|i| i.canonical.as_str())
993 .collect();
994 let theirs_set: Vec<&str> = theirs.iter().map(|i| i.canonical.as_str()).collect();
995 let ours_set: Vec<&str> = ours.iter().map(|i| i.canonical.as_str()).collect();
996
997 let mut merged: Vec<YamlSeqItem> = Vec::new();
998 for t_item in theirs {
999 let was_in_base = base_set.contains(&t_item.canonical.as_str());
1000 let still_in_ours = ours_set.contains(&t_item.canonical.as_str());
1001 if was_in_base && !still_in_ours {
1002 continue;
1003 }
1004 if let Some(o_existing) = ours.iter().find(|o| o.canonical == t_item.canonical) {
1005 merged.push(o_existing.clone());
1006 } else {
1007 merged.push(t_item.clone());
1008 }
1009 }
1010
1011 for o_item in ours {
1012 let was_in_base = base_set.contains(&o_item.canonical.as_str());
1013 let in_theirs = theirs_set.contains(&o_item.canonical.as_str());
1014 if !was_in_base && !in_theirs {
1015 merged.push(o_item.clone());
1016 }
1017 }
1018
1019 merged
1020}
1021
1022fn opt_yaml_entries_equal(a: Option<&YamlEntry>, b: Option<&YamlEntry>) -> bool {
1023 match (a, b) {
1024 (None, None) => true,
1025 (Some(ea), Some(eb)) => yaml_entry_semantically_equal(ea, eb),
1026 _ => false,
1027 }
1028}
1029
1030fn format_opt_yaml_entry(entry: Option<&YamlEntry>) -> String {
1031 match entry {
1032 None => "<none>".to_string(),
1033 Some(e) => match &e.body {
1034 YamlNode::Scalar(s) => s.clone(),
1035 _ => {
1036 let mut buf = String::new();
1037 emit_yaml_entry(e, &mut buf);
1038 buf.trim().to_string()
1039 }
1040 },
1041 }
1042}
1043
1044fn emit_yaml_map(map: &YamlMap, out: &mut String) {
1045 for entry in &map.entries {
1046 emit_yaml_entry(entry, out);
1047 }
1048 for line in &map.trailing_lines {
1049 out.push_str(line);
1050 out.push('\n');
1051 }
1052}
1053
1054fn emit_yaml_entry(entry: &YamlEntry, out: &mut String) {
1055 for t in &entry.leading_trivia {
1056 out.push_str(t);
1057 out.push('\n');
1058 }
1059 out.push_str(&entry.header_line);
1060 out.push('\n');
1061 match &entry.body {
1062 YamlNode::Scalar(_) => {}
1063 YamlNode::Mapping(sub) => emit_yaml_map(sub, out),
1064 YamlNode::Sequence(items) => {
1065 for item in items {
1066 for t in &item.leading_trivia {
1067 out.push_str(t);
1068 out.push('\n');
1069 }
1070 for l in &item.lines {
1071 out.push_str(l);
1072 out.push('\n');
1073 }
1074 }
1075 }
1076 YamlNode::RawBlock(lines) => {
1077 for l in lines {
1078 out.push_str(l);
1079 out.push('\n');
1080 }
1081 }
1082 }
1083}