1409 lines · 47.6 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. #![expect(missing_docs)] use std::io;use std::io::Write;use std::iter::zip;use std::pin::Pin; use bstr::BStr;use bstr::BString;use bstr::ByteSlice as _;use bstr::ByteVec as _;use futures::Stream;use futures::StreamExt as _;use futures::future::try_join_all;use futures::stream::BoxStream;use futures::try_join;use itertools::Itertools as _;use tokio::io::AsyncRead;use tokio::io::AsyncReadExt as _; use crate::backend::BackendError;use crate::backend::BackendResult;use crate::backend::CommitId;use crate::backend::CopyId;use crate::backend::FileId;use crate::backend::SymlinkId;use crate::backend::TreeId;use crate::backend::TreeValue;use crate::conflict_labels::ConflictLabels;use crate::copies::CopiesTreeDiffEntry;use crate::copies::CopiesTreeDiffEntryPath;use crate::diff::ContentDiff;use crate::diff::DiffHunk;use crate::diff::DiffHunkKind;use crate::files;use crate::files::MergeResult;use crate::merge::Diff;use crate::merge::Merge;use crate::merge::MergedTreeValue;use crate::merge::SameChange;use crate::repo_path::RepoPath;use crate::store::Store;use crate::tree_merge::MergeOptions; /// 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_ENDING_EOL_COMMENT: &str = "(no 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" ")?;                    file.write_all(line)?;                }            }            DiffHunkKind::Different => {                for line in hunk.contents[0].lines_with_terminator() {                    file.write_all(b"-")?;                    file.write_all(line)?;                }                for line in hunk.contents[1].lines_with_terminator() {                    file.write_all(b"+")?;                    file.write_all(line)?;                }            }        }    }    Ok(())} async fn get_file_contents(    store: &Store,    path: &RepoPath,    term: Option<&FileId>,) -> BackendResult<BString> {    match term {        Some(id) => {            let mut reader = store.read_file(path, id).await?;            let mut content = vec![];            reader                .read_to_end(&mut content)                .await                .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>> {    merge        .try_map_async(|term| get_file_contents(store, path, term.as_ref()))        .await} /// 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(MaterializedFileConflictValue),    OtherConflict {        id: MergedTreeValue,        labels: ConflictLabels,    },    GitSubmodule(CommitId),    Tree(TreeId),} impl MaterializedTreeValue {    pub fn is_absent(&self) -> bool {        matches!(self, Self::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 copy_id: CopyId,    pub reader: Pin<Box<dyn AsyncRead + Send>>,} impl MaterializedFileValue {    /// Reads file content until EOF. The provided `path` is used only for error    /// reporting purpose.    pub async fn read_all(&mut self, path: &RepoPath) -> BackendResult<Vec<u8>> {        let mut buf = Vec::new();        self.reader            .read_to_end(&mut buf)            .await            .map_err(|err| BackendError::ReadFile {                path: path.to_owned(),                id: self.id.clone(),                source: err.into(),            })?;        Ok(buf)    }} /// Conflicted [`TreeValue::File`]s with file contents.pub struct MaterializedFileConflictValue {    /// File ids which preserve the shape of the tree conflict, to be used with    /// [`Merge::update_from_simplified()`].    pub unsimplified_ids: Merge<Option<FileId>>,    /// Simplified file ids, in which redundant id pairs are dropped.    pub ids: Merge<Option<FileId>>,    /// Simplified conflict labels, matching `ids`.    pub labels: ConflictLabels,    /// File contents corresponding to the simplified `ids`.    // TODO: or Vec<(FileId, Box<dyn Read>)> so that caller can stop reading    // when null bytes found?    pub contents: Merge<BString>,    /// Merged executable bit. `None` if there are changes in both executable    /// bit and file absence.    pub executable: Option<bool>,    /// Merged copy id. `None` if no single value could be determined.    pub copy_id: Option<CopyId>,} /// 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,    conflict_labels: &ConflictLabels,) -> BackendResult<MaterializedTreeValue> {    match materialize_tree_value_no_access_denied(store, path, value, conflict_labels).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,    conflict_labels: &ConflictLabels,) -> BackendResult<MaterializedTreeValue> {    match value.into_resolved() {        Ok(None) => Ok(MaterializedTreeValue::Absent),        Ok(Some(TreeValue::File {            id,            executable,            copy_id,        })) => {            let reader = store.read_file(path, &id).await?;            Ok(MaterializedTreeValue::File(MaterializedFileValue {                id,                executable,                copy_id,                reader,            }))        }        Ok(Some(TreeValue::Symlink(id))) => {            let target = store.read_symlink(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)),        Err(conflict) => {            match try_materialize_file_conflict_value(store, path, &conflict, conflict_labels)                .await?            {                Some(file) => Ok(MaterializedTreeValue::FileConflict(file)),                None => Ok(MaterializedTreeValue::OtherConflict {                    id: conflict,                    labels: conflict_labels.clone(),                }),            }        }    }} /// Suppose `conflict` contains only files or absent entries, reads the file/// contents.pub async fn try_materialize_file_conflict_value(    store: &Store,    path: &RepoPath,    conflict: &MergedTreeValue,    conflict_labels: &ConflictLabels,) -> BackendResult<Option<MaterializedFileConflictValue>> {    let (Some(unsimplified_ids), Some(executable_bits)) =        (conflict.to_file_merge(), conflict.to_executable_merge())    else {        return Ok(None);    };    let (labels, ids) = conflict_labels.simplify_with(&unsimplified_ids);    let contents = extract_as_single_hunk(&ids, store, path).await?;    let executable = resolve_file_executable(&executable_bits);    Ok(Some(MaterializedFileConflictValue {        unsimplified_ids,        ids,        labels,        contents,        executable,        copy_id: Some(CopyId::placeholder()),    }))} /// Resolves conflicts in file executable bit, returns the original state if the/// file is deleted and executable bit is unchanged.pub fn resolve_file_executable(merge: &Merge<Option<bool>>) -> Option<bool> {    let resolved = merge.resolve_trivial(SameChange::Accept).copied()?;    if resolved.is_some() {        resolved    } else {        // If the merge is resolved to None (absent), there should be the same        // number of Some(true) and Some(false). Pick the old state if        // unambiguous, so the new file inherits the original executable bit.        merge.removes().flatten().copied().all_equal_value().ok()    }} /// Describes what style should be used when materializing conflicts.#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Deserialize)]#[serde(rename_all = "kebab-case")]pub enum ConflictMarkerStyle {    /// Style which shows a snapshot and a series of diffs to apply.    Diff,    /// Similar to "diff", but always picks the first side as the snapshot. May    /// become the default in a future version.    DiffExperimental,    /// Style which shows a snapshot for each base and side.    Snapshot,    /// Style which replicates Git's "diff3" style to support external tools.    Git,} impl ConflictMarkerStyle {    /// Returns true if this style allows `%%%%%%%` conflict markers.    pub fn allows_diff(&self) -> bool {        matches!(self, Self::Diff | Self::DiffExperimental)    }} /// Options for conflict materialization.#[derive(Clone, Debug)]pub struct ConflictMaterializeOptions {    pub marker_style: ConflictMarkerStyle,    pub marker_len: Option<usize>,    pub merge: MergeOptions,} /// 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'%',    Note = 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::Note),            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() {        write!(output, "{conflict_marker}")    } else {        write!(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)} fn detect_eol(single_hunk: &Merge<impl AsRef<[u8]>>) -> &'static BStr {    let use_crlf = single_hunk        .iter()        .filter_map(|content| {            let content = content.as_ref();            let newline_index = content.find_byte(b'\n')?;            Some(newline_index > 0 && content[newline_index - 1] == b'\r')        })        .all_equal_value()        .unwrap_or(false);    if use_crlf {        b"\r\n".into()    } else {        b"\n".into()    }} pub fn materialize_merge_result<T: AsRef<[u8]>>(    single_hunk: &Merge<T>,    labels: &ConflictLabels,    output: &mut dyn Write,    options: &ConflictMaterializeOptions,) -> io::Result<()> {    let merge_result = files::merge_hunks(single_hunk, &options.merge);    match merge_result {        MergeResult::Resolved(content) => output.write_all(&content),        MergeResult::Conflict(hunks) => {            let marker_len = options                .marker_len                .unwrap_or_else(|| choose_materialized_conflict_marker_len(single_hunk));            materialize_conflict_hunks(                hunks,                options.marker_style,                marker_len,                labels,                output,                detect_eol(single_hunk),            )        }    }} pub fn materialize_merge_result_to_bytes<T: AsRef<[u8]>>(    single_hunk: &Merge<T>,    labels: &ConflictLabels,    options: &ConflictMaterializeOptions,) -> BString {    let merge_result = files::merge_hunks(single_hunk, &options.merge);    match merge_result {        MergeResult::Resolved(content) => content,        MergeResult::Conflict(hunks) => {            let marker_len = options                .marker_len                .unwrap_or_else(|| choose_materialized_conflict_marker_len(single_hunk));            let mut output = Vec::new();            materialize_conflict_hunks(                hunks,                options.marker_style,                marker_len,                labels,                &mut output,                detect_eol(single_hunk),            )            .expect("writing to an in-memory buffer should never fail");            output.into()        }    }}

Lines 1–500 of 1409.