mirror of
https://github.com/martinvonz/jj.git
synced 2025-05-05 23:42:50 +00:00
make_diff_files() will be async function that uses materialized_diff_stream() internally. apply_diff_builtin() will take callbacks to handle binary/conflict files.
1226 lines
41 KiB
Rust
1226 lines
41 KiB
Rust
use std::borrow::Cow;
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
|
|
use futures::StreamExt as _;
|
|
use futures::TryFutureExt as _;
|
|
use futures::TryStreamExt as _;
|
|
use itertools::Itertools as _;
|
|
use jj_lib::backend::BackendResult;
|
|
use jj_lib::backend::MergedTreeId;
|
|
use jj_lib::backend::TreeValue;
|
|
use jj_lib::conflicts::materialize_merge_result_to_bytes;
|
|
use jj_lib::conflicts::materialize_tree_value;
|
|
use jj_lib::conflicts::ConflictMarkerStyle;
|
|
use jj_lib::conflicts::MaterializedTreeValue;
|
|
use jj_lib::diff::Diff;
|
|
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::merged_tree::MergedTree;
|
|
use jj_lib::merged_tree::MergedTreeBuilder;
|
|
use jj_lib::merged_tree::TreeDiffEntry;
|
|
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 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,
|
|
},
|
|
}
|
|
|
|
/// 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 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,
|
|
conflict_marker_style: ConflictMarkerStyle,
|
|
) -> 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: 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)?;
|
|
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) => {
|
|
let buf =
|
|
materialize_merge_result_to_bytes(&file.contents, conflict_marker_style).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: 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 = Diff::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 =
|
|
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(),
|
|
});
|
|
}
|
|
DiffHunkKind::Different => {
|
|
let sides = &hunk.contents;
|
|
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],
|
|
conflict_marker_style: ConflictMarkerStyle,
|
|
) -> Result<Vec<scm_record::File<'static>>, BuiltinToolError> {
|
|
let mut files = Vec::new();
|
|
for changed_path in changed_files {
|
|
let left_info = read_file_contents(store, left_tree, changed_path, conflict_marker_style)?;
|
|
let right_info =
|
|
read_file_contents(store, right_tree, changed_path, conflict_marker_style)?;
|
|
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::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 for displaying purposes, not for file access.
|
|
path: Cow::Owned(changed_path.to_fs_path_unchecked(Path::new(""))),
|
|
file_mode: left_info.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],
|
|
) -> BackendResult<MergedTreeId> {
|
|
let mut tree_builder = MergedTreeBuilder::new(left_tree.id().clone());
|
|
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 (
|
|
scm_record::SelectedChanges {
|
|
contents,
|
|
file_mode,
|
|
},
|
|
_unselected,
|
|
) = file.get_selected_contents();
|
|
|
|
// If a file was not present in the selected changes (i.e. split out of a
|
|
// change, deleted, etc.) remove it from the tree.
|
|
if file_mode == scm_record::FileMode::Absent {
|
|
tree_builder.set_or_remove(path, Merge::absent());
|
|
continue;
|
|
}
|
|
|
|
match contents {
|
|
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::Text { contents } => {
|
|
let file_id = store
|
|
.write_file(&path, &mut contents.as_bytes())
|
|
.block_on()?;
|
|
|
|
tree_builder.set_or_remove(
|
|
path,
|
|
Merge::normal(TreeValue::File {
|
|
id: file_id,
|
|
executable: file_mode == 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,
|
|
conflict_marker_style: ConflictMarkerStyle,
|
|
) -> Result<MergedTreeId, BuiltinToolError> {
|
|
let store = left_tree.store().clone();
|
|
// TODO: handle copy tracking
|
|
let changed_files: Vec<_> = left_tree
|
|
.diff_stream(right_tree, matcher)
|
|
.map(|TreeDiffEntry { path, values }| values.map(|_| path))
|
|
.try_collect()
|
|
.block_on()?;
|
|
let files = make_diff_files(
|
|
&store,
|
|
left_tree,
|
|
right_tree,
|
|
&changed_files,
|
|
conflict_marker_style,
|
|
)?;
|
|
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(buf) => {
|
|
let contents = buf_to_file_contents(None, buf.into());
|
|
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(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 contents = std::str::from_utf8(contents).map_err(|err| {
|
|
BuiltinToolError::DecodeUtf8 {
|
|
source: err,
|
|
item: "conflicting hunk",
|
|
}
|
|
})?;
|
|
let changed_lines =
|
|
make_section_changed_lines(contents, change_type);
|
|
Ok(changed_lines)
|
|
})
|
|
.flatten_ok()
|
|
.try_collect()?;
|
|
scm_record::Section::Changed { lines }
|
|
}
|
|
};
|
|
sections.push(section);
|
|
}
|
|
}
|
|
}
|
|
Ok(sections)
|
|
}
|
|
|
|
fn make_merge_file(
|
|
merge_tool_file: &MergeToolFile,
|
|
) -> Result<scm_record::File<'static>, BuiltinToolError> {
|
|
let file = &merge_tool_file.file;
|
|
let file_mode = if file.executable.expect("should have been resolved") {
|
|
mode::EXECUTABLE
|
|
} else {
|
|
mode::NORMAL
|
|
};
|
|
let merge_result = files::merge_hunks(&file.contents);
|
|
let sections = make_merge_sections(merge_result)?;
|
|
Ok(scm_record::File {
|
|
old_path: None,
|
|
// Path for displaying purposes, not for file access.
|
|
path: Cow::Owned(
|
|
merge_tool_file
|
|
.repo_path
|
|
.to_fs_path_unchecked(Path::new("")),
|
|
),
|
|
file_mode,
|
|
sections,
|
|
})
|
|
}
|
|
|
|
pub fn edit_merge_builtin(
|
|
tree: &MergedTree,
|
|
merge_tool_files: &[MergeToolFile],
|
|
) -> Result<MergedTreeId, BuiltinToolError> {
|
|
let mut input = scm_record::helpers::CrosstermInput;
|
|
let recorder = scm_record::Recorder::new(
|
|
scm_record::RecordState {
|
|
is_read_only: false,
|
|
files: merge_tool_files.iter().map(make_merge_file).try_collect()?,
|
|
commits: Default::default(),
|
|
},
|
|
&mut input,
|
|
);
|
|
let state = recorder.run()?;
|
|
|
|
apply_diff_builtin(
|
|
tree.store(),
|
|
tree,
|
|
tree,
|
|
merge_tool_files
|
|
.iter()
|
|
.map(|file| file.repo_path.clone())
|
|
.collect_vec(),
|
|
&state.files,
|
|
)
|
|
.map_err(BuiltinToolError::BackendError)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use jj_lib::backend::FileId;
|
|
use jj_lib::conflicts::extract_as_single_hunk;
|
|
use jj_lib::merge::MergedTreeValue;
|
|
use jj_lib::repo::Repo as _;
|
|
use testutils::repo_path;
|
|
use testutils::TestRepo;
|
|
|
|
use super::*;
|
|
|
|
fn make_diff(
|
|
store: &Arc<Store>,
|
|
left_tree: &MergedTree,
|
|
right_tree: &MergedTree,
|
|
changed_files: &[RepoPathBuf],
|
|
) -> Vec<scm_record::File<'static>> {
|
|
make_diff_files(
|
|
store,
|
|
left_tree,
|
|
right_tree,
|
|
changed_files,
|
|
ConflictMarkerStyle::Diff,
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn apply_diff(
|
|
store: &Arc<Store>,
|
|
left_tree: &MergedTree,
|
|
right_tree: &MergedTree,
|
|
changed_files: &[RepoPathBuf],
|
|
files: &[scm_record::File],
|
|
) -> MergedTreeId {
|
|
apply_diff_builtin(store, left_tree, right_tree, changed_files.to_vec(), files).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_diff_builtin() {
|
|
let test_repo = TestRepo::init();
|
|
let store = test_repo.repo.store();
|
|
|
|
let unused_path = repo_path("unused");
|
|
let unchanged = repo_path("unchanged");
|
|
let changed_path = repo_path("changed");
|
|
let added_path = repo_path("added");
|
|
let left_tree = testutils::create_tree(
|
|
&test_repo.repo,
|
|
&[
|
|
(unused_path, "unused\n"),
|
|
(unchanged, "unchanged\n"),
|
|
(changed_path, "line1\nline2\nline3\n"),
|
|
],
|
|
);
|
|
let right_tree = testutils::create_tree(
|
|
&test_repo.repo,
|
|
&[
|
|
(unused_path, "unused\n"),
|
|
(unchanged, "unchanged\n"),
|
|
(changed_path, "line1\nchanged1\nchanged2\nline3\nadded1\n"),
|
|
(added_path, "added\n"),
|
|
],
|
|
);
|
|
|
|
let changed_files = vec![
|
|
unchanged.to_owned(),
|
|
changed_path.to_owned(),
|
|
added_path.to_owned(),
|
|
];
|
|
let files = make_diff(store, &left_tree, &right_tree, &changed_files);
|
|
insta::assert_debug_snapshot!(files, @r#"
|
|
[
|
|
File {
|
|
old_path: None,
|
|
path: "unchanged",
|
|
file_mode: Unix(
|
|
33188,
|
|
),
|
|
sections: [
|
|
Unchanged {
|
|
lines: [
|
|
"unchanged\n",
|
|
],
|
|
},
|
|
],
|
|
},
|
|
File {
|
|
old_path: None,
|
|
path: "changed",
|
|
file_mode: Unix(
|
|
33188,
|
|
),
|
|
sections: [
|
|
Unchanged {
|
|
lines: [
|
|
"line1\n",
|
|
],
|
|
},
|
|
Changed {
|
|
lines: [
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Removed,
|
|
line: "line2\n",
|
|
},
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Added,
|
|
line: "changed1\n",
|
|
},
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Added,
|
|
line: "changed2\n",
|
|
},
|
|
],
|
|
},
|
|
Unchanged {
|
|
lines: [
|
|
"line3\n",
|
|
],
|
|
},
|
|
Changed {
|
|
lines: [
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Added,
|
|
line: "added1\n",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
File {
|
|
old_path: None,
|
|
path: "added",
|
|
file_mode: Absent,
|
|
sections: [
|
|
FileMode {
|
|
is_checked: false,
|
|
mode: Unix(
|
|
33188,
|
|
),
|
|
},
|
|
Changed {
|
|
lines: [
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Added,
|
|
line: "added\n",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
]
|
|
"#);
|
|
|
|
let no_changes_tree_id = apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
|
|
let no_changes_tree = store.get_root_tree(&no_changes_tree_id).unwrap();
|
|
assert_eq!(
|
|
no_changes_tree.id(),
|
|
left_tree.id(),
|
|
"no-changes tree was different",
|
|
);
|
|
|
|
let mut files = files;
|
|
for file in &mut files {
|
|
file.toggle_all();
|
|
}
|
|
let all_changes_tree_id =
|
|
apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
|
|
let all_changes_tree = store.get_root_tree(&all_changes_tree_id).unwrap();
|
|
assert_eq!(
|
|
all_changes_tree.id(),
|
|
right_tree.id(),
|
|
"all-changes tree was different",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_diff_builtin_add_empty_file() {
|
|
let test_repo = TestRepo::init();
|
|
let store = test_repo.repo.store();
|
|
|
|
let added_empty_file_path = repo_path("empty_file");
|
|
let left_tree = testutils::create_tree(&test_repo.repo, &[]);
|
|
let right_tree = testutils::create_tree(&test_repo.repo, &[(added_empty_file_path, "")]);
|
|
|
|
let changed_files = vec![added_empty_file_path.to_owned()];
|
|
let files = make_diff(store, &left_tree, &right_tree, &changed_files);
|
|
insta::assert_debug_snapshot!(files, @r#"
|
|
[
|
|
File {
|
|
old_path: None,
|
|
path: "empty_file",
|
|
file_mode: Absent,
|
|
sections: [
|
|
FileMode {
|
|
is_checked: false,
|
|
mode: Unix(
|
|
33188,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
]
|
|
"#);
|
|
let no_changes_tree_id = apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
|
|
let no_changes_tree = store.get_root_tree(&no_changes_tree_id).unwrap();
|
|
assert_eq!(
|
|
no_changes_tree.id(),
|
|
left_tree.id(),
|
|
"no-changes tree was different",
|
|
);
|
|
|
|
let mut files = files;
|
|
for file in &mut files {
|
|
file.toggle_all();
|
|
}
|
|
let all_changes_tree_id =
|
|
apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
|
|
let all_changes_tree = store.get_root_tree(&all_changes_tree_id).unwrap();
|
|
assert_eq!(
|
|
all_changes_tree.id(),
|
|
right_tree.id(),
|
|
"all-changes tree was different",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_diff_builtin_add_executable_file() {
|
|
let test_repo = TestRepo::init();
|
|
let store = test_repo.repo.store();
|
|
|
|
let added_executable_file_path = repo_path("executable_file");
|
|
let left_tree = testutils::create_tree(&test_repo.repo, &[]);
|
|
let right_tree = {
|
|
// let store = test_repo.repo.store();
|
|
let mut tree_builder = store.tree_builder(store.empty_tree_id().clone());
|
|
testutils::write_executable_file(
|
|
&mut tree_builder,
|
|
added_executable_file_path,
|
|
"executable",
|
|
);
|
|
let id = tree_builder.write_tree().unwrap();
|
|
MergedTree::resolved(store.get_tree(RepoPathBuf::root(), &id).unwrap())
|
|
};
|
|
|
|
let changed_files = vec![added_executable_file_path.to_owned()];
|
|
let files = make_diff(store, &left_tree, &right_tree, &changed_files);
|
|
insta::assert_debug_snapshot!(files, @r###"
|
|
[
|
|
File {
|
|
old_path: None,
|
|
path: "executable_file",
|
|
file_mode: Absent,
|
|
sections: [
|
|
FileMode {
|
|
is_checked: false,
|
|
mode: Unix(
|
|
33261,
|
|
),
|
|
},
|
|
Changed {
|
|
lines: [
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Added,
|
|
line: "executable",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
]
|
|
"###);
|
|
let no_changes_tree_id = apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
|
|
let no_changes_tree = store.get_root_tree(&no_changes_tree_id).unwrap();
|
|
assert_eq!(
|
|
no_changes_tree.id(),
|
|
left_tree.id(),
|
|
"no-changes tree was different",
|
|
);
|
|
|
|
let mut files = files;
|
|
for file in &mut files {
|
|
file.toggle_all();
|
|
}
|
|
let all_changes_tree_id =
|
|
apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
|
|
let all_changes_tree = store.get_root_tree(&all_changes_tree_id).unwrap();
|
|
assert_eq!(
|
|
all_changes_tree.id(),
|
|
right_tree.id(),
|
|
"all-changes tree was different",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_diff_builtin_delete_file() {
|
|
let test_repo = TestRepo::init();
|
|
let store = test_repo.repo.store();
|
|
|
|
let file_path = repo_path("file_with_content");
|
|
let left_tree = testutils::create_tree(&test_repo.repo, &[(file_path, "content\n")]);
|
|
let right_tree = testutils::create_tree(&test_repo.repo, &[]);
|
|
|
|
let changed_files = vec![file_path.to_owned()];
|
|
let files = make_diff(store, &left_tree, &right_tree, &changed_files);
|
|
insta::assert_debug_snapshot!(files, @r###"
|
|
[
|
|
File {
|
|
old_path: None,
|
|
path: "file_with_content",
|
|
file_mode: Unix(
|
|
33188,
|
|
),
|
|
sections: [
|
|
FileMode {
|
|
is_checked: false,
|
|
mode: Absent,
|
|
},
|
|
Changed {
|
|
lines: [
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Removed,
|
|
line: "content\n",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
]
|
|
"###);
|
|
let no_changes_tree_id = apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
|
|
let no_changes_tree = store.get_root_tree(&no_changes_tree_id).unwrap();
|
|
assert_eq!(
|
|
no_changes_tree.id(),
|
|
left_tree.id(),
|
|
"no-changes tree was different",
|
|
);
|
|
|
|
let mut files = files;
|
|
for file in &mut files {
|
|
file.toggle_all();
|
|
}
|
|
let all_changes_tree_id =
|
|
apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
|
|
let all_changes_tree = store.get_root_tree(&all_changes_tree_id).unwrap();
|
|
assert_eq!(
|
|
all_changes_tree.id(),
|
|
right_tree.id(),
|
|
"all-changes tree was different",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_diff_builtin_delete_empty_file() {
|
|
let test_repo = TestRepo::init();
|
|
let store = test_repo.repo.store();
|
|
|
|
let added_empty_file_path = repo_path("empty_file");
|
|
let left_tree = testutils::create_tree(&test_repo.repo, &[(added_empty_file_path, "")]);
|
|
let right_tree = testutils::create_tree(&test_repo.repo, &[]);
|
|
|
|
let changed_files = vec![added_empty_file_path.to_owned()];
|
|
let files = make_diff(store, &left_tree, &right_tree, &changed_files);
|
|
insta::assert_debug_snapshot!(files, @r#"
|
|
[
|
|
File {
|
|
old_path: None,
|
|
path: "empty_file",
|
|
file_mode: Unix(
|
|
33188,
|
|
),
|
|
sections: [
|
|
FileMode {
|
|
is_checked: false,
|
|
mode: Absent,
|
|
},
|
|
],
|
|
},
|
|
]
|
|
"#);
|
|
let no_changes_tree_id = apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
|
|
let no_changes_tree = store.get_root_tree(&no_changes_tree_id).unwrap();
|
|
assert_eq!(
|
|
no_changes_tree.id(),
|
|
left_tree.id(),
|
|
"no-changes tree was different",
|
|
);
|
|
|
|
let mut files = files;
|
|
for file in &mut files {
|
|
file.toggle_all();
|
|
}
|
|
let all_changes_tree_id =
|
|
apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
|
|
let all_changes_tree = store.get_root_tree(&all_changes_tree_id).unwrap();
|
|
assert_eq!(
|
|
all_changes_tree.id(),
|
|
right_tree.id(),
|
|
"all-changes tree was different",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_diff_builtin_modify_empty_file() {
|
|
let test_repo = TestRepo::init();
|
|
let store = test_repo.repo.store();
|
|
|
|
let empty_file_path = repo_path("empty_file");
|
|
let left_tree = testutils::create_tree(&test_repo.repo, &[(empty_file_path, "")]);
|
|
let right_tree =
|
|
testutils::create_tree(&test_repo.repo, &[(empty_file_path, "modified\n")]);
|
|
|
|
let changed_files = vec![empty_file_path.to_owned()];
|
|
let files = make_diff(store, &left_tree, &right_tree, &changed_files);
|
|
insta::assert_debug_snapshot!(files, @r#"
|
|
[
|
|
File {
|
|
old_path: None,
|
|
path: "empty_file",
|
|
file_mode: Unix(
|
|
33188,
|
|
),
|
|
sections: [
|
|
Changed {
|
|
lines: [
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Added,
|
|
line: "modified\n",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
]
|
|
"#);
|
|
let no_changes_tree_id = apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
|
|
let no_changes_tree = store.get_root_tree(&no_changes_tree_id).unwrap();
|
|
assert_eq!(
|
|
no_changes_tree.id(),
|
|
left_tree.id(),
|
|
"no-changes tree was different",
|
|
);
|
|
|
|
let mut files = files;
|
|
for file in &mut files {
|
|
file.toggle_all();
|
|
}
|
|
let all_changes_tree_id =
|
|
apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
|
|
let all_changes_tree = store.get_root_tree(&all_changes_tree_id).unwrap();
|
|
assert_eq!(
|
|
all_changes_tree.id(),
|
|
right_tree.id(),
|
|
"all-changes tree was different",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_diff_builtin_make_file_empty() {
|
|
let test_repo = TestRepo::init();
|
|
let store = test_repo.repo.store();
|
|
|
|
let file_path = repo_path("file_with_content");
|
|
let left_tree = testutils::create_tree(&test_repo.repo, &[(file_path, "content\n")]);
|
|
let right_tree = testutils::create_tree(&test_repo.repo, &[(file_path, "")]);
|
|
|
|
let changed_files = vec![file_path.to_owned()];
|
|
let files = make_diff(store, &left_tree, &right_tree, &changed_files);
|
|
insta::assert_debug_snapshot!(files, @r###"
|
|
[
|
|
File {
|
|
old_path: None,
|
|
path: "file_with_content",
|
|
file_mode: Unix(
|
|
33188,
|
|
),
|
|
sections: [
|
|
Changed {
|
|
lines: [
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Removed,
|
|
line: "content\n",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
]
|
|
"###);
|
|
let no_changes_tree_id = apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
|
|
let no_changes_tree = store.get_root_tree(&no_changes_tree_id).unwrap();
|
|
assert_eq!(
|
|
no_changes_tree.id(),
|
|
left_tree.id(),
|
|
"no-changes tree was different",
|
|
);
|
|
|
|
let mut files = files;
|
|
for file in &mut files {
|
|
file.toggle_all();
|
|
}
|
|
let all_changes_tree_id =
|
|
apply_diff(store, &left_tree, &right_tree, &changed_files, &files);
|
|
let all_changes_tree = store.get_root_tree(&all_changes_tree_id).unwrap();
|
|
assert_eq!(
|
|
all_changes_tree.id(),
|
|
right_tree.id(),
|
|
"all-changes tree was different",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_make_merge_sections() {
|
|
let test_repo = TestRepo::init();
|
|
let store = test_repo.repo.store();
|
|
|
|
let path = repo_path("file");
|
|
let base_tree = testutils::create_tree(
|
|
&test_repo.repo,
|
|
&[(path, "base 1\nbase 2\nbase 3\nbase 4\nbase 5\n")],
|
|
);
|
|
let left_tree = testutils::create_tree(
|
|
&test_repo.repo,
|
|
&[(path, "left 1\nbase 2\nbase 3\nbase 4\nleft 5\n")],
|
|
);
|
|
let right_tree = testutils::create_tree(
|
|
&test_repo.repo,
|
|
&[(path, "right 1\nbase 2\nbase 3\nbase 4\nright 5\n")],
|
|
);
|
|
|
|
fn to_file_id(tree_value: MergedTreeValue) -> Option<FileId> {
|
|
match tree_value.into_resolved() {
|
|
Ok(Some(TreeValue::File { id, executable: _ })) => Some(id.clone()),
|
|
other => {
|
|
panic!("merge should have been a FileId: {other:?}")
|
|
}
|
|
}
|
|
}
|
|
let merge = Merge::from_vec(vec![
|
|
to_file_id(left_tree.path_value(path).unwrap()),
|
|
to_file_id(base_tree.path_value(path).unwrap()),
|
|
to_file_id(right_tree.path_value(path).unwrap()),
|
|
]);
|
|
let content = extract_as_single_hunk(&merge, store, path)
|
|
.block_on()
|
|
.unwrap();
|
|
let merge_result = files::merge_hunks(&content);
|
|
let sections = make_merge_sections(merge_result).unwrap();
|
|
insta::assert_debug_snapshot!(sections, @r#"
|
|
[
|
|
Changed {
|
|
lines: [
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Added,
|
|
line: "left 1\n",
|
|
},
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Removed,
|
|
line: "base 1\n",
|
|
},
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Added,
|
|
line: "right 1\n",
|
|
},
|
|
],
|
|
},
|
|
Unchanged {
|
|
lines: [
|
|
"base 2\n",
|
|
"base 3\n",
|
|
"base 4\n",
|
|
],
|
|
},
|
|
Changed {
|
|
lines: [
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Added,
|
|
line: "left 5\n",
|
|
},
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Removed,
|
|
line: "base 5\n",
|
|
},
|
|
SectionChangedLine {
|
|
is_checked: false,
|
|
change_type: Added,
|
|
line: "right 5\n",
|
|
},
|
|
],
|
|
},
|
|
]
|
|
"#);
|
|
}
|
|
}
|