jj/lib/src/conflicts.rs
Scott Taylor 66faeb4487 conflicts: show "noeol" state in conflict marker comment
This isn't strictly necessary since it doesn't affect parsing, but it
should make it more understandable for the user.
2025-01-27 22:59:06 +00:00

1003 lines
34 KiB
Rust

// 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;
use futures::stream::BoxStream;
use futures::try_join;
use futures::Stream;
use futures::StreamExt;
use futures::TryStreamExt;
use itertools::Itertools;
use pollster::FutureExt;
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 = " [noeol]";
/// Comment for missing terminating newline in the "add" side of a diff.
const ADD_NO_EOL_COMMENT: &str = " [+noeol]";
/// Comment for missing terminating newline in the "remove" side of a diff.
const REMOVE_NO_EOL_COMMENT: &str = " [-noeol]";
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 {
id: FileId,
executable: bool,
reader: Box<dyn Read>,
},
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()
}
}
/// 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 {
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})", maybe_no_eol_comment(left)),
)?;
write_and_ensure_newline(output, left)?;
write_conflict_marker(
output,
ConflictMarkerLineChar::GitAncestor,
conflict_marker_len,
&format!("Base{}", maybe_no_eol_comment(base)),
)?;
write_and_ensure_newline(output, base)?;
// VS Code doesn't seem to support any trailing text on the separator line
write_conflict_marker(
output,
ConflictMarkerLineChar::GitSeparator,
conflict_marker_len,
"",
)?;
write_and_ensure_newline(output, right)?;
write_conflict_marker(
output,
ConflictMarkerLineChar::ConflictEnd,
conflict_marker_len,
&format!(
"Side #2{} ({conflict_info} ends)",
maybe_no_eol_comment(right)
),
)?;
Ok(())
}
fn materialize_jj_style_conflict(
hunk: &Merge<BString>,
conflict_info: &str,
conflict_marker_style: ConflictMarkerStyle,
conflict_marker_len: usize,
output: &mut dyn Write,
) -> io::Result<()> {
// Write a positive snapshot (side) of a conflict
let write_side = |add_index: usize, data: &[u8], output: &mut dyn Write| {
write_conflict_marker(
output,
ConflictMarkerLineChar::Add,
conflict_marker_len,
&format!(
"Contents of side #{}{}",
add_index + 1,
maybe_no_eol_comment(data)
),
)?;
write_and_ensure_newline(output, data)
};
// Write a negative snapshot (base) of a conflict
let write_base = |base_str: &str, data: &[u8], output: &mut dyn Write| {
write_conflict_marker(
output,
ConflictMarkerLineChar::Remove,
conflict_marker_len,
&format!("Contents of {base_str}{}", maybe_no_eol_comment(data)),
)?;
write_and_ensure_newline(output, data)
};
// Write a diff from a negative term to a positive term
let write_diff =
|base_str: &str, add_index: usize, diff: &[DiffHunk], output: &mut dyn Write| {
let no_eol_remove = diff
.last()
.is_some_and(|diff_hunk| has_no_eol(diff_hunk.contents[0]));
let no_eol_add = diff
.last()
.is_some_and(|diff_hunk| has_no_eol(diff_hunk.contents[1]));
let no_eol_comment = match (no_eol_remove, no_eol_add) {
(true, true) => NO_EOL_COMMENT,
(true, _) => REMOVE_NO_EOL_COMMENT,
(_, true) => ADD_NO_EOL_COMMENT,
_ => "",
};
write_conflict_marker(
output,
ConflictMarkerLineChar::Diff,
conflict_marker_len,
&format!(
"Changes from {base_str} to side #{}{no_eol_comment}",
add_index + 1
),
)?;
write_diff_hunks(diff, output)
};
write_conflict_marker(
output,
ConflictMarkerLineChar::ConflictStart,
conflict_marker_len,
conflict_info,
)?;
let mut add_index = 0;
for (base_index, left) in hunk.removes().enumerate() {
// The vast majority of conflicts one actually tries to resolve manually have 1
// base.
let base_str = if hunk.removes().len() == 1 {
"base".to_string()
} else {
format!("base #{}", base_index + 1)
};
let Some(right1) = hunk.get_add(add_index) else {
// If we have no more positive terms, emit the remaining negative terms as
// snapshots.
write_base(&base_str, left, output)?;
continue;
};
// For any style other than "diff", always emit sides and bases separately
if conflict_marker_style != ConflictMarkerStyle::Diff {
write_side(add_index, right1, output)?;
write_base(&base_str, left, output)?;
add_index += 1;
continue;
}
let diff1 = Diff::by_line([&left, &right1]).hunks().collect_vec();
// Check if the diff against the next positive term is better. Since we want to
// preserve the order of the terms, we don't match against any later positive
// terms.
if let Some(right2) = hunk.get_add(add_index + 1) {
let diff2 = Diff::by_line([&left, &right2]).hunks().collect_vec();
if diff_size(&diff2) < diff_size(&diff1) {
// If the next positive term is a better match, emit the current positive term
// as a snapshot and the next positive term as a diff.
write_side(add_index, right1, output)?;
write_diff(&base_str, add_index + 1, &diff2, output)?;
add_index += 2;
continue;
}
}
write_diff(&base_str, add_index, &diff1, output)?;
add_index += 1;
}
// Emit the remaining positive terms as snapshots.
for (add_index, slice) in hunk.adds().enumerate().skip(add_index) {
write_side(add_index, slice, output)?;
}
write_conflict_marker(
output,
ConflictMarkerLineChar::ConflictEnd,
conflict_marker_len,
&format!("{conflict_info} ends"),
)?;
Ok(())
}
fn maybe_no_eol_comment(slice: &[u8]) -> &'static str {
if has_no_eol(slice) {
NO_EOL_COMMENT
} else {
""
}
}
// Write a chunk of data, ensuring that it doesn't end with a line which is
// missing its terminating newline.
fn write_and_ensure_newline(output: &mut dyn Write, data: &[u8]) -> io::Result<()> {
output.write_all(data)?;
if has_no_eol(data) {
writeln!(output)?;
}
Ok(())
}
// Check whether a slice is missing its terminating newline character.
fn has_no_eol(slice: &[u8]) -> bool {
slice.last().is_some_and(|&last| last != b'\n')
}
fn diff_size(hunks: &[DiffHunk]) -> usize {
hunks
.iter()
.map(|hunk| match hunk.kind {
DiffHunkKind::Matching => 0,
DiffHunkKind::Different => hunk.contents.iter().map(|content| content.len()).sum(),
})
.sum()
}
pub struct MaterializedTreeDiffEntry {
pub path: CopiesTreeDiffEntryPath,
pub values: BackendResult<(MaterializedTreeValue, MaterializedTreeValue)>,
}
pub fn materialized_diff_stream<'a>(
store: &'a Store,
tree_diff: BoxStream<'a, CopiesTreeDiffEntry>,
) -> impl Stream<Item = MaterializedTreeDiffEntry> + 'a {
tree_diff
.map(|CopiesTreeDiffEntry { path, values }| async {
match values {
Err(err) => MaterializedTreeDiffEntry {
path,
values: Err(err),
},
Ok((before, after)) => {
let before_future = materialize_tree_value(store, path.source(), before);
let after_future = materialize_tree_value(store, path.target(), after);
let values = try_join!(before_future, after_future);
MaterializedTreeDiffEntry { path, values }
}
}
})
.buffered((store.concurrency() / 2).max(1))
}
/// Parses conflict markers from a slice.
///
/// Returns `None` if there were no valid conflict markers. The caller
/// has to provide the expected number of merge sides (adds). Conflict
/// markers that are otherwise valid will be considered invalid if
/// they don't have the expected arity.
///
/// All conflict markers in the file must be at least as long as the expected
/// length. Any shorter conflict markers will be ignored.
// TODO: "parse" is not usually the opposite of "materialize", so maybe we
// should rename them to "serialize" and "deserialize"?
pub fn parse_conflict(
input: &[u8],
num_sides: usize,
expected_marker_len: usize,
) -> Option<Vec<Merge<BString>>> {
if input.is_empty() {
return None;
}
let mut hunks = vec![];
let mut pos = 0;
let mut resolved_start = 0;
let mut conflict_start = None;
let mut conflict_start_len = 0;
for line in input.lines_with_terminator() {
match parse_conflict_marker(line, expected_marker_len) {
Some(ConflictMarkerLineChar::ConflictStart) => {
conflict_start = Some(pos);
conflict_start_len = line.len();
}
Some(ConflictMarkerLineChar::ConflictEnd) => {
if let Some(conflict_start_index) = conflict_start.take() {
let conflict_body = &input[conflict_start_index + conflict_start_len..pos];
let hunk = parse_conflict_hunk(conflict_body, expected_marker_len);
if hunk.num_sides() == num_sides {
let resolved_slice = &input[resolved_start..conflict_start_index];
if !resolved_slice.is_empty() {
hunks.push(Merge::resolved(BString::from(resolved_slice)));
}
hunks.push(hunk);
resolved_start = pos + line.len();
}
}
}
_ => {}
}
pos += line.len();
}
if hunks.is_empty() {
None
} else {
if resolved_start < input.len() {
hunks.push(Merge::resolved(BString::from(&input[resolved_start..])));
}
Some(hunks)
}
}
/// This method handles parsing both JJ-style and Git-style conflict markers,
/// meaning that switching conflict marker styles won't prevent existing files
/// with other conflict marker styles from being parsed successfully. The
/// conflict marker style to use for parsing is determined based on the first
/// line of the hunk.
fn parse_conflict_hunk(input: &[u8], expected_marker_len: usize) -> Merge<BString> {
// If the hunk starts with a conflict marker, find its first character
let initial_conflict_marker = input
.lines_with_terminator()
.next()
.and_then(|line| parse_conflict_marker(line, expected_marker_len));
match initial_conflict_marker {
// JJ-style conflicts must start with one of these 3 conflict marker lines
Some(
ConflictMarkerLineChar::Diff
| ConflictMarkerLineChar::Remove
| ConflictMarkerLineChar::Add,
) => parse_jj_style_conflict_hunk(input, expected_marker_len),
// Git-style conflicts either must not start with a conflict marker line, or must start with
// the "|||||||" conflict marker line (if the first side was empty)
None | Some(ConflictMarkerLineChar::GitAncestor) => {
parse_git_style_conflict_hunk(input, expected_marker_len)
}
// No other conflict markers are allowed at the start of a hunk
Some(_) => Merge::resolved(BString::new(vec![])),
}
}
fn parse_jj_style_conflict_hunk(input: &[u8], expected_marker_len: usize) -> Merge<BString> {
enum State {
Diff,
Remove,
Add,
Unknown,
}
let mut state = State::Unknown;
let mut removes = vec![];
let mut adds = vec![];
for line in input.lines_with_terminator() {
match parse_conflict_marker(line, expected_marker_len) {
Some(ConflictMarkerLineChar::Diff) => {
state = State::Diff;
removes.push(BString::new(vec![]));
adds.push(BString::new(vec![]));
continue;
}
Some(ConflictMarkerLineChar::Remove) => {
state = State::Remove;
removes.push(BString::new(vec![]));
continue;
}
Some(ConflictMarkerLineChar::Add) => {
state = State::Add;
adds.push(BString::new(vec![]));
continue;
}
_ => {}
}
match state {
State::Diff => {
if let Some(rest) = line.strip_prefix(b"-") {
removes.last_mut().unwrap().extend_from_slice(rest);
} else if let Some(rest) = line.strip_prefix(b"+") {
adds.last_mut().unwrap().extend_from_slice(rest);
} else if let Some(rest) = line.strip_prefix(b" ") {
removes.last_mut().unwrap().extend_from_slice(rest);
adds.last_mut().unwrap().extend_from_slice(rest);
} else if line == b"\n" || line == b"\r\n" {
// Some editors strip trailing whitespace, so " \n" might become "\n". It would
// be unfortunate if this prevented the conflict from being parsed, so we add
// the empty line to the "remove" and "add" as if there was a space in front
removes.last_mut().unwrap().extend_from_slice(line);
adds.last_mut().unwrap().extend_from_slice(line);
} else {
// Doesn't look like a valid conflict
return Merge::resolved(BString::new(vec![]));
}
}
State::Remove => {
removes.last_mut().unwrap().extend_from_slice(line);
}
State::Add => {
adds.last_mut().unwrap().extend_from_slice(line);
}
State::Unknown => {
// Doesn't look like a valid conflict
return Merge::resolved(BString::new(vec![]));
}
}
}
if adds.len() == removes.len() + 1 {
Merge::from_removes_adds(removes, adds)
} else {
// Doesn't look like a valid conflict
Merge::resolved(BString::new(vec![]))
}
}
fn parse_git_style_conflict_hunk(input: &[u8], expected_marker_len: usize) -> Merge<BString> {
#[derive(PartialEq, Eq)]
enum State {
Left,
Base,
Right,
}
let mut state = State::Left;
let mut left = BString::new(vec![]);
let mut base = BString::new(vec![]);
let mut right = BString::new(vec![]);
for line in input.lines_with_terminator() {
match parse_conflict_marker(line, expected_marker_len) {
Some(ConflictMarkerLineChar::GitAncestor) => {
if state == State::Left {
state = State::Base;
continue;
} else {
// Base must come after left
return Merge::resolved(BString::new(vec![]));
}
}
Some(ConflictMarkerLineChar::GitSeparator) => {
if state == State::Base {
state = State::Right;
continue;
} else {
// Right must come after base
return Merge::resolved(BString::new(vec![]));
}
}
_ => {}
}
match state {
State::Left => left.extend_from_slice(line),
State::Base => base.extend_from_slice(line),
State::Right => right.extend_from_slice(line),
}
}
if state == State::Right {
Merge::from_vec(vec![left, base, right])
} else {
// Doesn't look like a valid conflict
Merge::resolved(BString::new(vec![]))
}
}
/// Parses conflict markers in `content` and returns an updated version of
/// `file_ids` with the new contents. If no (valid) conflict markers remain, a
/// single resolves `FileId` will be returned.
pub async fn update_from_content(
file_ids: &Merge<Option<FileId>>,
store: &Store,
path: &RepoPath,
content: &[u8],
conflict_marker_style: ConflictMarkerStyle,
conflict_marker_len: usize,
) -> BackendResult<Merge<Option<FileId>>> {
let simplified_file_ids = file_ids.clone().simplify();
// First check if the new content is unchanged compared to the old content. If
// it is, we don't need parse the content or write any new objects to the
// store. This is also a way of making sure that unchanged tree/file
// conflicts (for example) are not converted to regular files in the working
// copy.
let mut old_content = Vec::with_capacity(content.len());
let merge_hunk = extract_as_single_hunk(&simplified_file_ids, store, path).await?;
materialize_merge_result_with_marker_len(
&merge_hunk,
conflict_marker_style,
conflict_marker_len,
&mut old_content,
)
.unwrap();
if content == old_content {
return Ok(file_ids.clone());
}
// Parse conflicts from the new content using the arity of the simplified
// conflicts.
let Some(mut hunks) = parse_conflict(
content,
simplified_file_ids.num_sides(),
conflict_marker_len,
) else {
// Either there are no markers or they don't have the expected arity
let file_id = store.write_file(path, &mut &content[..]).await?;
return Ok(Merge::normal(file_id));
};
// If there is a conflict at the end of the file and a term ends with a newline,
// check whether the original term ended with a newline. If it didn't, then
// remove the newline since it was added automatically when materializing.
if let Some(last_hunk) = hunks.last_mut().filter(|hunk| !hunk.is_resolved()) {
for (original_content, term) in merge_hunk.iter().zip_eq(last_hunk.iter_mut()) {
if term.last() == Some(&b'\n') && has_no_eol(original_content) {
term.pop();
}
}
}
let mut contents = simplified_file_ids.map(|_| vec![]);
for hunk in hunks {
if let Some(slice) = hunk.as_resolved() {
for content in contents.iter_mut() {
content.extend_from_slice(slice);
}
} else {
for (content, slice) in zip(contents.iter_mut(), hunk.into_iter()) {
content.extend(Vec::from(slice));
}
}
}
// If the user edited the empty placeholder for an absent side, we consider the
// conflict resolved.
if zip(contents.iter(), simplified_file_ids.iter())
.any(|(content, file_id)| file_id.is_none() && !content.is_empty())
{
let file_id = store.write_file(path, &mut &content[..]).await?;
return Ok(Merge::normal(file_id));
}
// Now write the new files contents we found by parsing the file with conflict
// markers.
// TODO: Write these concurrently
let new_file_ids: Vec<Option<FileId>> = zip(contents.iter(), simplified_file_ids.iter())
.map(|(content, file_id)| -> BackendResult<Option<FileId>> {
match file_id {
Some(_) => {
let file_id = store.write_file(path, &mut content.as_slice()).block_on()?;
Ok(Some(file_id))
}
None => {
// The missing side of a conflict is still represented by
// the empty string we materialized it as
Ok(None)
}
}
})
.try_collect()?;
// If the conflict was simplified, expand the conflict to the original
// number of sides.
let new_file_ids = if new_file_ids.len() != file_ids.iter().len() {
file_ids
.clone()
.update_from_simplified(Merge::from_vec(new_file_ids))
} else {
Merge::from_vec(new_file_ids)
};
Ok(new_file_ids)
}