1#![expect(missing_docs)]
16
17use std::io;
18use std::io::Write;
19use std::iter::zip;
20use std::pin::Pin;
21
22use bstr::BStr;
23use bstr::BString;
24use bstr::ByteSlice as _;
25use bstr::ByteVec as _;
26use futures::Stream;
27use futures::StreamExt as _;
28use futures::stream::BoxStream;
29use futures::try_join;
30use itertools::Itertools as _;
31use pollster::FutureExt as _;
32use tokio::io::AsyncRead;
33use tokio::io::AsyncReadExt as _;
34
35use crate::backend::BackendError;
36use crate::backend::BackendResult;
37use crate::backend::CommitId;
38use crate::backend::CopyId;
39use crate::backend::FileId;
40use crate::backend::SymlinkId;
41use crate::backend::TreeId;
42use crate::backend::TreeValue;
43use crate::conflict_labels::ConflictLabels;
44use crate::copies::CopiesTreeDiffEntry;
45use crate::copies::CopiesTreeDiffEntryPath;
46use crate::diff::ContentDiff;
47use crate::diff::DiffHunk;
48use crate::diff::DiffHunkKind;
49use crate::files;
50use crate::files::MergeResult;
51use crate::merge::Diff;
52use crate::merge::Merge;
53use crate::merge::MergedTreeValue;
54use crate::merge::SameChange;
55use crate::repo_path::RepoPath;
56use crate::store::Store;
57use crate::tree_merge::MergeOptions;
58
59pub const MIN_CONFLICT_MARKER_LEN: usize = 7;
61
62const CONFLICT_MARKER_LEN_INCREMENT: usize = 4;
67
68const NO_ENDING_EOL_COMMENT: &str = "(no terminating newline)";
70
71fn write_diff_hunks(hunks: &[DiffHunk], file: &mut dyn Write) -> io::Result<()> {
72 for hunk in hunks {
73 match hunk.kind {
74 DiffHunkKind::Matching => {
75 debug_assert!(hunk.contents.iter().all_equal());
76 for line in hunk.contents[0].lines_with_terminator() {
77 file.write_all(b" ")?;
78 file.write_all(line)?;
79 }
80 }
81 DiffHunkKind::Different => {
82 for line in hunk.contents[0].lines_with_terminator() {
83 file.write_all(b"-")?;
84 file.write_all(line)?;
85 }
86 for line in hunk.contents[1].lines_with_terminator() {
87 file.write_all(b"+")?;
88 file.write_all(line)?;
89 }
90 }
91 }
92 }
93 Ok(())
94}
95
96async fn get_file_contents(
97 store: &Store,
98 path: &RepoPath,
99 term: Option<&FileId>,
100) -> BackendResult<BString> {
101 match term {
102 Some(id) => {
103 let mut reader = store.read_file(path, id).await?;
104 let mut content = vec![];
105 reader
106 .read_to_end(&mut content)
107 .await
108 .map_err(|err| BackendError::ReadFile {
109 path: path.to_owned(),
110 id: id.clone(),
111 source: err.into(),
112 })?;
113 Ok(BString::new(content))
114 }
115 None => Ok(BString::new(vec![])),
118 }
119}
120
121pub async fn extract_as_single_hunk(
122 merge: &Merge<Option<FileId>>,
123 store: &Store,
124 path: &RepoPath,
125) -> BackendResult<Merge<BString>> {
126 merge
127 .try_map_async(|term| get_file_contents(store, path, term.as_ref()))
128 .await
129}
130
131pub enum MaterializedTreeValue {
134 Absent,
135 AccessDenied(Box<dyn std::error::Error + Send + Sync>),
136 File(MaterializedFileValue),
137 Symlink {
138 id: SymlinkId,
139 target: String,
140 },
141 FileConflict(MaterializedFileConflictValue),
142 OtherConflict {
143 id: MergedTreeValue,
144 labels: ConflictLabels,
145 },
146 GitSubmodule(CommitId),
147 Tree(TreeId),
148}
149
150impl MaterializedTreeValue {
151 pub fn is_absent(&self) -> bool {
152 matches!(self, Self::Absent)
153 }
154
155 pub fn is_present(&self) -> bool {
156 !self.is_absent()
157 }
158}
159
160pub struct MaterializedFileValue {
162 pub id: FileId,
163 pub executable: bool,
164 pub copy_id: CopyId,
165 pub reader: Pin<Box<dyn AsyncRead + Send>>,
166}
167
168impl MaterializedFileValue {
169 pub async fn read_all(&mut self, path: &RepoPath) -> BackendResult<Vec<u8>> {
172 let mut buf = Vec::new();
173 self.reader
174 .read_to_end(&mut buf)
175 .await
176 .map_err(|err| BackendError::ReadFile {
177 path: path.to_owned(),
178 id: self.id.clone(),
179 source: err.into(),
180 })?;
181 Ok(buf)
182 }
183}
184
185pub struct MaterializedFileConflictValue {
187 pub unsimplified_ids: Merge<Option<FileId>>,
190 pub ids: Merge<Option<FileId>>,
192 pub labels: ConflictLabels,
194 pub contents: Merge<BString>,
198 pub executable: Option<bool>,
201 pub copy_id: Option<CopyId>,
203}
204
205pub async fn materialize_tree_value(
208 store: &Store,
209 path: &RepoPath,
210 value: MergedTreeValue,
211 conflict_labels: &ConflictLabels,
212) -> BackendResult<MaterializedTreeValue> {
213 match materialize_tree_value_no_access_denied(store, path, value, conflict_labels).await {
214 Err(BackendError::ReadAccessDenied { source, .. }) => {
215 Ok(MaterializedTreeValue::AccessDenied(source))
216 }
217 result => result,
218 }
219}
220
221async fn materialize_tree_value_no_access_denied(
222 store: &Store,
223 path: &RepoPath,
224 value: MergedTreeValue,
225 conflict_labels: &ConflictLabels,
226) -> BackendResult<MaterializedTreeValue> {
227 match value.into_resolved() {
228 Ok(None) => Ok(MaterializedTreeValue::Absent),
229 Ok(Some(TreeValue::File {
230 id,
231 executable,
232 copy_id,
233 })) => {
234 let reader = store.read_file(path, &id).await?;
235 Ok(MaterializedTreeValue::File(MaterializedFileValue {
236 id,
237 executable,
238 copy_id,
239 reader,
240 }))
241 }
242 Ok(Some(TreeValue::Symlink(id))) => {
243 let target = store.read_symlink(path, &id).await?;
244 Ok(MaterializedTreeValue::Symlink { id, target })
245 }
246 Ok(Some(TreeValue::GitSubmodule(id))) => Ok(MaterializedTreeValue::GitSubmodule(id)),
247 Ok(Some(TreeValue::Tree(id))) => Ok(MaterializedTreeValue::Tree(id)),
248 Err(conflict) => {
249 match try_materialize_file_conflict_value(store, path, &conflict, conflict_labels)
250 .await?
251 {
252 Some(file) => Ok(MaterializedTreeValue::FileConflict(file)),
253 None => Ok(MaterializedTreeValue::OtherConflict {
254 id: conflict,
255 labels: conflict_labels.clone(),
256 }),
257 }
258 }
259 }
260}
261
262pub async fn try_materialize_file_conflict_value(
265 store: &Store,
266 path: &RepoPath,
267 conflict: &MergedTreeValue,
268 conflict_labels: &ConflictLabels,
269) -> BackendResult<Option<MaterializedFileConflictValue>> {
270 let (Some(unsimplified_ids), Some(executable_bits)) =
271 (conflict.to_file_merge(), conflict.to_executable_merge())
272 else {
273 return Ok(None);
274 };
275 let (labels, ids) = conflict_labels.simplify_with(&unsimplified_ids);
276 let contents = extract_as_single_hunk(&ids, store, path).await?;
277 let executable = resolve_file_executable(&executable_bits);
278 Ok(Some(MaterializedFileConflictValue {
279 unsimplified_ids,
280 ids,
281 labels,
282 contents,
283 executable,
284 copy_id: Some(CopyId::placeholder()),
285 }))
286}
287
288pub fn resolve_file_executable(merge: &Merge<Option<bool>>) -> Option<bool> {
291 let resolved = merge.resolve_trivial(SameChange::Accept).copied()?;
292 if resolved.is_some() {
293 resolved
294 } else {
295 merge.removes().flatten().copied().all_equal_value().ok()
299 }
300}
301
302#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Deserialize)]
304#[serde(rename_all = "kebab-case")]
305pub enum ConflictMarkerStyle {
306 Diff,
308 DiffExperimental,
311 Snapshot,
313 Git,
315}
316
317impl ConflictMarkerStyle {
318 pub fn allows_diff(&self) -> bool {
320 matches!(self, Self::Diff | Self::DiffExperimental)
321 }
322}
323
324#[derive(Clone, Debug)]
326pub struct ConflictMaterializeOptions {
327 pub marker_style: ConflictMarkerStyle,
328 pub marker_len: Option<usize>,
329 pub merge: MergeOptions,
330}
331
332#[derive(Clone, Copy, PartialEq, Eq)]
335#[repr(u8)]
336enum ConflictMarkerLineChar {
337 ConflictStart = b'<',
338 ConflictEnd = b'>',
339 Add = b'+',
340 Remove = b'-',
341 Diff = b'%',
342 Note = b'\\',
343 GitAncestor = b'|',
344 GitSeparator = b'=',
345}
346
347impl ConflictMarkerLineChar {
348 fn to_byte(self) -> u8 {
350 self as u8
351 }
352
353 fn parse_byte(byte: u8) -> Option<Self> {
355 match byte {
356 b'<' => Some(Self::ConflictStart),
357 b'>' => Some(Self::ConflictEnd),
358 b'+' => Some(Self::Add),
359 b'-' => Some(Self::Remove),
360 b'%' => Some(Self::Diff),
361 b'\\' => Some(Self::Note),
362 b'|' => Some(Self::GitAncestor),
363 b'=' => Some(Self::GitSeparator),
364 _ => None,
365 }
366 }
367}
368
369struct ConflictMarkerLine {
372 kind: ConflictMarkerLineChar,
373 len: usize,
374}
375
376fn write_conflict_marker(
378 output: &mut dyn Write,
379 kind: ConflictMarkerLineChar,
380 len: usize,
381 suffix_text: &str,
382) -> io::Result<()> {
383 let conflict_marker = BString::new(vec![kind.to_byte(); len]);
384
385 if suffix_text.is_empty() {
386 write!(output, "{conflict_marker}")
387 } else {
388 write!(output, "{conflict_marker} {suffix_text}")
389 }
390}
391
392fn parse_conflict_marker_any_len(line: &[u8]) -> Option<ConflictMarkerLine> {
395 let first_byte = *line.first()?;
396 let kind = ConflictMarkerLineChar::parse_byte(first_byte)?;
397 let len = line.iter().take_while(|&&b| b == first_byte).count();
398
399 if let Some(next_byte) = line.get(len) {
400 if !next_byte.is_ascii_whitespace() {
402 return None;
403 }
404 }
405
406 Some(ConflictMarkerLine { kind, len })
407}
408
409fn parse_conflict_marker(line: &[u8], expected_len: usize) -> Option<ConflictMarkerLineChar> {
412 parse_conflict_marker_any_len(line)
413 .filter(|marker| marker.len >= expected_len)
414 .map(|marker| marker.kind)
415}
416
417pub fn choose_materialized_conflict_marker_len<T: AsRef<[u8]>>(single_hunk: &Merge<T>) -> usize {
420 let max_existing_marker_len = single_hunk
421 .iter()
422 .flat_map(|file| file.as_ref().lines_with_terminator())
423 .filter_map(parse_conflict_marker_any_len)
424 .map(|marker| marker.len)
425 .max()
426 .unwrap_or_default();
427
428 max_existing_marker_len
429 .saturating_add(CONFLICT_MARKER_LEN_INCREMENT)
430 .max(MIN_CONFLICT_MARKER_LEN)
431}
432
433fn detect_eol(single_hunk: &Merge<impl AsRef<[u8]>>) -> &'static BStr {
434 let use_crlf = single_hunk
435 .iter()
436 .filter_map(|content| {
437 let content = content.as_ref();
438 let newline_index = content.find_byte(b'\n')?;
439 Some(newline_index > 0 && content[newline_index - 1] == b'\r')
440 })
441 .all_equal_value()
442 .unwrap_or(false);
443 if use_crlf {
444 b"\r\n".into()
445 } else {
446 b"\n".into()
447 }
448}
449
450pub fn materialize_merge_result<T: AsRef<[u8]>>(
451 single_hunk: &Merge<T>,
452 labels: &ConflictLabels,
453 output: &mut dyn Write,
454 options: &ConflictMaterializeOptions,
455) -> io::Result<()> {
456 let merge_result = files::merge_hunks(single_hunk, &options.merge);
457 match merge_result {
458 MergeResult::Resolved(content) => output.write_all(&content),
459 MergeResult::Conflict(hunks) => {
460 let marker_len = options
461 .marker_len
462 .unwrap_or_else(|| choose_materialized_conflict_marker_len(single_hunk));
463 materialize_conflict_hunks(
464 hunks,
465 options.marker_style,
466 marker_len,
467 labels,
468 output,
469 detect_eol(single_hunk),
470 )
471 }
472 }
473}
474
475pub fn materialize_merge_result_to_bytes<T: AsRef<[u8]>>(
476 single_hunk: &Merge<T>,
477 labels: &ConflictLabels,
478 options: &ConflictMaterializeOptions,
479) -> BString {
480 let merge_result = files::merge_hunks(single_hunk, &options.merge);
481 match merge_result {
482 MergeResult::Resolved(content) => content,
483 MergeResult::Conflict(hunks) => {
484 let marker_len = options
485 .marker_len
486 .unwrap_or_else(|| choose_materialized_conflict_marker_len(single_hunk));
487 let mut output = Vec::new();
488 materialize_conflict_hunks(
489 hunks,
490 options.marker_style,
491 marker_len,
492 labels,
493 &mut output,
494 detect_eol(single_hunk),
495 )
496 .expect("writing to an in-memory buffer should never fail");
497 output.into()
498 }
499 }
500}
501
502fn materialize_conflict_hunks(
503 hunks: Vec<Merge<BString>>,
506 conflict_marker_style: ConflictMarkerStyle,
507 conflict_marker_len: usize,
508 labels: &ConflictLabels,
509 output: &mut dyn Write,
510 eol: &BStr,
511) -> io::Result<()> {
512 let num_conflicts = hunks
513 .iter()
514 .filter(|hunk| hunk.as_resolved().is_none())
515 .count();
516 let mut conflict_index = 0;
517 for hunk in hunks {
518 if let Some(content) = hunk.as_resolved() {
519 output.write_all(content)?;
520 } else {
521 conflict_index += 1;
522 let conflict_info = format!("conflict {conflict_index} of {num_conflicts}");
523
524 let all_sides_have_ending_eol = hunk
529 .iter()
530 .all(|content| content.last().is_none_or(|last| *last == b'\n'));
531 let mut sides = build_hunk_sides(hunk, labels);
532 if !all_sides_have_ending_eol {
533 for side in &mut sides {
534 side.contents.push_str(eol);
535 }
536 }
537
538 match (conflict_marker_style, sides.as_slice()) {
539 (ConflictMarkerStyle::Git, [left, base, right]) => {
541 materialize_git_style_conflict(
542 left,
543 base,
544 right,
545 conflict_marker_len,
546 output,
547 eol,
548 )?;
549 }
550 _ => {
551 materialize_jj_style_conflict(
552 sides,
553 &conflict_info,
554 conflict_marker_style,
555 conflict_marker_len,
556 output,
557 eol,
558 )?;
559 }
560 }
561
562 if all_sides_have_ending_eol {
563 output.write_all(eol)?;
564 }
565 }
566 }
567 Ok(())
568}
569
570#[derive(Debug)]
571struct HunkTerm {
572 contents: BString,
573 label: String,
574}
575
576fn build_hunk_sides(hunk: Merge<BString>, labels: &ConflictLabels) -> Merge<HunkTerm> {
577 let (removes, adds) = hunk.into_removes_adds();
578 let num_bases = removes.len();
579 let removes = removes.enumerate().map(|(base_index, contents)| {
580 let label = labels
581 .get_remove(base_index)
582 .map(|label| label.to_owned())
583 .unwrap_or_else(|| {
584 if num_bases == 1 {
587 "base".to_string()
588 } else {
589 format!("base #{}", base_index + 1)
590 }
591 });
592 HunkTerm { contents, label }
593 });
594 let adds = adds.enumerate().map(|(add_index, contents)| {
595 let label = labels.get_add(add_index).map_or_else(
596 || format!("side #{}", add_index + 1),
597 |label| label.to_owned(),
598 );
599 HunkTerm { contents, label }
600 });
601 let mut hunk_terms = Merge::from_removes_adds(removes, adds);
602 for term in &mut hunk_terms {
603 if term.contents.last().is_some_and(|ch| *ch != b'\n') {
605 term.label.push(' ');
606 term.label.push_str(NO_ENDING_EOL_COMMENT);
607 }
608 }
609 hunk_terms
610}
611
612fn materialize_git_style_conflict(
613 left: &HunkTerm,
614 base: &HunkTerm,
615 right: &HunkTerm,
616 conflict_marker_len: usize,
617 output: &mut dyn Write,
618 eol: &BStr,
619) -> io::Result<()> {
620 write_conflict_marker(
621 output,
622 ConflictMarkerLineChar::ConflictStart,
623 conflict_marker_len,
624 &left.label,
625 )?;
626 output.write_all(eol)?;
627 output.write_all(&left.contents)?;
628
629 write_conflict_marker(
630 output,
631 ConflictMarkerLineChar::GitAncestor,
632 conflict_marker_len,
633 &base.label,
634 )?;
635 output.write_all(eol)?;
636 output.write_all(&base.contents)?;
637
638 write_conflict_marker(
640 output,
641 ConflictMarkerLineChar::GitSeparator,
642 conflict_marker_len,
643 "",
644 )?;
645 output.write_all(eol)?;
646
647 output.write_all(&right.contents)?;
648 write_conflict_marker(
652 output,
653 ConflictMarkerLineChar::ConflictEnd,
654 conflict_marker_len,
655 &right.label,
656 )?;
657
658 Ok(())
659}
660
661fn materialize_jj_style_conflict(
662 hunk: Merge<HunkTerm>,
663 conflict_info: &str,
664 conflict_marker_style: ConflictMarkerStyle,
665 conflict_marker_len: usize,
666 output: &mut dyn Write,
667 eol: &BStr,
668) -> io::Result<()> {
669 let write_side = |side: &HunkTerm, output: &mut dyn Write| {
671 write_conflict_marker(
672 output,
673 ConflictMarkerLineChar::Add,
674 conflict_marker_len,
675 &side.label,
676 )?;
677 output.write_all(eol)?;
678 output.write_all(&side.contents)
679 };
680
681 let write_base = |side: &HunkTerm, output: &mut dyn Write| {
683 write_conflict_marker(
684 output,
685 ConflictMarkerLineChar::Remove,
686 conflict_marker_len,
687 &side.label,
688 )?;
689 output.write_all(eol)?;
690 output.write_all(&side.contents)
691 };
692
693 let write_diff =
695 |base: &HunkTerm, add: &HunkTerm, diff: &[DiffHunk], output: &mut dyn Write| {
696 write_conflict_marker(
697 output,
698 ConflictMarkerLineChar::Diff,
699 conflict_marker_len,
700 &format!("diff from: {}", base.label),
701 )?;
702 output.write_all(eol)?;
703 write_conflict_marker(
704 output,
705 ConflictMarkerLineChar::Note,
706 conflict_marker_len,
707 &format!(" to: {}", add.label),
708 )?;
709 output.write_all(eol)?;
710 write_diff_hunks(diff, output)
711 };
712
713 write_conflict_marker(
714 output,
715 ConflictMarkerLineChar::ConflictStart,
716 conflict_marker_len,
717 conflict_info,
718 )?;
719 output.write_all(eol)?;
720 let mut snapshot_written = false;
721 if conflict_marker_style != ConflictMarkerStyle::Diff {
723 write_side(hunk.first(), output)?;
724 snapshot_written = true;
725 }
726 for (base_index, left) in hunk.removes().enumerate() {
727 let add_index = if snapshot_written {
728 base_index + 1
729 } else {
730 base_index
731 };
732
733 let right1 = hunk.get_add(add_index).unwrap();
734
735 if !conflict_marker_style.allows_diff() {
738 write_base(left, output)?;
739 write_side(right1, output)?;
740 continue;
741 }
742
743 let diff1 = ContentDiff::by_line([&left.contents, &right1.contents])
744 .hunks()
745 .collect_vec();
746 if !snapshot_written {
750 let right2 = hunk.get_add(add_index + 1).unwrap();
751 let diff2 = ContentDiff::by_line([&left.contents, &right2.contents])
752 .hunks()
753 .collect_vec();
754 if diff_size(&diff2) < diff_size(&diff1) {
755 write_side(right1, output)?;
758 write_diff(left, right2, &diff2, output)?;
759 snapshot_written = true;
760 continue;
761 }
762 }
763
764 write_diff(left, right1, &diff1, output)?;
765 }
766
767 if !snapshot_written {
769 write_side(hunk.get_add(hunk.num_sides() - 1).unwrap(), output)?;
770 }
771 write_conflict_marker(
772 output,
773 ConflictMarkerLineChar::ConflictEnd,
774 conflict_marker_len,
775 &format!("{conflict_info} ends"),
776 )?;
777 Ok(())
778}
779
780fn diff_size(hunks: &[DiffHunk]) -> usize {
781 hunks
782 .iter()
783 .map(|hunk| match hunk.kind {
784 DiffHunkKind::Matching => 0,
785 DiffHunkKind::Different => hunk.contents.iter().map(|content| content.len()).sum(),
786 })
787 .sum()
788}
789
790pub struct MaterializedTreeDiffEntry {
791 pub path: CopiesTreeDiffEntryPath,
792 pub values: BackendResult<Diff<MaterializedTreeValue>>,
793}
794
795pub fn materialized_diff_stream(
796 store: &Store,
797 tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
798 conflict_labels: Diff<&ConflictLabels>,
799) -> impl Stream<Item = MaterializedTreeDiffEntry> {
800 tree_diff
801 .map(async |CopiesTreeDiffEntry { path, values }| match values {
802 Err(err) => MaterializedTreeDiffEntry {
803 path,
804 values: Err(err),
805 },
806 Ok(values) => {
807 let before_future = materialize_tree_value(
808 store,
809 path.source(),
810 values.before,
811 conflict_labels.before,
812 );
813 let after_future = materialize_tree_value(
814 store,
815 path.target(),
816 values.after,
817 conflict_labels.after,
818 );
819 let values = try_join!(before_future, after_future)
820 .map(|(before, after)| Diff { before, after });
821 MaterializedTreeDiffEntry { path, values }
822 }
823 })
824 .buffered((store.concurrency() / 2).max(1))
825}
826
827pub fn parse_conflict(
839 input: &[u8],
840 num_sides: usize,
841 expected_marker_len: usize,
842) -> Option<Vec<Merge<BString>>> {
843 if input.is_empty() {
844 return None;
845 }
846 let mut hunks = vec![];
847 let mut pos = 0;
848 let mut resolved_start = 0;
849 let mut conflict_start = None;
850 let mut conflict_start_len = 0;
851 for line in input.lines_with_terminator() {
852 match parse_conflict_marker(line, expected_marker_len) {
853 Some(ConflictMarkerLineChar::ConflictStart) => {
854 conflict_start = Some(pos);
855 conflict_start_len = line.len();
856 }
857 Some(ConflictMarkerLineChar::ConflictEnd) => {
858 if let Some(conflict_start_index) = conflict_start.take() {
859 let conflict_body = &input[conflict_start_index + conflict_start_len..pos];
860 let mut hunk = parse_conflict_hunk(conflict_body, expected_marker_len);
861 if hunk.num_sides() == num_sides {
862 let resolved_slice = &input[resolved_start..conflict_start_index];
863 if !resolved_slice.is_empty() {
864 hunks.push(Merge::resolved(BString::from(resolved_slice)));
865 }
866 if !line.ends_with(b"\n") {
867 for term in &mut hunk {
871 if term.pop_if(|x| *x == b'\n').is_some() {
872 term.pop_if(|x| *x == b'\r');
873 }
874 }
875 }
876 hunks.push(hunk);
877 resolved_start = pos + line.len();
878 }
879 }
880 }
881 _ => {}
882 }
883 pos += line.len();
884 }
885
886 if hunks.is_empty() {
887 None
888 } else {
889 if resolved_start < input.len() {
890 hunks.push(Merge::resolved(BString::from(&input[resolved_start..])));
891 }
892 Some(hunks)
893 }
894}
895
896fn parse_conflict_hunk(input: &[u8], expected_marker_len: usize) -> Merge<BString> {
902 let initial_conflict_marker = input
904 .lines_with_terminator()
905 .next()
906 .and_then(|line| parse_conflict_marker(line, expected_marker_len));
907
908 match initial_conflict_marker {
909 Some(
911 ConflictMarkerLineChar::Diff
912 | ConflictMarkerLineChar::Remove
913 | ConflictMarkerLineChar::Add,
914 ) => parse_jj_style_conflict_hunk(input, expected_marker_len),
915 None | Some(ConflictMarkerLineChar::GitAncestor) => {
918 parse_git_style_conflict_hunk(input, expected_marker_len)
919 }
920 Some(_) => Merge::resolved(BString::new(vec![])),
922 }
923}
924
925fn parse_jj_style_conflict_hunk(input: &[u8], expected_marker_len: usize) -> Merge<BString> {
926 enum State {
927 Diff,
928 Remove,
929 Add,
930 Unknown,
931 }
932 let mut state = State::Unknown;
933 let mut removes = vec![];
934 let mut adds = vec![];
935 for line in input.lines_with_terminator() {
936 match parse_conflict_marker(line, expected_marker_len) {
937 Some(ConflictMarkerLineChar::Diff) => {
938 state = State::Diff;
939 removes.push(BString::new(vec![]));
940 adds.push(BString::new(vec![]));
941 continue;
942 }
943 Some(ConflictMarkerLineChar::Remove) => {
944 state = State::Remove;
945 removes.push(BString::new(vec![]));
946 continue;
947 }
948 Some(ConflictMarkerLineChar::Add) => {
949 state = State::Add;
950 adds.push(BString::new(vec![]));
951 continue;
952 }
953 Some(ConflictMarkerLineChar::Note) => {
954 continue;
955 }
956 _ => {}
957 }
958 match state {
959 State::Diff => {
960 if let Some(rest) = line.strip_prefix(b"-") {
961 removes.last_mut().unwrap().extend_from_slice(rest);
962 } else if let Some(rest) = line.strip_prefix(b"+") {
963 adds.last_mut().unwrap().extend_from_slice(rest);
964 } else if let Some(rest) = line.strip_prefix(b" ") {
965 removes.last_mut().unwrap().extend_from_slice(rest);
966 adds.last_mut().unwrap().extend_from_slice(rest);
967 } else if line == b"\n" || line == b"\r\n" {
968 removes.last_mut().unwrap().extend_from_slice(line);
972 adds.last_mut().unwrap().extend_from_slice(line);
973 } else {
974 return Merge::resolved(BString::new(vec![]));
976 }
977 }
978 State::Remove => {
979 removes.last_mut().unwrap().extend_from_slice(line);
980 }
981 State::Add => {
982 adds.last_mut().unwrap().extend_from_slice(line);
983 }
984 State::Unknown => {
985 return Merge::resolved(BString::new(vec![]));
987 }
988 }
989 }
990
991 if adds.len() == removes.len() + 1 {
992 Merge::from_removes_adds(removes, adds)
993 } else {
994 Merge::resolved(BString::new(vec![]))
996 }
997}
998
999fn parse_git_style_conflict_hunk(input: &[u8], expected_marker_len: usize) -> Merge<BString> {
1000 #[derive(PartialEq, Eq)]
1001 enum State {
1002 Left,
1003 Base,
1004 Right,
1005 }
1006 let mut state = State::Left;
1007 let mut left = BString::new(vec![]);
1008 let mut base = BString::new(vec![]);
1009 let mut right = BString::new(vec![]);
1010 for line in input.lines_with_terminator() {
1011 match parse_conflict_marker(line, expected_marker_len) {
1012 Some(ConflictMarkerLineChar::GitAncestor) => {
1013 if state == State::Left {
1014 state = State::Base;
1015 continue;
1016 } else {
1017 return Merge::resolved(BString::new(vec![]));
1019 }
1020 }
1021 Some(ConflictMarkerLineChar::GitSeparator) => {
1022 if state == State::Base {
1023 state = State::Right;
1024 continue;
1025 } else {
1026 return Merge::resolved(BString::new(vec![]));
1028 }
1029 }
1030 _ => {}
1031 }
1032 match state {
1033 State::Left => left.extend_from_slice(line),
1034 State::Base => base.extend_from_slice(line),
1035 State::Right => right.extend_from_slice(line),
1036 }
1037 }
1038
1039 if state == State::Right {
1040 Merge::from_vec(vec![left, base, right])
1041 } else {
1042 Merge::resolved(BString::new(vec![]))
1044 }
1045}
1046
1047pub async fn update_from_content(
1051 file_ids: &Merge<Option<FileId>>,
1052 store: &Store,
1053 path: &RepoPath,
1054 content: &[u8],
1055 conflict_marker_len: usize,
1056) -> BackendResult<Merge<Option<FileId>>> {
1057 let simplified_file_ids = file_ids.simplify();
1058
1059 let old_contents = extract_as_single_hunk(&simplified_file_ids, store, path).await?;
1060 let old_hunks = files::merge_hunks(&old_contents, store.merge_options());
1061
1062 let new_hunks = parse_conflict(
1065 content,
1066 simplified_file_ids.num_sides(),
1067 conflict_marker_len,
1068 );
1069
1070 let unchanged = match (&old_hunks, &new_hunks) {
1073 (MergeResult::Resolved(old), None) => old == content,
1074 (MergeResult::Conflict(old), Some(new)) => old == new,
1075 (MergeResult::Resolved(_), Some(_)) | (MergeResult::Conflict(_), None) => false,
1076 };
1077 if unchanged {
1078 return Ok(file_ids.clone());
1079 }
1080
1081 let Some(hunks) = new_hunks else {
1082 let file_id = store.write_file(path, &mut &content[..]).await?;
1084 return Ok(Merge::normal(file_id));
1085 };
1086
1087 let mut contents = simplified_file_ids.map(|_| vec![]);
1088 for hunk in hunks {
1089 if let Some(slice) = hunk.as_resolved() {
1090 for content in &mut contents {
1091 content.extend_from_slice(slice);
1092 }
1093 } else {
1094 for (content, slice) in zip(&mut contents, hunk) {
1095 content.extend(Vec::from(slice));
1096 }
1097 }
1098 }
1099
1100 let new_file_ids: Vec<Option<FileId>> = zip(&contents, &simplified_file_ids)
1104 .map(|(content, file_id)| -> BackendResult<Option<FileId>> {
1105 if file_id.is_some() || !content.is_empty() {
1106 let file_id = store.write_file(path, &mut content.as_slice()).block_on()?;
1107 Ok(Some(file_id))
1108 } else {
1109 Ok(None)
1112 }
1113 })
1114 .try_collect()?;
1115
1116 let new_file_ids = if new_file_ids.len() != file_ids.iter().len() {
1119 file_ids
1120 .clone()
1121 .update_from_simplified(Merge::from_vec(new_file_ids))
1122 } else {
1123 Merge::from_vec(new_file_ids)
1124 };
1125 Ok(new_file_ids)
1126}
1127
1128#[cfg(test)]
1129mod tests {
1130 #![expect(clippy::too_many_arguments)]
1131
1132 use test_case::test_case;
1133 use test_case::test_matrix;
1134
1135 use super::*;
1136 use crate::files::FileMergeHunkLevel;
1137
1138 #[test]
1139 fn test_resolve_file_executable() {
1140 fn resolve<const N: usize>(values: [Option<bool>; N]) -> Option<bool> {
1141 resolve_file_executable(&Merge::from_vec(values.to_vec()))
1142 }
1143
1144 assert_eq!(resolve([None]), None);
1146 assert_eq!(resolve([Some(false)]), Some(false));
1147 assert_eq!(resolve([Some(true)]), Some(true));
1148
1149 assert_eq!(resolve([Some(true), Some(true), Some(true)]), Some(true));
1151 assert_eq!(resolve([Some(true), Some(false), Some(false)]), Some(true));
1152 assert_eq!(resolve([Some(false), Some(true), Some(false)]), Some(false));
1153 assert_eq!(resolve([None, None, Some(true)]), Some(true));
1154
1155 assert_eq!(resolve([Some(false), Some(true), None]), None);
1157
1158 assert_eq!(resolve([Some(true), Some(true), None]), Some(true));
1160 assert_eq!(resolve([None, Some(false), Some(false)]), Some(false));
1161 assert_eq!(
1162 resolve([None, None, Some(true), Some(true), None]),
1163 Some(true)
1164 );
1165
1166 assert_eq!(
1168 resolve([Some(true), Some(true), None, Some(false), Some(false)]),
1169 None
1170 );
1171 assert_eq!(
1172 resolve([
1173 None,
1174 Some(true),
1175 Some(true),
1176 Some(false),
1177 Some(false),
1178 Some(false),
1179 Some(false),
1180 ]),
1181 None
1182 );
1183 }
1184
1185 #[test_case(Merge::resolved("\n") => "\n"; "starts with LF")]
1186 #[test_case(Merge::resolved("a\r\n") => "\r\n"; "crlf")]
1187 #[test_case(Merge::resolved("a\n") => "\n"; "lf")]
1188 #[test_case(Merge::resolved("abc") => "\n"; "no eol")]
1189 #[test_case(Merge::from_vec(vec![
1190 "a",
1191 "a\n",
1192 "ab",
1193 ]) => "\n"; "only the second side has the LF eol")]
1194 #[test_case(Merge::from_vec(vec![
1195 "a\r\n",
1196 "ab",
1197 "a\n",
1198 ]) => "\n"; "both sides have different EOLs")]
1199 #[test_case(Merge::from_vec(vec![
1200 "a",
1201 "a\r\n",
1202 "ab",
1203 ]) => "\r\n"; "only the second side has the CRLF eol")]
1204 fn test_detect_eol(single_hunk: Merge<impl AsRef<[u8]>>) -> &'static str {
1205 detect_eol(&single_hunk).to_str().unwrap()
1206 }
1207
1208 #[test]
1209 fn test_detect_eol_consistency() {
1210 let crlf_side = "crlf\r\n";
1211 let lf_side = "lf\n";
1212 let merges = [
1213 Merge::from_vec(vec![crlf_side, "base", lf_side]),
1214 Merge::from_vec(vec![lf_side, "base", crlf_side]),
1215 ];
1216
1217 assert_eq!(detect_eol(&merges[0]), detect_eol(&merges[1]));
1218 }
1219
1220 #[test_case(indoc::indoc!{b"
1221 <<<<<<< conflict 1 of 1
1222 %%%%%%% diff from base to side #1
1223 -aa
1224 +cc
1225 +++++++ side #2
1226 bb
1227 >>>>>>> conflict 1 of 1 ends
1228 "}, Merge::from_vec(vec![
1229 "cc\n",
1230 "aa\n",
1231 "bb\n",
1232 ]); "all sides end with EOL")]
1233 #[test_case(indoc::indoc!{b"
1234 <<<<<<< conflict 1 of 1
1235 %%%%%%% diff from base to side #1
1236 -aa
1237 +cc
1238 +++++++ side #2
1239 bb
1240 >>>>>>> conflict 1 of 1 ends"
1241 }, Merge::from_vec(vec![
1242 "cc",
1243 "aa",
1244 "bb",
1245 ]); "all sides end without EOL")]
1246 #[test_case(indoc::indoc!{b"
1247 <<<<<<< conflict 1 of 1
1248 %%%%%%% diff from base to side #1
1249 -aa
1250 +cc
1251
1252 +++++++ side #2
1253 bb
1254 >>>>>>> conflict 1 of 1 ends"
1255 }, Merge::from_vec(vec![
1256 "cc\n",
1257 "aa\n",
1258 "bb",
1259 ]); "side 2 removes the ending EOL")]
1260 #[test_case(indoc::indoc!{b"
1261 <<<<<<< conflict 1 of 1
1262 %%%%%%% diff from base to side #1
1263 -aa
1264 +cc
1265 +++++++ side #2
1266 bb
1267
1268 >>>>>>> conflict 1 of 1 ends"
1269 }, Merge::from_vec(vec![
1270 "cc",
1271 "aa",
1272 "bb\n",
1273 ]); "side 2 adds the ending EOL")]
1274 #[test_case(indoc::indoc!{b"
1275 <<<<<<< conflict 1 of 1
1276 %%%%%%% diff from base to side #1
1277 -aa
1278 -
1279 +cc
1280 +++++++ side #2
1281 bb
1282
1283 >>>>>>> conflict 1 of 1 ends"
1284 }, Merge::from_vec(vec![
1285 "cc",
1286 "aa\n",
1287 "bb\n",
1288 ]); "side 1 removes the ending EOL")]
1289 #[test_case(indoc::indoc!{b"
1290 <<<<<<< conflict 1 of 1
1291 %%%%%%% diff from base to side #1
1292 -aa
1293 +cc
1294 +
1295 +++++++ side #2
1296 bb
1297 >>>>>>> conflict 1 of 1 ends"
1298 }, Merge::from_vec(vec![
1299 "cc\n",
1300 "aa",
1301 "bb",
1302 ]); "side 1 adds the ending EOL")]
1303 fn test_parse_conflict(contents: &[u8], expected_merge: Merge<&str>) {
1304 let actual_result = parse_conflict(contents, 2, 7).unwrap()[0]
1305 .clone()
1306 .map(|content| content.to_str().unwrap().to_owned());
1307 let expected_merge = expected_merge.map(|content| content.to_string());
1308 assert_eq!(actual_result, expected_merge);
1309
1310 let actual_result = parse_conflict(&contents.replace(b"\n", b"\r\n"), 2, 7).unwrap()[0]
1312 .clone()
1313 .map(|content| content.to_str().unwrap().to_owned());
1314 let expected_merge = expected_merge.map(|content| content.replace('\n', "\r\n"));
1315 assert_eq!(actual_result, expected_merge);
1316 }
1317
1318 const BASE: &str = "aa";
1319 const SIDE1: &str = "bb";
1320 const SIDE2: &str = "cc";
1321 const WITH_ENDING_EOL: &str = "\n";
1322 const WITHOUT_ENDING_EOL: &str = "";
1323 const GIT_STYLE: ConflictMarkerStyle = ConflictMarkerStyle::Git;
1324 const DIFF_STYLE: ConflictMarkerStyle = ConflictMarkerStyle::Diff;
1325 const DIFF_EXPERIMENTAL_STYLE: ConflictMarkerStyle = ConflictMarkerStyle::DiffExperimental;
1326 const SNAPSHOT_STYLE: ConflictMarkerStyle = ConflictMarkerStyle::Snapshot;
1327 const LF_EOL: &str = "\n";
1328 const CRLF_EOL: &str = "\r\n";
1329 fn long(original: &str) -> String {
1330 std::iter::repeat_n(original, 3).collect_vec().join("\n")
1331 }
1332 fn prepended(original: &str) -> String {
1333 format!("{original}\n{BASE}")
1334 }
1335 #[test_matrix(
1336 BASE,
1337 [WITH_ENDING_EOL, WITHOUT_ENDING_EOL],
1338 [SIDE1, &long(SIDE1), &prepended(SIDE1)],
1339 [WITH_ENDING_EOL, WITHOUT_ENDING_EOL],
1340 [SIDE2, &long(SIDE2), &prepended(SIDE2)],
1341 [WITH_ENDING_EOL, WITHOUT_ENDING_EOL],
1342 [GIT_STYLE, DIFF_STYLE, DIFF_EXPERIMENTAL_STYLE, SNAPSHOT_STYLE],
1343 [LF_EOL, CRLF_EOL]
1344 )]
1345 fn test_materialize_conflict(
1346 base: &str,
1347 base_ending_eol: &str,
1348 side1: &str,
1349 side1_ending_eol: &str,
1350 side2: &str,
1351 side2_ending_eol: &str,
1352 style: ConflictMarkerStyle,
1353 eol: &str,
1354 ) {
1355 let base = format!("\n{base}{base_ending_eol}").replace('\n', eol);
1357 let side1 = format!("\n{side1}{side1_ending_eol}").replace('\n', eol);
1358 let side2 = format!("\n{side2}{side2_ending_eol}").replace('\n', eol);
1359 let merge = Merge::from_vec(vec![side2.as_str(), base.as_str(), side1.as_str()]);
1360 let options = ConflictMaterializeOptions {
1361 marker_style: style,
1362 marker_len: None,
1363 merge: MergeOptions {
1364 hunk_level: FileMergeHunkLevel::Line,
1365 same_change: SameChange::Accept,
1366 },
1367 };
1368 let actual_contents = String::from_utf8(
1369 materialize_merge_result_to_bytes(&merge, &ConflictLabels::unlabeled(), &options)
1370 .into(),
1371 )
1372 .unwrap();
1373 for line in actual_contents.as_bytes().lines_with_terminator() {
1375 let line = line.as_bstr();
1376 if !line.ends_with(b"\n") {
1377 continue;
1378 }
1379 let should_end_with_crlf = eol == "\r\n";
1380 assert!(
1381 line.ends_with(b"\r\n") == should_end_with_crlf,
1382 "Expect all the lines with EOL to end with {eol:?}, but got {line:?} from\n{}",
1383 actual_contents
1384 .replace('\r', "\u{240D}")
1386 .replace('\n', "\u{240A}\n")
1387 );
1388 }
1389 let hunks = parse_conflict(actual_contents.as_bytes(), 2, 7).unwrap();
1390 assert!(hunks.len() >= 2);
1391 let leading_eol = hunks[0].as_resolved().unwrap();
1393 let mut actual_merge = hunks[1].clone();
1394 for content in &mut actual_merge {
1395 content.insert_str(0, leading_eol);
1396 }
1397 if hunks.len() == 3 {
1399 let new_content = hunks[2].as_resolved().unwrap();
1400 for content in &mut actual_merge {
1401 content.extend_from_slice(new_content);
1402 }
1403 }
1404 assert!(hunks.len() <= 3);
1405 let actual_merge = actual_merge.map(|content| content.to_str().unwrap().to_owned());
1406 let merge = merge.map(|content| content.to_string());
1407 assert_eq!(actual_merge, merge);
1408 }
1409}