// Copyright 2020 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. #![allow(missing_docs)] use std::io;use std::io::Read;use std::io::Write;use std::iter::zip; use bstr::BString;use bstr::ByteSlice as _;use futures::stream::BoxStream;use futures::try_join;use futures::Stream;use futures::StreamExt as _;use futures::TryStreamExt as _;use itertools::Itertools as _;use pollster::FutureExt as _; use crate::backend::BackendError;use crate::backend::BackendResult;use crate::backend::CommitId;use crate::backend::FileId;use crate::backend::SymlinkId;use crate::backend::TreeId;use crate::backend::TreeValue;use crate::copies::CopiesTreeDiffEntry;use crate::copies::CopiesTreeDiffEntryPath;use crate::diff::Diff;use crate::diff::DiffHunk;use crate::diff::DiffHunkKind;use crate::files;use crate::files::MergeResult;use crate::merge::Merge;use crate::merge::MergeBuilder;use crate::merge::MergedTreeValue;use crate::repo_path::RepoPath;use crate::store::Store; /// Minimum length of conflict markers.pub const MIN_CONFLICT_MARKER_LEN: usize = 7; /// If a file already contains lines which look like conflict markers of length/// N, then the conflict markers we add will be of length (N + increment). This/// number is chosen to make the conflict markers noticeably longer than the/// existing markers.const CONFLICT_MARKER_LEN_INCREMENT: usize = 4; /// Comment for missing terminating newline in a term of a conflict.const NO_EOL_COMMENT: &str = " (no terminating newline)"; /// Comment for missing terminating newline in the "add" side of a diff.const ADD_NO_EOL_COMMENT: &str = " (removes terminating newline)"; /// Comment for missing terminating newline in the "remove" side of a diff.const REMOVE_NO_EOL_COMMENT: &str = " (adds terminating newline)"; fn write_diff_hunks(hunks: &[DiffHunk], file: &mut dyn Write) -> io::Result<()> { for hunk in hunks { match hunk.kind { DiffHunkKind::Matching => { debug_assert!(hunk.contents.iter().all_equal()); for line in hunk.contents[0].lines_with_terminator() { file.write_all(b" ")?; write_and_ensure_newline(file, line)?; } } DiffHunkKind::Different => { for line in hunk.contents[0].lines_with_terminator() { file.write_all(b"-")?; write_and_ensure_newline(file, line)?; } for line in hunk.contents[1].lines_with_terminator() { file.write_all(b"+")?; write_and_ensure_newline(file, line)?; } } } } Ok(())} async fn get_file_contents( store: &Store, path: &RepoPath, term: &Option<FileId>,) -> BackendResult<BString> { match term { Some(id) => { let mut content = vec![]; store .read_file_async(path, id) .await? .read_to_end(&mut content) .map_err(|err| BackendError::ReadFile { path: path.to_owned(), id: id.clone(), source: err.into(), })?; Ok(BString::new(content)) } // If the conflict had removed the file on one side, we pretend that the file // was empty there. None => Ok(BString::new(vec![])), }} pub async fn extract_as_single_hunk( merge: &Merge<Option<FileId>>, store: &Store, path: &RepoPath,) -> BackendResult<Merge<BString>> { let builder: MergeBuilder<BString> = futures::stream::iter(merge.iter()) .then(|term| get_file_contents(store, path, term)) .try_collect() .await?; Ok(builder.build())} /// A type similar to `MergedTreeValue` but with associated data to include in/// e.g. the working copy or in a diff.pub enum MaterializedTreeValue { Absent, AccessDenied(Box<dyn std::error::Error + Send + Sync>), File(MaterializedFileValue), Symlink { id: SymlinkId, target: String, }, FileConflict { id: Merge<Option<FileId>>, // TODO: or Vec<(FileId, Box<dyn Read>)> so that caller can stop reading // when null bytes found? contents: Merge<BString>, executable: bool, }, OtherConflict { id: MergedTreeValue, }, GitSubmodule(CommitId), Tree(TreeId),} impl MaterializedTreeValue { pub fn is_absent(&self) -> bool { matches!(self, MaterializedTreeValue::Absent) } pub fn is_present(&self) -> bool { !self.is_absent() }} /// [`TreeValue::File`] with file content `reader`.pub struct MaterializedFileValue { pub id: FileId, pub executable: bool, pub reader: Box<dyn Read>,} impl MaterializedFileValue { /// Reads file content until EOF. The provided `path` is used only for error /// reporting purpose. pub fn read_all(&mut self, path: &RepoPath) -> BackendResult<Vec<u8>> { let mut buf = Vec::new(); self.reader .read_to_end(&mut buf) .map_err(|err| BackendError::ReadFile { path: path.to_owned(), id: self.id.clone(), source: err.into(), })?; Ok(buf) }} /// Reads the data associated with a `MergedTreeValue` so it can be written to/// e.g. the working copy or diff.pub async fn materialize_tree_value( store: &Store, path: &RepoPath, value: MergedTreeValue,) -> BackendResult<MaterializedTreeValue> { match materialize_tree_value_no_access_denied(store, path, value).await { Err(BackendError::ReadAccessDenied { source, .. }) => { Ok(MaterializedTreeValue::AccessDenied(source)) } result => result, }} async fn materialize_tree_value_no_access_denied( store: &Store, path: &RepoPath, value: MergedTreeValue,) -> BackendResult<MaterializedTreeValue> { match value.into_resolved() { Ok(None) => Ok(MaterializedTreeValue::Absent), Ok(Some(TreeValue::File { id, executable })) => { let reader = store.read_file_async(path, &id).await?; Ok(MaterializedTreeValue::File(MaterializedFileValue { id, executable, reader, })) } Ok(Some(TreeValue::Symlink(id))) => { let target = store.read_symlink_async(path, &id).await?; Ok(MaterializedTreeValue::Symlink { id, target }) } Ok(Some(TreeValue::GitSubmodule(id))) => Ok(MaterializedTreeValue::GitSubmodule(id)), Ok(Some(TreeValue::Tree(id))) => Ok(MaterializedTreeValue::Tree(id)), Ok(Some(TreeValue::Conflict(_))) => { panic!("cannot materialize legacy conflict object at path {path:?}"); } Err(conflict) => { let Some(file_merge) = conflict.to_file_merge() else { return Ok(MaterializedTreeValue::OtherConflict { id: conflict }); }; let file_merge = file_merge.simplify(); let contents = extract_as_single_hunk(&file_merge, store, path).await?; let executable = if let Some(merge) = conflict.to_executable_merge() { merge.resolve_trivial().copied().unwrap_or_default() } else { false }; Ok(MaterializedTreeValue::FileConflict { id: file_merge, contents, executable, }) } }} /// Describes what style should be used when materializing conflicts.#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)]#[serde(rename_all = "kebab-case")]pub enum ConflictMarkerStyle { /// Style which shows a snapshot and a series of diffs to apply. #[default] Diff, /// Style which shows a snapshot for each base and side. Snapshot, /// Style which replicates Git's "diff3" style to support external tools. Git,} /// Characters which can be repeated to form a conflict marker line when/// materializing and parsing conflicts.#[derive(Clone, Copy, PartialEq, Eq)]#[repr(u8)]enum ConflictMarkerLineChar { ConflictStart = b'<', ConflictEnd = b'>', Add = b'+', Remove = b'-', Diff = b'%', GitAncestor = b'|', GitSeparator = b'=',} impl ConflictMarkerLineChar { /// Get the ASCII byte used for this conflict marker. fn to_byte(self) -> u8 { self as u8 } /// Parse a byte to see if it corresponds with any kind of conflict marker. fn parse_byte(byte: u8) -> Option<Self> { match byte { b'<' => Some(Self::ConflictStart), b'>' => Some(Self::ConflictEnd), b'+' => Some(Self::Add), b'-' => Some(Self::Remove), b'%' => Some(Self::Diff), b'|' => Some(Self::GitAncestor), b'=' => Some(Self::GitSeparator), _ => None, } }} /// Represents a conflict marker line parsed from the file. Conflict marker/// lines consist of a single ASCII character repeated for a certain length.struct ConflictMarkerLine { kind: ConflictMarkerLineChar, len: usize,} /// Write a conflict marker to an output file.fn write_conflict_marker( output: &mut dyn Write, kind: ConflictMarkerLineChar, len: usize, suffix_text: &str,) -> io::Result<()> { let conflict_marker = BString::new(vec![kind.to_byte(); len]); if suffix_text.is_empty() { writeln!(output, "{conflict_marker}") } else { writeln!(output, "{conflict_marker} {suffix_text}") }} /// Parse a conflict marker from a line of a file. The conflict marker may have/// any length (even less than MIN_CONFLICT_MARKER_LEN).fn parse_conflict_marker_any_len(line: &[u8]) -> Option<ConflictMarkerLine> { let first_byte = *line.first()?; let kind = ConflictMarkerLineChar::parse_byte(first_byte)?; let len = line.iter().take_while(|&&b| b == first_byte).count(); if let Some(next_byte) = line.get(len) { // If there is a character after the marker, it must be ASCII whitespace if !next_byte.is_ascii_whitespace() { return None; } } Some(ConflictMarkerLine { kind, len })} /// Parse a conflict marker, expecting it to be at least a certain length. Any/// shorter conflict markers are ignored.fn parse_conflict_marker(line: &[u8], expected_len: usize) -> Option<ConflictMarkerLineChar> { parse_conflict_marker_any_len(line) .filter(|marker| marker.len >= expected_len) .map(|marker| marker.kind)} /// Given a Merge of files, choose the conflict marker length to use when/// materializing conflicts.pub fn choose_materialized_conflict_marker_len<T: AsRef<[u8]>>(single_hunk: &Merge<T>) -> usize { let max_existing_marker_len = single_hunk .iter() .flat_map(|file| file.as_ref().lines_with_terminator()) .filter_map(parse_conflict_marker_any_len) .map(|marker| marker.len) .max() .unwrap_or_default(); max_existing_marker_len .saturating_add(CONFLICT_MARKER_LEN_INCREMENT) .max(MIN_CONFLICT_MARKER_LEN)} pub fn materialize_merge_result<T: AsRef<[u8]>>( single_hunk: &Merge<T>, conflict_marker_style: ConflictMarkerStyle, output: &mut dyn Write,) -> io::Result<()> { let merge_result = files::merge(single_hunk); match &merge_result { MergeResult::Resolved(content) => output.write_all(content), MergeResult::Conflict(hunks) => { let conflict_marker_len = choose_materialized_conflict_marker_len(single_hunk); materialize_conflict_hunks(hunks, conflict_marker_style, conflict_marker_len, output) } }} pub fn materialize_merge_result_with_marker_len<T: AsRef<[u8]>>( single_hunk: &Merge<T>, conflict_marker_style: ConflictMarkerStyle, conflict_marker_len: usize, output: &mut dyn Write,) -> io::Result<()> { let merge_result = files::merge(single_hunk); match &merge_result { MergeResult::Resolved(content) => output.write_all(content), MergeResult::Conflict(hunks) => { materialize_conflict_hunks(hunks, conflict_marker_style, conflict_marker_len, output) } }} pub fn materialize_merge_result_to_bytes<T: AsRef<[u8]>>( single_hunk: &Merge<T>, conflict_marker_style: ConflictMarkerStyle,) -> BString { let merge_result = files::merge(single_hunk); match merge_result { MergeResult::Resolved(content) => content, MergeResult::Conflict(hunks) => { let conflict_marker_len = choose_materialized_conflict_marker_len(single_hunk); let mut output = Vec::new(); materialize_conflict_hunks( &hunks, conflict_marker_style, conflict_marker_len, &mut output, ) .expect("writing to an in-memory buffer should never fail"); output.into() } }} pub fn materialize_merge_result_to_bytes_with_marker_len<T: AsRef<[u8]>>( single_hunk: &Merge<T>, conflict_marker_style: ConflictMarkerStyle, conflict_marker_len: usize,) -> BString { let merge_result = files::merge(single_hunk); match merge_result { MergeResult::Resolved(content) => content, MergeResult::Conflict(hunks) => { let mut output = Vec::new(); materialize_conflict_hunks( &hunks, conflict_marker_style, conflict_marker_len, &mut output, ) .expect("writing to an in-memory buffer should never fail"); output.into() } }} fn materialize_conflict_hunks( hunks: &[Merge<BString>], conflict_marker_style: ConflictMarkerStyle, conflict_marker_len: usize, output: &mut dyn Write,) -> io::Result<()> { let num_conflicts = hunks .iter() .filter(|hunk| hunk.as_resolved().is_none()) .count(); let mut conflict_index = 0; for hunk in hunks { if let Some(content) = hunk.as_resolved() { output.write_all(content)?; } else { conflict_index += 1; let conflict_info = format!("Conflict {conflict_index} of {num_conflicts}"); match (conflict_marker_style, hunk.as_slice()) { // 2-sided conflicts can use Git-style conflict markers (ConflictMarkerStyle::Git, [left, base, right]) => { materialize_git_style_conflict( left, base, right, &conflict_info, conflict_marker_len, output, )?; } _ => { materialize_jj_style_conflict( hunk, &conflict_info, conflict_marker_style, conflict_marker_len, output, )?; } } } } Ok(())} fn materialize_git_style_conflict( left: &[u8], base: &[u8], right: &[u8], conflict_info: &str, conflict_marker_len: usize, output: &mut dyn Write,) -> io::Result<()> { write_conflict_marker( output, ConflictMarkerLineChar::ConflictStart, conflict_marker_len, &format!("Side #1 ({conflict_info})"), )?; write_and_ensure_newline(output, left)?; write_conflict_marker( output, ConflictMarkerLineChar::GitAncestor, conflict_marker_len, "Base", )?;Lines 1–500 of 1018. Load more
// Copyright 2020 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #![allow(missing_docs)] use std::io; use std::io::Read; use std::io::Write; use std::iter::zip; use bstr::BString; use bstr::ByteSlice as _; use futures::stream::BoxStream; use futures::try_join; use futures::Stream; use futures::StreamExt as _; use futures::TryStreamExt as _; use itertools::Itertools as _; use pollster::FutureExt as _; use crate::backend::BackendError; use crate::backend::BackendResult; use crate::backend::CommitId; use crate::backend::FileId; use crate::backend::SymlinkId; use crate::backend::TreeId; use crate::backend::TreeValue; use crate::copies::CopiesTreeDiffEntry; use crate::copies::CopiesTreeDiffEntryPath; use crate::diff::Diff; use crate::diff::DiffHunk; use crate::diff::DiffHunkKind; use crate::files; use crate::files::MergeResult; use crate::merge::Merge; use crate::merge::MergeBuilder; use crate::merge::MergedTreeValue; use crate::repo_path::RepoPath; use crate::store::Store; /// Minimum length of conflict markers. pub const MIN_CONFLICT_MARKER_LEN: usize = 7; /// If a file already contains lines which look like conflict markers of length /// N, then the conflict markers we add will be of length (N + increment). This /// number is chosen to make the conflict markers noticeably longer than the /// existing markers. const CONFLICT_MARKER_LEN_INCREMENT: usize = 4; /// Comment for missing terminating newline in a term of a conflict. const NO_EOL_COMMENT: &str = " (no terminating newline)"; /// Comment for missing terminating newline in the "add" side of a diff. const ADD_NO_EOL_COMMENT: &str = " (removes terminating newline)"; /// Comment for missing terminating newline in the "remove" side of a diff. const REMOVE_NO_EOL_COMMENT: &str = " (adds terminating newline)"; fn write_diff_hunks(hunks: &[DiffHunk], file: &mut dyn Write) -> io::Result<()> { for hunk in hunks { match hunk.kind { DiffHunkKind::Matching => { debug_assert!(hunk.contents.iter().all_equal()); for line in hunk.contents[0].lines_with_terminator() { file.write_all(b" ")?; write_and_ensure_newline(file, line)?; } } DiffHunkKind::Different => { for line in hunk.contents[0].lines_with_terminator() { file.write_all(b"-")?; write_and_ensure_newline(file, line)?; } for line in hunk.contents[1].lines_with_terminator() { file.write_all(b"+")?; write_and_ensure_newline(file, line)?; } } } } Ok(()) } async fn get_file_contents( store: &Store, path: &RepoPath, term: &Option<FileId>, ) -> BackendResult<BString> { match term { Some(id) => { let mut content = vec![]; store .read_file_async(path, id) .await? .read_to_end(&mut content) .map_err(|err| BackendError::ReadFile { path: path.to_owned(), id: id.clone(), source: err.into(), })?; Ok(BString::new(content)) } // If the conflict had removed the file on one side, we pretend that the file // was empty there. None => Ok(BString::new(vec![])), } } pub async fn extract_as_single_hunk( merge: &Merge<Option<FileId>>, store: &Store, path: &RepoPath, ) -> BackendResult<Merge<BString>> { let builder: MergeBuilder<BString> = futures::stream::iter(merge.iter()) .then(|term| get_file_contents(store, path, term)) .try_collect() .await?; Ok(builder.build()) } /// A type similar to `MergedTreeValue` but with associated data to include in /// e.g. the working copy or in a diff. pub enum MaterializedTreeValue { Absent, AccessDenied(Box<dyn std::error::Error + Send + Sync>), File(MaterializedFileValue), Symlink { id: SymlinkId, target: String, }, FileConflict { id: Merge<Option<FileId>>, // TODO: or Vec<(FileId, Box<dyn Read>)> so that caller can stop reading // when null bytes found? contents: Merge<BString>, executable: bool, }, OtherConflict { id: MergedTreeValue, }, GitSubmodule(CommitId), Tree(TreeId), } impl MaterializedTreeValue { pub fn is_absent(&self) -> bool { matches!(self, MaterializedTreeValue::Absent) } pub fn is_present(&self) -> bool { !self.is_absent() } } /// [`TreeValue::File`] with file content `reader`. pub struct MaterializedFileValue { pub id: FileId, pub executable: bool, pub reader: Box<dyn Read>, } impl MaterializedFileValue { /// Reads file content until EOF. The provided `path` is used only for error /// reporting purpose. pub fn read_all(&mut self, path: &RepoPath) -> BackendResult<Vec<u8>> { let mut buf = Vec::new(); self.reader .read_to_end(&mut buf) .map_err(|err| BackendError::ReadFile { path: path.to_owned(), id: self.id.clone(), source: err.into(), })?; Ok(buf) } } /// Reads the data associated with a `MergedTreeValue` so it can be written to /// e.g. the working copy or diff. pub async fn materialize_tree_value( store: &Store, path: &RepoPath, value: MergedTreeValue, ) -> BackendResult<MaterializedTreeValue> { match materialize_tree_value_no_access_denied(store, path, value).await { Err(BackendError::ReadAccessDenied { source, .. }) => { Ok(MaterializedTreeValue::AccessDenied(source)) } result => result, } } async fn materialize_tree_value_no_access_denied( store: &Store, path: &RepoPath, value: MergedTreeValue, ) -> BackendResult<MaterializedTreeValue> { match value.into_resolved() { Ok(None) => Ok(MaterializedTreeValue::Absent), Ok(Some(TreeValue::File { id, executable })) => { let reader = store.read_file_async(path, &id).await?; Ok(MaterializedTreeValue::File(MaterializedFileValue { id, executable, reader, })) } Ok(Some(TreeValue::Symlink(id))) => { let target = store.read_symlink_async(path, &id).await?; Ok(MaterializedTreeValue::Symlink { id, target }) } Ok(Some(TreeValue::GitSubmodule(id))) => Ok(MaterializedTreeValue::GitSubmodule(id)), Ok(Some(TreeValue::Tree(id))) => Ok(MaterializedTreeValue::Tree(id)), Ok(Some(TreeValue::Conflict(_))) => { panic!("cannot materialize legacy conflict object at path {path:?}"); } Err(conflict) => { let Some(file_merge) = conflict.to_file_merge() else { return Ok(MaterializedTreeValue::OtherConflict { id: conflict }); }; let file_merge = file_merge.simplify(); let contents = extract_as_single_hunk(&file_merge, store, path).await?; let executable = if let Some(merge) = conflict.to_executable_merge() { merge.resolve_trivial().copied().unwrap_or_default() } else { false }; Ok(MaterializedTreeValue::FileConflict { id: file_merge, contents, executable, }) } } } /// Describes what style should be used when materializing conflicts. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Deserialize)] #[serde(rename_all = "kebab-case")] pub enum ConflictMarkerStyle { /// Style which shows a snapshot and a series of diffs to apply. #[default] Diff, /// Style which shows a snapshot for each base and side. Snapshot, /// Style which replicates Git's "diff3" style to support external tools. Git, } /// Characters which can be repeated to form a conflict marker line when /// materializing and parsing conflicts. #[derive(Clone, Copy, PartialEq, Eq)] #[repr(u8)] enum ConflictMarkerLineChar { ConflictStart = b'<', ConflictEnd = b'>', Add = b'+', Remove = b'-', Diff = b'%', GitAncestor = b'|', GitSeparator = b'=', } impl ConflictMarkerLineChar { /// Get the ASCII byte used for this conflict marker. fn to_byte(self) -> u8 { self as u8 } /// Parse a byte to see if it corresponds with any kind of conflict marker. fn parse_byte(byte: u8) -> Option<Self> { match byte { b'<' => Some(Self::ConflictStart), b'>' => Some(Self::ConflictEnd), b'+' => Some(Self::Add), b'-' => Some(Self::Remove), b'%' => Some(Self::Diff), b'|' => Some(Self::GitAncestor), b'=' => Some(Self::GitSeparator), _ => None, } } } /// Represents a conflict marker line parsed from the file. Conflict marker /// lines consist of a single ASCII character repeated for a certain length. struct ConflictMarkerLine { kind: ConflictMarkerLineChar, len: usize, } /// Write a conflict marker to an output file. fn write_conflict_marker( output: &mut dyn Write, kind: ConflictMarkerLineChar, len: usize, suffix_text: &str, ) -> io::Result<()> { let conflict_marker = BString::new(vec![kind.to_byte(); len]); if suffix_text.is_empty() { writeln!(output, "{conflict_marker}") } else { writeln!(output, "{conflict_marker} {suffix_text}") } } /// Parse a conflict marker from a line of a file. The conflict marker may have /// any length (even less than MIN_CONFLICT_MARKER_LEN). fn parse_conflict_marker_any_len(line: &[u8]) -> Option<ConflictMarkerLine> { let first_byte = *line.first()?; let kind = ConflictMarkerLineChar::parse_byte(first_byte)?; let len = line.iter().take_while(|&&b| b == first_byte).count(); if let Some(next_byte) = line.get(len) { // If there is a character after the marker, it must be ASCII whitespace if !next_byte.is_ascii_whitespace() { return None; } } Some(ConflictMarkerLine { kind, len }) } /// Parse a conflict marker, expecting it to be at least a certain length. Any /// shorter conflict markers are ignored. fn parse_conflict_marker(line: &[u8], expected_len: usize) -> Option<ConflictMarkerLineChar> { parse_conflict_marker_any_len(line) .filter(|marker| marker.len >= expected_len) .map(|marker| marker.kind) } /// Given a Merge of files, choose the conflict marker length to use when /// materializing conflicts. pub fn choose_materialized_conflict_marker_len<T: AsRef<[u8]>>(single_hunk: &Merge<T>) -> usize { let max_existing_marker_len = single_hunk .iter() .flat_map(|file| file.as_ref().lines_with_terminator()) .filter_map(parse_conflict_marker_any_len) .map(|marker| marker.len) .max() .unwrap_or_default(); max_existing_marker_len .saturating_add(CONFLICT_MARKER_LEN_INCREMENT) .max(MIN_CONFLICT_MARKER_LEN) } pub fn materialize_merge_result<T: AsRef<[u8]>>( single_hunk: &Merge<T>, conflict_marker_style: ConflictMarkerStyle, output: &mut dyn Write, ) -> io::Result<()> { let merge_result = files::merge(single_hunk); match &merge_result { MergeResult::Resolved(content) => output.write_all(content), MergeResult::Conflict(hunks) => { let conflict_marker_len = choose_materialized_conflict_marker_len(single_hunk); materialize_conflict_hunks(hunks, conflict_marker_style, conflict_marker_len, output) } } } pub fn materialize_merge_result_with_marker_len<T: AsRef<[u8]>>( single_hunk: &Merge<T>, conflict_marker_style: ConflictMarkerStyle, conflict_marker_len: usize, output: &mut dyn Write, ) -> io::Result<()> { let merge_result = files::merge(single_hunk); match &merge_result { MergeResult::Resolved(content) => output.write_all(content), MergeResult::Conflict(hunks) => { materialize_conflict_hunks(hunks, conflict_marker_style, conflict_marker_len, output) } } } pub fn materialize_merge_result_to_bytes<T: AsRef<[u8]>>( single_hunk: &Merge<T>, conflict_marker_style: ConflictMarkerStyle, ) -> BString { let merge_result = files::merge(single_hunk); match merge_result { MergeResult::Resolved(content) => content, MergeResult::Conflict(hunks) => { let conflict_marker_len = choose_materialized_conflict_marker_len(single_hunk); let mut output = Vec::new(); materialize_conflict_hunks( &hunks, conflict_marker_style, conflict_marker_len, &mut output, ) .expect("writing to an in-memory buffer should never fail"); output.into() } } } pub fn materialize_merge_result_to_bytes_with_marker_len<T: AsRef<[u8]>>( single_hunk: &Merge<T>, conflict_marker_style: ConflictMarkerStyle, conflict_marker_len: usize, ) -> BString { let merge_result = files::merge(single_hunk); match merge_result { MergeResult::Resolved(content) => content, MergeResult::Conflict(hunks) => { let mut output = Vec::new(); materialize_conflict_hunks( &hunks, conflict_marker_style, conflict_marker_len, &mut output, ) .expect("writing to an in-memory buffer should never fail"); output.into() } } } fn materialize_conflict_hunks( hunks: &[Merge<BString>], conflict_marker_style: ConflictMarkerStyle, conflict_marker_len: usize, output: &mut dyn Write, ) -> io::Result<()> { let num_conflicts = hunks .iter() .filter(|hunk| hunk.as_resolved().is_none()) .count(); let mut conflict_index = 0; for hunk in hunks { if let Some(content) = hunk.as_resolved() { output.write_all(content)?; } else { conflict_index += 1; let conflict_info = format!("Conflict {conflict_index} of {num_conflicts}"); match (conflict_marker_style, hunk.as_slice()) { // 2-sided conflicts can use Git-style conflict markers (ConflictMarkerStyle::Git, [left, base, right]) => { materialize_git_style_conflict( left, base, right, &conflict_info, conflict_marker_len, output, )?; } _ => { materialize_jj_style_conflict( hunk, &conflict_info, conflict_marker_style, conflict_marker_len, output, )?; } } } } Ok(()) } fn materialize_git_style_conflict( left: &[u8], base: &[u8], right: &[u8], conflict_info: &str, conflict_marker_len: usize, output: &mut dyn Write, ) -> io::Result<()> { write_conflict_marker( output, ConflictMarkerLineChar::ConflictStart, conflict_marker_len, &format!("Side #1 ({conflict_info})"), )?; write_and_ensure_newline(output, left)?; write_conflict_marker( output, ConflictMarkerLineChar::GitAncestor, conflict_marker_len, "Base", )?;