improve: show whole-diff totals alongside file stats
This commit is contained in:
+136
-4
@@ -2817,7 +2817,7 @@ impl Repository {
|
||||
}
|
||||
|
||||
fn build_comparison(
|
||||
&self,
|
||||
&mut self,
|
||||
prepared: &mut PreparedComparison,
|
||||
same: bool,
|
||||
) -> Result<Comparison, String> {
|
||||
@@ -2835,6 +2835,7 @@ impl Repository {
|
||||
}
|
||||
files.push(ChangedFile {
|
||||
review_revisions: None,
|
||||
stats: None,
|
||||
path: path.clone(),
|
||||
old_path: None,
|
||||
status: if a.is_none() {
|
||||
@@ -2887,11 +2888,18 @@ impl Repository {
|
||||
Ok(self.comparison_catalog(prepared))
|
||||
}
|
||||
|
||||
fn comparison_catalog(&self, prepared: &PreparedComparison) -> Comparison {
|
||||
fn comparison_catalog(&mut self, prepared: &PreparedComparison) -> Comparison {
|
||||
let stats = if prepared.files.is_empty() {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
self.ensure_private()
|
||||
.and_then(|()| self.comparison_stats(prepared))
|
||||
};
|
||||
let files = prepared
|
||||
.files
|
||||
.iter()
|
||||
.map(|(path, old_path, status)| ChangedFile {
|
||||
.enumerate()
|
||||
.map(|(index, (path, old_path, status))| ChangedFile {
|
||||
review_revisions: Some([
|
||||
review_file_revision(prepared.left.get(old_path.as_ref().unwrap_or(path))),
|
||||
review_file_revision(prepared.right.get(path)),
|
||||
@@ -2900,12 +2908,16 @@ impl Repository {
|
||||
old_path: old_path.clone(),
|
||||
status: *status,
|
||||
loaded: false,
|
||||
stats: stats.as_ref().ok().and_then(|stats| stats[index]),
|
||||
before: vec![],
|
||||
after: vec![],
|
||||
before_eof: false,
|
||||
after_eof: false,
|
||||
hunks: vec![],
|
||||
notice: None,
|
||||
notice: stats
|
||||
.as_ref()
|
||||
.err()
|
||||
.map(|error| format!("Line statistics unavailable: {error}")),
|
||||
})
|
||||
.collect();
|
||||
let editable_paths = if prepared.editable_after {
|
||||
@@ -2934,6 +2946,125 @@ impl Repository {
|
||||
}
|
||||
}
|
||||
|
||||
fn comparison_stats(
|
||||
&self,
|
||||
prepared: &PreparedComparison,
|
||||
) -> Result<Vec<Option<(usize, usize)>>, String> {
|
||||
// ponytail: unopened counts use Git's text/Myers rules; share the renderer's
|
||||
// counter if exact binary-detection and pathological-diff parity is needed.
|
||||
let store = &self.private.as_ref().unwrap().1;
|
||||
let sides: Vec<_> = prepared
|
||||
.files
|
||||
.iter()
|
||||
.map(|(path, old_path, _)| {
|
||||
[
|
||||
prepared.left.get(old_path.as_ref().unwrap_or(path)),
|
||||
prepared.right.get(path),
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
let objects: BTreeSet<_> = sides
|
||||
.iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.filter(|file| file.projection.is_none())
|
||||
.filter_map(|file| file.object.as_ref().map(|(_, id)| id.as_str()))
|
||||
.collect();
|
||||
let mut sizes = BTreeMap::new();
|
||||
if !objects.is_empty() {
|
||||
let mut input = Vec::new();
|
||||
for id in objects {
|
||||
writeln!(&mut input, "{id}").unwrap();
|
||||
}
|
||||
let mut command = git_command(store);
|
||||
command.arg("cat-file").arg("--batch-check");
|
||||
let output = text(checked(run(command, input, &self.cancelled)?)?)?;
|
||||
for line in output.lines() {
|
||||
let fields: Vec<_> = line.split(' ').collect();
|
||||
if fields.len() != 3 || fields[1] != "blob" {
|
||||
return Err("Invalid statistics blob metadata".into());
|
||||
}
|
||||
let size = fields[2]
|
||||
.parse::<usize>()
|
||||
.map_err(|_| "Invalid statistics blob size")?;
|
||||
sizes.insert(fields[0].to_owned(), size);
|
||||
}
|
||||
}
|
||||
let mut stats = vec![Some((0, 0)); sides.len()];
|
||||
let mut trees = [Vec::new(), Vec::new()];
|
||||
for (index, files) in sides.iter().enumerate() {
|
||||
self.check_cancelled()?;
|
||||
if files.iter().flatten().any(|file| {
|
||||
file.projection.is_some()
|
||||
|| file.object.as_ref().is_some_and(|(_, id)| {
|
||||
sizes
|
||||
.get(id)
|
||||
.is_none_or(|size| *size > COMPARISON_FILE_LIMIT)
|
||||
})
|
||||
}) {
|
||||
stats[index] = None;
|
||||
continue;
|
||||
}
|
||||
for (side, file) in files.iter().enumerate() {
|
||||
if let Some((_, id)) = file.and_then(|file| file.object.as_ref()) {
|
||||
// Flat synthetic paths avoid attributes, quoting and rename detection.
|
||||
// Normalize modes: the renderer compares link targets as ordinary text.
|
||||
write!(&mut trees[side], "100644 blob {id}\t{index}\0").unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
if trees.iter().all(Vec::is_empty) {
|
||||
return Ok(stats);
|
||||
}
|
||||
let mut ids = Vec::new();
|
||||
for tree in trees {
|
||||
let mut command = git_command(store);
|
||||
command.args(["mktree", "-z"]);
|
||||
ids.push(text(checked(run(command, tree, &self.cancelled)?)?)?);
|
||||
}
|
||||
let mut command = git_command(store);
|
||||
command
|
||||
.args(["-c", "core.attributesFile=/dev/null"])
|
||||
.args([
|
||||
"diff-tree",
|
||||
"--numstat",
|
||||
"-z",
|
||||
"--no-renames",
|
||||
"--no-ext-diff",
|
||||
"--no-textconv",
|
||||
"--diff-algorithm=myers",
|
||||
"--no-indent-heuristic",
|
||||
])
|
||||
.args(ids.iter().map(|id| id.trim()))
|
||||
.env("GIT_ATTR_NOSYSTEM", "1")
|
||||
.env("GIT_CONFIG_NOSYSTEM", "1")
|
||||
.env("GIT_CONFIG_GLOBAL", "/dev/null");
|
||||
let output = checked(run(command, Vec::new(), &self.cancelled)?)?;
|
||||
for record in output.split(|byte| *byte == 0).filter(|r| !r.is_empty()) {
|
||||
let record = std::str::from_utf8(record).map_err(|_| "Invalid line statistics")?;
|
||||
let fields: Vec<_> = record.split('\t').collect();
|
||||
if fields.len() != 3 {
|
||||
return Err("Invalid line statistics".into());
|
||||
}
|
||||
let index = fields[2]
|
||||
.parse::<usize>()
|
||||
.ok()
|
||||
.filter(|index| *index < stats.len())
|
||||
.ok_or("Invalid line statistics index")?;
|
||||
stats[index] = Some(if fields[0] == "-" && fields[1] == "-" {
|
||||
(0, 0)
|
||||
} else {
|
||||
(
|
||||
fields[0].parse().map_err(|_| "Invalid added line count")?,
|
||||
fields[1]
|
||||
.parse()
|
||||
.map_err(|_| "Invalid removed line count")?,
|
||||
)
|
||||
});
|
||||
}
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
/// Prepare one catalog entry using its original comparison's frozen sources.
|
||||
pub fn load_file(
|
||||
&mut self,
|
||||
@@ -3044,6 +3175,7 @@ impl Repository {
|
||||
notices.dedup();
|
||||
Ok(ChangedFile {
|
||||
review_revisions: None,
|
||||
stats: None,
|
||||
path: path.to_path_buf(),
|
||||
old_path: None,
|
||||
status,
|
||||
|
||||
Reference in New Issue
Block a user