ok/jj
1
0
Fork 0
forked from mirrors/jj

copies: determine copy/rename operation by CopiesTreeDiffStream

Not all callers need this information, but I assumed it's relatively cheap to
look up the source path in the target tree compared to diffing.

This could be represented as Regular(_)|Copied(_, _)|Renamed(_, _), but it's
a bit weird if Copied and Renamed were separate variants. Instead, I decided
to wrap copy metadata in Option.
This commit is contained in:
Yuya Nishihara 2024-08-22 19:27:25 +09:00
parent b6060ce6dd
commit f5187fa063
4 changed files with 60 additions and 23 deletions

View file

@ -35,6 +35,7 @@ use jj_lib::conflicts::materialized_diff_stream;
use jj_lib::conflicts::MaterializedTreeDiffEntry;
use jj_lib::conflicts::MaterializedTreeValue;
use jj_lib::copies::CopiesTreeDiffEntry;
use jj_lib::copies::CopyOperation;
use jj_lib::copies::CopyRecords;
use jj_lib::diff::Diff;
use jj_lib::diff::DiffHunk;
@ -1297,11 +1298,10 @@ pub fn show_git_diff(
writeln!(formatter, "index {left_hash}..{right_hash}")?;
}
(Some(left_mode), Some(right_mode)) => {
if left_path != right_path {
let operation = if to_tree.path_value(left_path)?.is_absent() {
"rename"
} else {
"copy"
if let Some(op) = path.copy_operation() {
let operation = match op {
CopyOperation::Copy => "copy",
CopyOperation::Rename => "rename",
};
// TODO: include similarity index?
writeln!(formatter, "{operation} from {left_path_string}")?;
@ -1375,13 +1375,13 @@ pub fn show_diff_summary(
let (before, after) = values?;
let before_path = path.source();
let after_path = path.target();
if before_path != after_path {
if let Some(op) = path.copy_operation() {
let (label, sigil) = match op {
CopyOperation::Copy => ("copied", "C"),
CopyOperation::Rename => ("renamed", "R"),
};
let path = path_converter.format_copied_path(before_path, after_path);
if to_tree.path_value(before_path).unwrap().is_absent() {
writeln!(formatter.labeled("renamed"), "R {path}")?
} else {
writeln!(formatter.labeled("copied"), "C {path}")?
}
writeln!(formatter.labeled(label), "{sigil} {path}")?
} else {
let path = path_converter.format_file_path(after_path);
match (before.is_present(), after.is_present()) {

View file

@ -90,6 +90,15 @@ impl CopyRecords {
}
}
/// Whether or not the source path was deleted.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CopyOperation {
/// The source path was not deleted.
Copy,
/// The source path was renamed to the destination.
Rename,
}
/// A `TreeDiffEntry` with copy information.
pub struct CopiesTreeDiffEntry {
/// The path.
@ -98,11 +107,11 @@ pub struct CopiesTreeDiffEntry {
pub values: BackendResult<(MergedTreeValue, MergedTreeValue)>,
}
/// Path of `CopiesTreeDiffEntry`.
/// Path and copy information of `CopiesTreeDiffEntry`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CopiesTreeDiffEntryPath {
/// The source path if this is a copy or rename.
pub source: Option<RepoPathBuf>,
/// The source path and copy information if this is a copy or rename.
pub source: Option<(RepoPathBuf, CopyOperation)>,
/// The target path.
pub target: RepoPathBuf,
}
@ -110,19 +119,26 @@ pub struct CopiesTreeDiffEntryPath {
impl CopiesTreeDiffEntryPath {
/// The source path.
pub fn source(&self) -> &RepoPath {
self.source.as_ref().unwrap_or(&self.target)
self.source.as_ref().map_or(&self.target, |(path, _)| path)
}
/// The target path.
pub fn target(&self) -> &RepoPath {
&self.target
}
/// Whether this entry was copied or renamed from the source. Returns `None`
/// if the path is unchanged.
pub fn copy_operation(&self) -> Option<CopyOperation> {
self.source.as_ref().map(|(_, op)| *op)
}
}
/// Wraps a `TreeDiffStream`, adding support for copies and renames.
pub struct CopiesTreeDiffStream<'a> {
inner: TreeDiffStream<'a>,
source_tree: MergedTree,
target_tree: MergedTree,
copy_records: &'a CopyRecords,
}
@ -131,14 +147,32 @@ impl<'a> CopiesTreeDiffStream<'a> {
pub fn new(
inner: TreeDiffStream<'a>,
source_tree: MergedTree,
target_tree: MergedTree,
copy_records: &'a CopyRecords,
) -> Self {
Self {
inner,
source_tree,
target_tree,
copy_records,
}
}
fn resolve_copy_source(
&self,
source: &RepoPath,
values: BackendResult<(MergedTreeValue, MergedTreeValue)>,
) -> BackendResult<(CopyOperation, (MergedTreeValue, MergedTreeValue))> {
let (_, target_value) = values?;
let source_value = self.source_tree.path_value(source)?;
// If the source path is deleted in the target tree, it's a rename.
let copy_op = if self.target_tree.path_value(source)?.is_absent() {
CopyOperation::Rename
} else {
CopyOperation::Copy
};
Ok((copy_op, (source_value, target_value)))
}
}
impl Stream for CopiesTreeDiffStream<'_> {
@ -163,16 +197,17 @@ impl Stream for CopiesTreeDiffStream<'_> {
}));
};
let (copy_op, values) = match self.resolve_copy_source(source, diff_entry.values) {
Ok((copy_op, values)) => (copy_op, Ok(values)),
// Fall back to "copy" (= path still exists) if unknown.
Err(err) => (CopyOperation::Copy, Err(err)),
};
return Poll::Ready(Some(CopiesTreeDiffEntry {
path: CopiesTreeDiffEntryPath {
source: Some(source.clone()),
source: Some((source.clone(), copy_op)),
target: diff_entry.path,
},
values: diff_entry.values.and_then(|(_, target_value)| {
self.source_tree
.path_value(source)
.map(|source_value| (source_value, target_value))
}),
values,
}));
}

View file

@ -313,6 +313,7 @@ impl MergedTree {
Box::pin(CopiesTreeDiffStream::new(
stream,
self.clone(),
other.clone(),
copy_records,
))
}

View file

@ -20,6 +20,7 @@ use jj_lib::backend::FileId;
use jj_lib::backend::MergedTreeId;
use jj_lib::backend::TreeValue;
use jj_lib::copies::CopiesTreeDiffEntryPath;
use jj_lib::copies::CopyOperation;
use jj_lib::copies::CopyRecords;
use jj_lib::files::MergeResult;
use jj_lib::matchers::EverythingMatcher;
@ -889,7 +890,7 @@ fn test_diff_copy_tracing() {
diff[1].clone(),
(
CopiesTreeDiffEntryPath {
source: Some(modified_path.to_owned()),
source: Some((modified_path.to_owned(), CopyOperation::Copy)),
target: copied_path.to_owned(),
},
(
@ -902,7 +903,7 @@ fn test_diff_copy_tracing() {
diff[2].clone(),
(
CopiesTreeDiffEntryPath {
source: Some(removed_path.to_owned()),
source: Some((removed_path.to_owned(), CopyOperation::Rename)),
target: added_path.to_owned(),
},
(