use std::borrow::Cow;use std::path::Path;use std::sync::Arc; use futures::{StreamExt, TryFutureExt, TryStreamExt};use itertools::Itertools;use jj_lib::backend::{BackendError, FileId, MergedTreeId, TreeValue};use jj_lib::conflicts::{materialize_tree_value, MaterializedTreeValue};use jj_lib::diff::{find_line_ranges, Diff, DiffHunk};use jj_lib::files::{self, ContentHunk, MergeResult};use jj_lib::matchers::Matcher;use jj_lib::merge::Merge;use jj_lib::merged_tree::{MergedTree, MergedTreeBuilder};use jj_lib::object_id::ObjectId;use jj_lib::repo_path::{RepoPath, RepoPathBuf};use jj_lib::store::Store;use pollster::FutureExt;use thiserror::Error; #[derive(Debug, Error)]pub enum BuiltinToolError { #[error("Failed to record changes")] Record(#[from] scm_record::RecordError), #[error(transparent)] ReadFileBackend(BackendError), #[error("Failed to read file {path:?} with ID {id}", id = id.hex())] ReadFileIo { path: RepoPathBuf, id: FileId, source: std::io::Error, }, #[error(transparent)] ReadSymlink(BackendError), #[error("Failed to decode UTF-8 text for item {item} (this should not happen)")] DecodeUtf8 { source: std::str::Utf8Error, item: &'static str, }, #[error("Rendering {item} {id} is unimplemented for the builtin difftool/mergetool")] Unimplemented { item: &'static str, id: String }, #[error("Backend error")] BackendError(#[from] jj_lib::backend::BackendError),} #[derive(Clone, Debug)]enum FileContents { Absent, Text { contents: String, hash: Option<String>, num_bytes: u64, }, Binary { hash: Option<String>, num_bytes: u64, },} /// Information about a file that was read from disk. Note that the file may not/// have existed, in which case its contents will be marked as absent.#[derive(Clone, Debug)]pub struct FileInfo { file_mode: scm_record::FileMode, contents: FileContents,} /// File modes according to the Git file mode conventions. used for display/// purposes and equality comparison.////// TODO: let `scm-record` accept strings instead of numbers for file modes? Or/// figure out some other way to represent file mode changes in a jj-compatible/// manner?mod mode { pub const NORMAL: usize = 0o100644; pub const EXECUTABLE: usize = 0o100755; pub const SYMLINK: usize = 0o120000;} fn describe_binary(hash: Option<&str>, num_bytes: u64) -> String { match hash { Some(hash) => { format!("{hash} ({num_bytes}B)") } None => format!("({num_bytes}B)"), }} fn buf_to_file_contents(hash: Option<String>, buf: Vec<u8>) -> FileContents { let num_bytes: u64 = buf.len().try_into().unwrap(); let text = if buf.contains(&0) { None } else { String::from_utf8(buf).ok() }; match text { Some(text) => FileContents::Text { contents: text, hash, num_bytes, }, None => FileContents::Binary { hash, num_bytes }, }} fn read_file_contents( store: &Store, tree: &MergedTree, path: &RepoPath,) -> Result<FileInfo, BuiltinToolError> { let value = tree.path_value(path); let materialized_value = materialize_tree_value(store, path, value) .map_err(BuiltinToolError::BackendError) .block_on()?; match materialized_value { MaterializedTreeValue::Absent => Ok(FileInfo { file_mode: scm_record::FileMode::absent(), contents: FileContents::Absent, }), MaterializedTreeValue::File { id, executable, mut reader, } => { let mut buf = Vec::new(); reader .read_to_end(&mut buf) .map_err(|err| BuiltinToolError::ReadFileIo { path: path.to_owned(), id: id.clone(), source: err, })?; let file_mode = if executable { scm_record::FileMode(mode::EXECUTABLE) } else { scm_record::FileMode(mode::NORMAL) }; let contents = buf_to_file_contents(Some(id.hex()), buf); Ok(FileInfo { file_mode, contents, }) } MaterializedTreeValue::Symlink { id, target } => { let file_mode = scm_record::FileMode(mode::SYMLINK); let num_bytes = target.len().try_into().unwrap(); Ok(FileInfo { file_mode, contents: FileContents::Text { contents: target, hash: Some(id.hex()), num_bytes, }, }) } MaterializedTreeValue::Tree(tree_id) => { unreachable!("list of changed files included a tree: {tree_id:?}"); } MaterializedTreeValue::GitSubmodule(id) => Err(BuiltinToolError::Unimplemented { item: "git submodule", id: id.hex(), }), MaterializedTreeValue::Conflict { id: _, contents } => { // TODO: Render the ID somehow? let contents = buf_to_file_contents(None, contents); Ok(FileInfo { file_mode: scm_record::FileMode(mode::NORMAL), contents, }) } }} fn make_section_changed_lines( contents: &str, change_type: scm_record::ChangeType,) -> Vec<scm_record::SectionChangedLine<'static>> { contents .split_inclusive('\n') .map(|line| scm_record::SectionChangedLine { is_checked: false, change_type, line: Cow::Owned(line.to_owned()), }) .collect()} fn make_diff_sections( left_contents: &str, right_contents: &str,) -> Result<Vec<scm_record::Section<'static>>, BuiltinToolError> { let diff = Diff::for_tokenizer( &[left_contents.as_bytes(), right_contents.as_bytes()], &find_line_ranges, ); let mut sections = Vec::new(); for hunk in diff.hunks() { match hunk { DiffHunk::Matching(text) => { let text = std::str::from_utf8(text).map_err(|err| BuiltinToolError::DecodeUtf8 { source: err, item: "matching text in diff hunk", })?; sections.push(scm_record::Section::Unchanged { lines: text .split_inclusive('\n') .map(|line| Cow::Owned(line.to_owned())) .collect(), }) } DiffHunk::Different(sides) => { assert_eq!(sides.len(), 2, "only two inputs were provided to the diff"); let left_side = std::str::from_utf8(sides[0]).map_err(|err| BuiltinToolError::DecodeUtf8 { source: err, item: "left side of diff hunk", })?; let right_side = std::str::from_utf8(sides[1]).map_err(|err| BuiltinToolError::DecodeUtf8 { source: err, item: "right side of diff hunk", })?; sections.push(scm_record::Section::Changed { lines: [ make_section_changed_lines(left_side, scm_record::ChangeType::Removed), make_section_changed_lines(right_side, scm_record::ChangeType::Added), ] .concat(), }) } } } Ok(sections)} pub fn make_diff_files( store: &Arc<Store>, left_tree: &MergedTree, right_tree: &MergedTree, changed_files: &[RepoPathBuf],) -> Result<Vec<scm_record::File<'static>>, BuiltinToolError> { let mut files = Vec::new(); for changed_path in changed_files { let FileInfo { file_mode: left_file_mode, contents: left_contents, } = read_file_contents(store, left_tree, changed_path)?; let FileInfo { file_mode: right_file_mode, contents: right_contents, } = read_file_contents(store, right_tree, changed_path)?; let mut sections = Vec::new(); if left_file_mode != right_file_mode && left_file_mode != scm_record::FileMode::absent() && right_file_mode != scm_record::FileMode::absent() { sections.push(scm_record::Section::FileMode { is_checked: false, before: left_file_mode, after: right_file_mode, }); } match (left_contents, right_contents) { (FileContents::Absent, FileContents::Absent) => {} ( FileContents::Absent, FileContents::Text { contents, hash: _, num_bytes: _, }, ) => sections.push(scm_record::Section::Changed { lines: make_section_changed_lines(&contents, scm_record::ChangeType::Added), }), (FileContents::Absent, FileContents::Binary { hash, num_bytes }) => { sections.push(scm_record::Section::Binary { is_checked: false, old_description: None, new_description: Some(Cow::Owned(describe_binary(hash.as_deref(), num_bytes))), }) } ( FileContents::Text { contents, hash: _, num_bytes: _, }, FileContents::Absent, ) => sections.push(scm_record::Section::Changed { lines: make_section_changed_lines(&contents, scm_record::ChangeType::Removed), }), ( FileContents::Text { contents: old_contents, hash: _, num_bytes: _, }, FileContents::Text { contents: new_contents, hash: _, num_bytes: _, }, ) => { sections.extend(make_diff_sections(&old_contents, &new_contents)?); } ( FileContents::Text { contents: _, hash: old_hash, num_bytes: old_num_bytes, } | FileContents::Binary { hash: old_hash, num_bytes: old_num_bytes, }, FileContents::Text { contents: _, hash: new_hash, num_bytes: new_num_bytes, } | FileContents::Binary { hash: new_hash, num_bytes: new_num_bytes, }, ) => sections.push(scm_record::Section::Binary { is_checked: false, old_description: Some(Cow::Owned(describe_binary( old_hash.as_deref(), old_num_bytes, ))), new_description: Some(Cow::Owned(describe_binary( new_hash.as_deref(), new_num_bytes, ))), }), (FileContents::Binary { hash, num_bytes }, FileContents::Absent) => { sections.push(scm_record::Section::Binary { is_checked: false, old_description: Some(Cow::Owned(describe_binary(hash.as_deref(), num_bytes))), new_description: None, }) } } files.push(scm_record::File { old_path: None, path: Cow::Owned(changed_path.to_fs_path(Path::new(""))), file_mode: Some(left_file_mode), sections, }); } Ok(files)} pub fn apply_diff_builtin( store: Arc<Store>, left_tree: &MergedTree, right_tree: &MergedTree, changed_files: Vec<RepoPathBuf>, files: &[scm_record::File],) -> Result<MergedTreeId, BackendError> { let mut tree_builder = MergedTreeBuilder::new(left_tree.id().clone()); assert_eq!( changed_files.len(), files.len(), "result had a different number of files" ); for (path, file) in changed_files.into_iter().zip(files) { let (selected, _unselected) = file.get_selected_contents(); match selected { scm_record::SelectedContents::Absent => { tree_builder.set_or_remove(path, Merge::absent()); } scm_record::SelectedContents::Unchanged => { // Do nothing. } scm_record::SelectedContents::Binary { old_description: _, new_description: _, } => { let value = right_tree.path_value(&path); tree_builder.set_or_remove(path, value); } scm_record::SelectedContents::Present { contents } => { let file_id = store.write_file(&path, &mut contents.as_bytes())?; tree_builder.set_or_remove( path, Merge::normal(TreeValue::File { id: file_id, executable: file.get_file_mode() == Some(scm_record::FileMode(mode::EXECUTABLE)), }), ) } } } let tree_id = tree_builder.write_tree(left_tree.store())?; Ok(tree_id)} pub fn edit_diff_builtin( left_tree: &MergedTree, right_tree: &MergedTree, matcher: &dyn Matcher,) -> Result<MergedTreeId, BuiltinToolError> { let store = left_tree.store().clone(); let changed_files: Vec<_> = left_tree .diff_stream(right_tree, matcher) .map(|(path, diff)| diff.map(|_| path)) .try_collect() .block_on()?; let files = make_diff_files(&store, left_tree, right_tree, &changed_files)?; let mut input = scm_record::helpers::CrosstermInput; let recorder = scm_record::Recorder::new( scm_record::RecordState { is_read_only: false, files, commits: Default::default(), }, &mut input, ); let result = recorder.run().map_err(BuiltinToolError::Record)?; let tree_id = apply_diff_builtin(store, left_tree, right_tree, changed_files, &result.files) .map_err(BuiltinToolError::BackendError)?; Ok(tree_id)} fn make_merge_sections( merge_result: MergeResult,) -> Result<Vec<scm_record::Section<'static>>, BuiltinToolError> { let mut sections = Vec::new(); match merge_result { MergeResult::Resolved(ContentHunk(buf)) => { let contents = buf_to_file_contents(None, buf); let section = match contents { FileContents::Absent => None, FileContents::Text { contents, hash: _, num_bytes: _, } => Some(scm_record::Section::Unchanged { lines: contents .split_inclusive('\n') .map(|line| Cow::Owned(line.to_owned())) .collect(), }), FileContents::Binary { hash, num_bytes } => Some(scm_record::Section::Binary { is_checked: false, old_description: None, new_description: Some(Cow::Owned(describe_binary(hash.as_deref(), num_bytes))), }), }; if let Some(section) = section { sections.push(section); } } MergeResult::Conflict(hunks) => { for hunk in hunks { let section = match hunk.into_resolved() { Ok(ContentHunk(contents)) => { let contents = std::str::from_utf8(&contents).map_err(|err| { BuiltinToolError::DecodeUtf8 { source: err, item: "unchanged hunk", } })?; scm_record::Section::Unchanged { lines: contents .split_inclusive('\n') .map(|line| Cow::Owned(line.to_owned())) .collect(), } } Err(merge) => { let lines: Vec<scm_record::SectionChangedLine> = merge .iter() .zip( [ scm_record::ChangeType::Added, scm_record::ChangeType::Removed, ] .into_iter() .cycle(), ) .map(|(contents, change_type)| -> Result<_, BuiltinToolError> { let ContentHunk(contents) = contents; let contents = std::str::from_utf8(contents).map_err(|err| { BuiltinToolError::DecodeUtf8 {Lines 1–500 of 809. Load more
use std::borrow::Cow; use std::path::Path; use std::sync::Arc; use futures::{StreamExt, TryFutureExt, TryStreamExt}; use itertools::Itertools; use jj_lib::backend::{BackendError, FileId, MergedTreeId, TreeValue}; use jj_lib::conflicts::{materialize_tree_value, MaterializedTreeValue}; use jj_lib::diff::{find_line_ranges, Diff, DiffHunk}; use jj_lib::files::{self, ContentHunk, MergeResult}; use jj_lib::matchers::Matcher; use jj_lib::merge::Merge; use jj_lib::merged_tree::{MergedTree, MergedTreeBuilder}; use jj_lib::object_id::ObjectId; use jj_lib::repo_path::{RepoPath, RepoPathBuf}; use jj_lib::store::Store; use pollster::FutureExt; use thiserror::Error; #[derive(Debug, Error)] pub enum BuiltinToolError { #[error("Failed to record changes")] Record(#[from] scm_record::RecordError), #[error(transparent)] ReadFileBackend(BackendError), #[error("Failed to read file {path:?} with ID {id}", id = id.hex())] ReadFileIo { path: RepoPathBuf, id: FileId, source: std::io::Error, }, #[error(transparent)] ReadSymlink(BackendError), #[error("Failed to decode UTF-8 text for item {item} (this should not happen)")] DecodeUtf8 { source: std::str::Utf8Error, item: &'static str, }, #[error("Rendering {item} {id} is unimplemented for the builtin difftool/mergetool")] Unimplemented { item: &'static str, id: String }, #[error("Backend error")] BackendError(#[from] jj_lib::backend::BackendError), } #[derive(Clone, Debug)] enum FileContents { Absent, Text { contents: String, hash: Option<String>, num_bytes: u64, }, Binary { hash: Option<String>, num_bytes: u64, }, } /// Information about a file that was read from disk. Note that the file may not /// have existed, in which case its contents will be marked as absent. #[derive(Clone, Debug)] pub struct FileInfo { file_mode: scm_record::FileMode, contents: FileContents, } /// File modes according to the Git file mode conventions. used for display /// purposes and equality comparison. /// /// TODO: let `scm-record` accept strings instead of numbers for file modes? Or /// figure out some other way to represent file mode changes in a jj-compatible /// manner? mod mode { pub const NORMAL: usize = 0o100644; pub const EXECUTABLE: usize = 0o100755; pub const SYMLINK: usize = 0o120000; } fn describe_binary(hash: Option<&str>, num_bytes: u64) -> String { match hash { Some(hash) => { format!("{hash} ({num_bytes}B)") } None => format!("({num_bytes}B)"), } } fn buf_to_file_contents(hash: Option<String>, buf: Vec<u8>) -> FileContents { let num_bytes: u64 = buf.len().try_into().unwrap(); let text = if buf.contains(&0) { None } else { String::from_utf8(buf).ok() }; match text { Some(text) => FileContents::Text { contents: text, hash, num_bytes, }, None => FileContents::Binary { hash, num_bytes }, } } fn read_file_contents( store: &Store, tree: &MergedTree, path: &RepoPath, ) -> Result<FileInfo, BuiltinToolError> { let value = tree.path_value(path); let materialized_value = materialize_tree_value(store, path, value) .map_err(BuiltinToolError::BackendError) .block_on()?; match materialized_value { MaterializedTreeValue::Absent => Ok(FileInfo { file_mode: scm_record::FileMode::absent(), contents: FileContents::Absent, }), MaterializedTreeValue::File { id, executable, mut reader, } => { let mut buf = Vec::new(); reader .read_to_end(&mut buf) .map_err(|err| BuiltinToolError::ReadFileIo { path: path.to_owned(), id: id.clone(), source: err, })?; let file_mode = if executable { scm_record::FileMode(mode::EXECUTABLE) } else { scm_record::FileMode(mode::NORMAL) }; let contents = buf_to_file_contents(Some(id.hex()), buf); Ok(FileInfo { file_mode, contents, }) } MaterializedTreeValue::Symlink { id, target } => { let file_mode = scm_record::FileMode(mode::SYMLINK); let num_bytes = target.len().try_into().unwrap(); Ok(FileInfo { file_mode, contents: FileContents::Text { contents: target, hash: Some(id.hex()), num_bytes, }, }) } MaterializedTreeValue::Tree(tree_id) => { unreachable!("list of changed files included a tree: {tree_id:?}"); } MaterializedTreeValue::GitSubmodule(id) => Err(BuiltinToolError::Unimplemented { item: "git submodule", id: id.hex(), }), MaterializedTreeValue::Conflict { id: _, contents } => { // TODO: Render the ID somehow? let contents = buf_to_file_contents(None, contents); Ok(FileInfo { file_mode: scm_record::FileMode(mode::NORMAL), contents, }) } } } fn make_section_changed_lines( contents: &str, change_type: scm_record::ChangeType, ) -> Vec<scm_record::SectionChangedLine<'static>> { contents .split_inclusive('\n') .map(|line| scm_record::SectionChangedLine { is_checked: false, change_type, line: Cow::Owned(line.to_owned()), }) .collect() } fn make_diff_sections( left_contents: &str, right_contents: &str, ) -> Result<Vec<scm_record::Section<'static>>, BuiltinToolError> { let diff = Diff::for_tokenizer( &[left_contents.as_bytes(), right_contents.as_bytes()], &find_line_ranges, ); let mut sections = Vec::new(); for hunk in diff.hunks() { match hunk { DiffHunk::Matching(text) => { let text = std::str::from_utf8(text).map_err(|err| BuiltinToolError::DecodeUtf8 { source: err, item: "matching text in diff hunk", })?; sections.push(scm_record::Section::Unchanged { lines: text .split_inclusive('\n') .map(|line| Cow::Owned(line.to_owned())) .collect(), }) } DiffHunk::Different(sides) => { assert_eq!(sides.len(), 2, "only two inputs were provided to the diff"); let left_side = std::str::from_utf8(sides[0]).map_err(|err| BuiltinToolError::DecodeUtf8 { source: err, item: "left side of diff hunk", })?; let right_side = std::str::from_utf8(sides[1]).map_err(|err| BuiltinToolError::DecodeUtf8 { source: err, item: "right side of diff hunk", })?; sections.push(scm_record::Section::Changed { lines: [ make_section_changed_lines(left_side, scm_record::ChangeType::Removed), make_section_changed_lines(right_side, scm_record::ChangeType::Added), ] .concat(), }) } } } Ok(sections) } pub fn make_diff_files( store: &Arc<Store>, left_tree: &MergedTree, right_tree: &MergedTree, changed_files: &[RepoPathBuf], ) -> Result<Vec<scm_record::File<'static>>, BuiltinToolError> { let mut files = Vec::new(); for changed_path in changed_files { let FileInfo { file_mode: left_file_mode, contents: left_contents, } = read_file_contents(store, left_tree, changed_path)?; let FileInfo { file_mode: right_file_mode, contents: right_contents, } = read_file_contents(store, right_tree, changed_path)?; let mut sections = Vec::new(); if left_file_mode != right_file_mode && left_file_mode != scm_record::FileMode::absent() && right_file_mode != scm_record::FileMode::absent() { sections.push(scm_record::Section::FileMode { is_checked: false, before: left_file_mode, after: right_file_mode, }); } match (left_contents, right_contents) { (FileContents::Absent, FileContents::Absent) => {} ( FileContents::Absent, FileContents::Text { contents, hash: _, num_bytes: _, }, ) => sections.push(scm_record::Section::Changed { lines: make_section_changed_lines(&contents, scm_record::ChangeType::Added), }), (FileContents::Absent, FileContents::Binary { hash, num_bytes }) => { sections.push(scm_record::Section::Binary { is_checked: false, old_description: None, new_description: Some(Cow::Owned(describe_binary(hash.as_deref(), num_bytes))), }) } ( FileContents::Text { contents, hash: _, num_bytes: _, }, FileContents::Absent, ) => sections.push(scm_record::Section::Changed { lines: make_section_changed_lines(&contents, scm_record::ChangeType::Removed), }), ( FileContents::Text { contents: old_contents, hash: _, num_bytes: _, }, FileContents::Text { contents: new_contents, hash: _, num_bytes: _, }, ) => { sections.extend(make_diff_sections(&old_contents, &new_contents)?); } ( FileContents::Text { contents: _, hash: old_hash, num_bytes: old_num_bytes, } | FileContents::Binary { hash: old_hash, num_bytes: old_num_bytes, }, FileContents::Text { contents: _, hash: new_hash, num_bytes: new_num_bytes, } | FileContents::Binary { hash: new_hash, num_bytes: new_num_bytes, }, ) => sections.push(scm_record::Section::Binary { is_checked: false, old_description: Some(Cow::Owned(describe_binary( old_hash.as_deref(), old_num_bytes, ))), new_description: Some(Cow::Owned(describe_binary( new_hash.as_deref(), new_num_bytes, ))), }), (FileContents::Binary { hash, num_bytes }, FileContents::Absent) => { sections.push(scm_record::Section::Binary { is_checked: false, old_description: Some(Cow::Owned(describe_binary(hash.as_deref(), num_bytes))), new_description: None, }) } } files.push(scm_record::File { old_path: None, path: Cow::Owned(changed_path.to_fs_path(Path::new(""))), file_mode: Some(left_file_mode), sections, }); } Ok(files) } pub fn apply_diff_builtin( store: Arc<Store>, left_tree: &MergedTree, right_tree: &MergedTree, changed_files: Vec<RepoPathBuf>, files: &[scm_record::File], ) -> Result<MergedTreeId, BackendError> { let mut tree_builder = MergedTreeBuilder::new(left_tree.id().clone()); assert_eq!( changed_files.len(), files.len(), "result had a different number of files" ); for (path, file) in changed_files.into_iter().zip(files) { let (selected, _unselected) = file.get_selected_contents(); match selected { scm_record::SelectedContents::Absent => { tree_builder.set_or_remove(path, Merge::absent()); } scm_record::SelectedContents::Unchanged => { // Do nothing. } scm_record::SelectedContents::Binary { old_description: _, new_description: _, } => { let value = right_tree.path_value(&path); tree_builder.set_or_remove(path, value); } scm_record::SelectedContents::Present { contents } => { let file_id = store.write_file(&path, &mut contents.as_bytes())?; tree_builder.set_or_remove( path, Merge::normal(TreeValue::File { id: file_id, executable: file.get_file_mode() == Some(scm_record::FileMode(mode::EXECUTABLE)), }), ) } } } let tree_id = tree_builder.write_tree(left_tree.store())?; Ok(tree_id) } pub fn edit_diff_builtin( left_tree: &MergedTree, right_tree: &MergedTree, matcher: &dyn Matcher, ) -> Result<MergedTreeId, BuiltinToolError> { let store = left_tree.store().clone(); let changed_files: Vec<_> = left_tree .diff_stream(right_tree, matcher) .map(|(path, diff)| diff.map(|_| path)) .try_collect() .block_on()?; let files = make_diff_files(&store, left_tree, right_tree, &changed_files)?; let mut input = scm_record::helpers::CrosstermInput; let recorder = scm_record::Recorder::new( scm_record::RecordState { is_read_only: false, files, commits: Default::default(), }, &mut input, ); let result = recorder.run().map_err(BuiltinToolError::Record)?; let tree_id = apply_diff_builtin(store, left_tree, right_tree, changed_files, &result.files) .map_err(BuiltinToolError::BackendError)?; Ok(tree_id) } fn make_merge_sections( merge_result: MergeResult, ) -> Result<Vec<scm_record::Section<'static>>, BuiltinToolError> { let mut sections = Vec::new(); match merge_result { MergeResult::Resolved(ContentHunk(buf)) => { let contents = buf_to_file_contents(None, buf); let section = match contents { FileContents::Absent => None, FileContents::Text { contents, hash: _, num_bytes: _, } => Some(scm_record::Section::Unchanged { lines: contents .split_inclusive('\n') .map(|line| Cow::Owned(line.to_owned())) .collect(), }), FileContents::Binary { hash, num_bytes } => Some(scm_record::Section::Binary { is_checked: false, old_description: None, new_description: Some(Cow::Owned(describe_binary(hash.as_deref(), num_bytes))), }), }; if let Some(section) = section { sections.push(section); } } MergeResult::Conflict(hunks) => { for hunk in hunks { let section = match hunk.into_resolved() { Ok(ContentHunk(contents)) => { let contents = std::str::from_utf8(&contents).map_err(|err| { BuiltinToolError::DecodeUtf8 { source: err, item: "unchanged hunk", } })?; scm_record::Section::Unchanged { lines: contents .split_inclusive('\n') .map(|line| Cow::Owned(line.to_owned())) .collect(), } } Err(merge) => { let lines: Vec<scm_record::SectionChangedLine> = merge .iter() .zip( [ scm_record::ChangeType::Added, scm_record::ChangeType::Removed, ] .into_iter() .cycle(), ) .map(|(contents, change_type)| -> Result<_, BuiltinToolError> { let ContentHunk(contents) = contents; let contents = std::str::from_utf8(contents).map_err(|err| { BuiltinToolError::DecodeUtf8 {