use std::borrow::Cow;use std::path::Path;use std::sync::Arc; use futures::StreamExt as _;use futures::stream::BoxStream;use itertools::Itertools as _;use jj_lib::backend::BackendResult;use jj_lib::backend::CopyId;use jj_lib::backend::MergedTreeId;use jj_lib::backend::TreeValue;use jj_lib::conflicts;use jj_lib::conflicts::ConflictMarkerStyle;use jj_lib::conflicts::ConflictMaterializeOptions;use jj_lib::conflicts::MIN_CONFLICT_MARKER_LEN;use jj_lib::conflicts::MaterializedTreeValue;use jj_lib::conflicts::materialize_merge_result_to_bytes;use jj_lib::conflicts::materialized_diff_stream;use jj_lib::copies::CopiesTreeDiffEntry;use jj_lib::copies::CopyRecords;use jj_lib::diff::ContentDiff;use jj_lib::diff::DiffHunkKind;use jj_lib::files;use jj_lib::files::MergeResult;use jj_lib::matchers::Matcher;use jj_lib::merge::Merge;use jj_lib::merge::MergedTreeValue;use jj_lib::merged_tree::MergedTree;use jj_lib::merged_tree::MergedTreeBuilder;use jj_lib::object_id::ObjectId as _;use jj_lib::repo_path::RepoPath;use jj_lib::repo_path::RepoPathBuf;use jj_lib::store::Store;use jj_lib::tree_merge::MergeOptions;use pollster::FutureExt as _;use thiserror::Error; use super::MergeToolFile; #[derive(Debug, Error)]pub enum BuiltinToolError { #[error("Failed to record changes")] Record(#[from] scm_record::RecordError), #[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, },} impl FileContents { fn describe(&self) -> Option<String> { match self { Self::Absent => None, Self::Text { contents: _, hash, num_bytes, } | Self::Binary { hash, num_bytes } => match hash { Some(hash) => Some(format!("{hash} ({num_bytes}B)")), None => Some(format!("({num_bytes}B)")), }, } }} /// 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 ABSENT: scm_record::FileMode = scm_record::FileMode::Absent; pub const NORMAL: scm_record::FileMode = scm_record::FileMode::Unix(0o100644); pub const EXECUTABLE: scm_record::FileMode = scm_record::FileMode::Unix(0o100755); pub const SYMLINK: scm_record::FileMode = scm_record::FileMode::Unix(0o120000);} 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( materialized_value: MaterializedTreeValue, path: &RepoPath, materialize_options: &ConflictMaterializeOptions,) -> Result<FileInfo, BuiltinToolError> { match materialized_value { MaterializedTreeValue::Absent => Ok(FileInfo { file_mode: mode::ABSENT, contents: FileContents::Absent, }), MaterializedTreeValue::AccessDenied(err) => Ok(FileInfo { file_mode: mode::NORMAL, contents: FileContents::Text { contents: format!("Access denied: {err}"), hash: None, num_bytes: 0, }, }), MaterializedTreeValue::File(mut file) => { let buf = file.read_all(path).block_on()?; let file_mode = if file.executable { mode::EXECUTABLE } else { mode::NORMAL }; let contents = buf_to_file_contents(Some(file.id.hex()), buf); Ok(FileInfo { file_mode, contents, }) } MaterializedTreeValue::Symlink { id, target } => { let file_mode = 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::FileConflict(file) => { // Since scm_record doesn't support diffs of conflicts, file // conflicts are compared in materialized form. The UI would look // scary, but it can at least allow squashing resolved hunks. let buf = materialize_merge_result_to_bytes(&file.contents, materialize_options).into(); // TODO: Render the ID somehow? let contents = buf_to_file_contents(None, buf); Ok(FileInfo { file_mode: mode::NORMAL, contents, }) } MaterializedTreeValue::OtherConflict { id } => { // TODO: Non-file conflict shouldn't be rendered as a normal file // TODO: Render the ID somehow? let contents = buf_to_file_contents(None, id.describe().into_bytes()); Ok(FileInfo { file_mode: 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 = ContentDiff::by_line([left_contents.as_bytes(), right_contents.as_bytes()]); let mut sections = Vec::new(); for hunk in diff.hunks() { match hunk.kind { DiffHunkKind::Matching => { debug_assert!(hunk.contents.iter().all_equal()); let text = hunk.contents[0]; let text = 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(), }); } DiffHunkKind::Different => { let sides = &hunk.contents; assert_eq!(sides.len(), 2, "only two inputs were provided to the diff"); let left_side = str::from_utf8(sides[0]).map_err(|err| BuiltinToolError::DecodeUtf8 { source: err, item: "left side of diff hunk", })?; let right_side = 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)} async fn make_diff_files( store: &Arc<Store>, tree_diff: BoxStream<'_, CopiesTreeDiffEntry>, marker_style: ConflictMarkerStyle,) -> Result<(Vec<RepoPathBuf>, Vec<scm_record::File<'static>>), BuiltinToolError> { let materialize_options = ConflictMaterializeOptions { marker_style, marker_len: None, merge: store.merge_options().clone(), }; let mut diff_stream = materialized_diff_stream(store, tree_diff); let mut changed_files = Vec::new(); let mut files = Vec::new(); while let Some(entry) = diff_stream.next().await { let left_path = entry.path.source(); let right_path = entry.path.target(); let (left_value, right_value) = entry.values?; let left_info = read_file_contents(left_value, left_path, &materialize_options)?; let right_info = read_file_contents(right_value, right_path, &materialize_options)?; let mut sections = Vec::new(); if left_info.file_mode != right_info.file_mode { sections.push(scm_record::Section::FileMode { is_checked: false, mode: right_info.file_mode, }); } match (left_info.contents, right_info.contents) { (FileContents::Absent, FileContents::Absent) => {} // In this context, `Absent` means the file doesn't exist. If it only // exists on one side, we will render a mode change section above. // The next two patterns are to avoid also rendering an empty // changed lines section that clutters the UI. ( FileContents::Absent, FileContents::Text { contents: _, hash: _, num_bytes: 0, }, ) => {} ( FileContents::Text { contents: _, hash: _, num_bytes: 0, }, 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::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::Binary { hash: Some(left_hash), .. }, FileContents::Binary { hash: Some(right_hash), .. }, ) if left_hash == right_hash => { // Binary file contents have not changed. } (left, right @ FileContents::Binary { .. }) | (left @ FileContents::Binary { .. }, right) => { sections.push(scm_record::Section::Binary { is_checked: false, old_description: left.describe().map(Cow::Owned), new_description: right.describe().map(Cow::Owned), }); } } files.push(scm_record::File { old_path: None, // Path for displaying purposes, not for file access. path: Cow::Owned(right_path.to_fs_path_unchecked(Path::new(""))), file_mode: left_info.file_mode, sections, }); changed_files.push(entry.path.target); } Ok((changed_files, files))} fn apply_diff_builtin( store: &Arc<Store>, left_tree: &MergedTree, right_tree: &MergedTree, changed_files: Vec<RepoPathBuf>, files: &[scm_record::File],) -> BackendResult<MergedTreeId> { // Start with the right tree to match external tool behavior. // This ensures unmatched paths keep their values from the right tree. let mut tree_builder = MergedTreeBuilder::new(right_tree.id().clone()); // First, revert all changed files to their left versions for path in &changed_files { let left_value = left_tree.path_value(path)?; tree_builder.set_or_remove(path.clone(), left_value); } // Then apply only the selected changes apply_changes( &mut tree_builder, changed_files, files, |path| left_tree.path_value(path), |path| right_tree.path_value(path), |path, contents, executable, copy_id| { let old_value = left_tree.path_value(path)?; let new_value = if old_value.is_resolved() { let id = store.write_file(path, &mut &contents[..]).block_on()?; Merge::normal(TreeValue::File { id, executable, copy_id, }) } else if let Some(old_file_ids) = old_value.to_file_merge() { // TODO: should error out if conflicts couldn't be parsed? let new_file_ids = conflicts::update_from_content( &old_file_ids, store, path, contents, MIN_CONFLICT_MARKER_LEN, // TODO: use the materialization parameter ) .block_on()?; match new_file_ids.into_resolved() { Ok(id) => Merge::resolved(id.map(|id| TreeValue::File { id, executable, copy_id: CopyId::placeholder(), })), Err(file_ids) => old_value.with_new_file_ids(&file_ids), } } else { panic!("unexpected content change at {path:?}: {old_value:?}"); }; Ok(new_value) }, )?; tree_builder.write_tree(store)} fn apply_changes( tree_builder: &mut MergedTreeBuilder, changed_files: Vec<RepoPathBuf>, files: &[scm_record::File], select_left: impl Fn(&RepoPath) -> BackendResult<MergedTreeValue>, select_right: impl Fn(&RepoPath) -> BackendResult<MergedTreeValue>, write_file: impl Fn(&RepoPath, &[u8], bool, CopyId) -> BackendResult<MergedTreeValue>,) -> BackendResult<()> { assert_eq!( changed_files.len(), files.len(), "result had a different number of files" ); // TODO: Write files concurrently for (path, file) in changed_files.into_iter().zip(files) { let file_mode_change_selected = file .sections .iter() .find_map(|sec| match sec { scm_record::Section::FileMode { is_checked, .. } => Some(*is_checked), _ => None, }) .unwrap_or(false); let ( scm_record::SelectedChanges { contents, file_mode, }, _unselected, ) = file.get_selected_contents(); if file_mode == mode::ABSENT { // The file is not present in the selected changes. // Either a file mode change was selected to delete an existing file, so we // should remove it from the tree, if file_mode_change_selected { tree_builder.set_or_remove(path, Merge::absent()); } // or the file's creation has been split out of the change, in which case we // don't need to change the tree. // In either case, we're done with this file afterwards. continue; } let executable = file_mode == mode::EXECUTABLE; match contents { scm_record::SelectedContents::Unchanged => { if file_mode_change_selected { // File contents haven't changed, but file mode needs to be updated on the tree. let value = override_file_executable_bit(select_left(&path)?, executable); tree_builder.set_or_remove(path, value); } else { // Neither file mode, nor contents changed => Do nothing. } } scm_record::SelectedContents::Binary {Lines 1–500 of 2265. Load more
use std::borrow::Cow; use std::path::Path; use std::sync::Arc; use futures::StreamExt as _; use futures::stream::BoxStream; use itertools::Itertools as _; use jj_lib::backend::BackendResult; use jj_lib::backend::CopyId; use jj_lib::backend::MergedTreeId; use jj_lib::backend::TreeValue; use jj_lib::conflicts; use jj_lib::conflicts::ConflictMarkerStyle; use jj_lib::conflicts::ConflictMaterializeOptions; use jj_lib::conflicts::MIN_CONFLICT_MARKER_LEN; use jj_lib::conflicts::MaterializedTreeValue; use jj_lib::conflicts::materialize_merge_result_to_bytes; use jj_lib::conflicts::materialized_diff_stream; use jj_lib::copies::CopiesTreeDiffEntry; use jj_lib::copies::CopyRecords; use jj_lib::diff::ContentDiff; use jj_lib::diff::DiffHunkKind; use jj_lib::files; use jj_lib::files::MergeResult; use jj_lib::matchers::Matcher; use jj_lib::merge::Merge; use jj_lib::merge::MergedTreeValue; use jj_lib::merged_tree::MergedTree; use jj_lib::merged_tree::MergedTreeBuilder; use jj_lib::object_id::ObjectId as _; use jj_lib::repo_path::RepoPath; use jj_lib::repo_path::RepoPathBuf; use jj_lib::store::Store; use jj_lib::tree_merge::MergeOptions; use pollster::FutureExt as _; use thiserror::Error; use super::MergeToolFile; #[derive(Debug, Error)] pub enum BuiltinToolError { #[error("Failed to record changes")] Record(#[from] scm_record::RecordError), #[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, }, } impl FileContents { fn describe(&self) -> Option<String> { match self { Self::Absent => None, Self::Text { contents: _, hash, num_bytes, } | Self::Binary { hash, num_bytes } => match hash { Some(hash) => Some(format!("{hash} ({num_bytes}B)")), None => Some(format!("({num_bytes}B)")), }, } } } /// 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 ABSENT: scm_record::FileMode = scm_record::FileMode::Absent; pub const NORMAL: scm_record::FileMode = scm_record::FileMode::Unix(0o100644); pub const EXECUTABLE: scm_record::FileMode = scm_record::FileMode::Unix(0o100755); pub const SYMLINK: scm_record::FileMode = scm_record::FileMode::Unix(0o120000); } 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( materialized_value: MaterializedTreeValue, path: &RepoPath, materialize_options: &ConflictMaterializeOptions, ) -> Result<FileInfo, BuiltinToolError> { match materialized_value { MaterializedTreeValue::Absent => Ok(FileInfo { file_mode: mode::ABSENT, contents: FileContents::Absent, }), MaterializedTreeValue::AccessDenied(err) => Ok(FileInfo { file_mode: mode::NORMAL, contents: FileContents::Text { contents: format!("Access denied: {err}"), hash: None, num_bytes: 0, }, }), MaterializedTreeValue::File(mut file) => { let buf = file.read_all(path).block_on()?; let file_mode = if file.executable { mode::EXECUTABLE } else { mode::NORMAL }; let contents = buf_to_file_contents(Some(file.id.hex()), buf); Ok(FileInfo { file_mode, contents, }) } MaterializedTreeValue::Symlink { id, target } => { let file_mode = 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::FileConflict(file) => { // Since scm_record doesn't support diffs of conflicts, file // conflicts are compared in materialized form. The UI would look // scary, but it can at least allow squashing resolved hunks. let buf = materialize_merge_result_to_bytes(&file.contents, materialize_options).into(); // TODO: Render the ID somehow? let contents = buf_to_file_contents(None, buf); Ok(FileInfo { file_mode: mode::NORMAL, contents, }) } MaterializedTreeValue::OtherConflict { id } => { // TODO: Non-file conflict shouldn't be rendered as a normal file // TODO: Render the ID somehow? let contents = buf_to_file_contents(None, id.describe().into_bytes()); Ok(FileInfo { file_mode: 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 = ContentDiff::by_line([left_contents.as_bytes(), right_contents.as_bytes()]); let mut sections = Vec::new(); for hunk in diff.hunks() { match hunk.kind { DiffHunkKind::Matching => { debug_assert!(hunk.contents.iter().all_equal()); let text = hunk.contents[0]; let text = 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(), }); } DiffHunkKind::Different => { let sides = &hunk.contents; assert_eq!(sides.len(), 2, "only two inputs were provided to the diff"); let left_side = str::from_utf8(sides[0]).map_err(|err| BuiltinToolError::DecodeUtf8 { source: err, item: "left side of diff hunk", })?; let right_side = 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) } async fn make_diff_files( store: &Arc<Store>, tree_diff: BoxStream<'_, CopiesTreeDiffEntry>, marker_style: ConflictMarkerStyle, ) -> Result<(Vec<RepoPathBuf>, Vec<scm_record::File<'static>>), BuiltinToolError> { let materialize_options = ConflictMaterializeOptions { marker_style, marker_len: None, merge: store.merge_options().clone(), }; let mut diff_stream = materialized_diff_stream(store, tree_diff); let mut changed_files = Vec::new(); let mut files = Vec::new(); while let Some(entry) = diff_stream.next().await { let left_path = entry.path.source(); let right_path = entry.path.target(); let (left_value, right_value) = entry.values?; let left_info = read_file_contents(left_value, left_path, &materialize_options)?; let right_info = read_file_contents(right_value, right_path, &materialize_options)?; let mut sections = Vec::new(); if left_info.file_mode != right_info.file_mode { sections.push(scm_record::Section::FileMode { is_checked: false, mode: right_info.file_mode, }); } match (left_info.contents, right_info.contents) { (FileContents::Absent, FileContents::Absent) => {} // In this context, `Absent` means the file doesn't exist. If it only // exists on one side, we will render a mode change section above. // The next two patterns are to avoid also rendering an empty // changed lines section that clutters the UI. ( FileContents::Absent, FileContents::Text { contents: _, hash: _, num_bytes: 0, }, ) => {} ( FileContents::Text { contents: _, hash: _, num_bytes: 0, }, 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::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::Binary { hash: Some(left_hash), .. }, FileContents::Binary { hash: Some(right_hash), .. }, ) if left_hash == right_hash => { // Binary file contents have not changed. } (left, right @ FileContents::Binary { .. }) | (left @ FileContents::Binary { .. }, right) => { sections.push(scm_record::Section::Binary { is_checked: false, old_description: left.describe().map(Cow::Owned), new_description: right.describe().map(Cow::Owned), }); } } files.push(scm_record::File { old_path: None, // Path for displaying purposes, not for file access. path: Cow::Owned(right_path.to_fs_path_unchecked(Path::new(""))), file_mode: left_info.file_mode, sections, }); changed_files.push(entry.path.target); } Ok((changed_files, files)) } fn apply_diff_builtin( store: &Arc<Store>, left_tree: &MergedTree, right_tree: &MergedTree, changed_files: Vec<RepoPathBuf>, files: &[scm_record::File], ) -> BackendResult<MergedTreeId> { // Start with the right tree to match external tool behavior. // This ensures unmatched paths keep their values from the right tree. let mut tree_builder = MergedTreeBuilder::new(right_tree.id().clone()); // First, revert all changed files to their left versions for path in &changed_files { let left_value = left_tree.path_value(path)?; tree_builder.set_or_remove(path.clone(), left_value); } // Then apply only the selected changes apply_changes( &mut tree_builder, changed_files, files, |path| left_tree.path_value(path), |path| right_tree.path_value(path), |path, contents, executable, copy_id| { let old_value = left_tree.path_value(path)?; let new_value = if old_value.is_resolved() { let id = store.write_file(path, &mut &contents[..]).block_on()?; Merge::normal(TreeValue::File { id, executable, copy_id, }) } else if let Some(old_file_ids) = old_value.to_file_merge() { // TODO: should error out if conflicts couldn't be parsed? let new_file_ids = conflicts::update_from_content( &old_file_ids, store, path, contents, MIN_CONFLICT_MARKER_LEN, // TODO: use the materialization parameter ) .block_on()?; match new_file_ids.into_resolved() { Ok(id) => Merge::resolved(id.map(|id| TreeValue::File { id, executable, copy_id: CopyId::placeholder(), })), Err(file_ids) => old_value.with_new_file_ids(&file_ids), } } else { panic!("unexpected content change at {path:?}: {old_value:?}"); }; Ok(new_value) }, )?; tree_builder.write_tree(store) } fn apply_changes( tree_builder: &mut MergedTreeBuilder, changed_files: Vec<RepoPathBuf>, files: &[scm_record::File], select_left: impl Fn(&RepoPath) -> BackendResult<MergedTreeValue>, select_right: impl Fn(&RepoPath) -> BackendResult<MergedTreeValue>, write_file: impl Fn(&RepoPath, &[u8], bool, CopyId) -> BackendResult<MergedTreeValue>, ) -> BackendResult<()> { assert_eq!( changed_files.len(), files.len(), "result had a different number of files" ); // TODO: Write files concurrently for (path, file) in changed_files.into_iter().zip(files) { let file_mode_change_selected = file .sections .iter() .find_map(|sec| match sec { scm_record::Section::FileMode { is_checked, .. } => Some(*is_checked), _ => None, }) .unwrap_or(false); let ( scm_record::SelectedChanges { contents, file_mode, }, _unselected, ) = file.get_selected_contents(); if file_mode == mode::ABSENT { // The file is not present in the selected changes. // Either a file mode change was selected to delete an existing file, so we // should remove it from the tree, if file_mode_change_selected { tree_builder.set_or_remove(path, Merge::absent()); } // or the file's creation has been split out of the change, in which case we // don't need to change the tree. // In either case, we're done with this file afterwards. continue; } let executable = file_mode == mode::EXECUTABLE; match contents { scm_record::SelectedContents::Unchanged => { if file_mode_change_selected { // File contents haven't changed, but file mode needs to be updated on the tree. let value = override_file_executable_bit(select_left(&path)?, executable); tree_builder.set_or_remove(path, value); } else { // Neither file mode, nor contents changed => Do nothing. } } scm_record::SelectedContents::Binary {