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