1018 lines · 34.7 KB · Rust
RawDownload
// 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.