feat: edit working-checkout comparisons with normal editor actions
This commit is contained in:
+87
-68
@@ -1,7 +1,7 @@
|
||||
//! Read-only active-repository discovery and isolated revision comparison.
|
||||
use crate::diff::{
|
||||
Catalog, Category, ChangedFile, Comparison, FileStatus, Hunk, RepoKind, Revision,
|
||||
RevisionTarget,
|
||||
Catalog, Category, ChangedFile, Comparison, FileStatus, RepoKind, Revision, RevisionTarget,
|
||||
line_hunks,
|
||||
};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
@@ -934,7 +934,7 @@ impl Repository {
|
||||
}
|
||||
Ok(snapshot)
|
||||
}
|
||||
fn working_snapshot(&self) -> Result<Snapshot, String> {
|
||||
fn working_snapshot(&self, buffers: &[(PathBuf, String)]) -> Result<Snapshot, String> {
|
||||
let mut tracked = BTreeSet::new();
|
||||
let mut submodules = BTreeSet::new();
|
||||
if self.kind == RepoKind::Git {
|
||||
@@ -1017,6 +1017,32 @@ impl Repository {
|
||||
}
|
||||
}
|
||||
}
|
||||
let overlays: BTreeMap<_, _> = buffers
|
||||
.iter()
|
||||
.filter_map(|(path, text)| {
|
||||
let relative = path.strip_prefix(&self.root).ok()?;
|
||||
if safe_path(relative).is_err()
|
||||
|| !path.is_absolute()
|
||||
|| text.as_bytes().contains(&0)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// Reject symlinks in every component, not just the final file.
|
||||
let mut full = self.root.clone();
|
||||
for component in relative.components() {
|
||||
full.push(component);
|
||||
let metadata = fs::symlink_metadata(&full).ok()?;
|
||||
if metadata.file_type().is_symlink() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
fs::symlink_metadata(&full)
|
||||
.ok()?
|
||||
.is_file()
|
||||
.then_some((relative.to_path_buf(), text))
|
||||
})
|
||||
.collect();
|
||||
paths.extend(overlays.keys().cloned());
|
||||
let mut total = 0;
|
||||
let mut snapshot = BTreeMap::new();
|
||||
for path in paths {
|
||||
@@ -1110,6 +1136,20 @@ impl Repository {
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
let bytes = if !bytes.contains(&0)
|
||||
&& std::str::from_utf8(&bytes).is_ok()
|
||||
&& let Some(text) = overlays.get(&path)
|
||||
{
|
||||
if text.len() > FILE_LIMIT {
|
||||
return Err(format!(
|
||||
"{} exceeds the 16 MiB comparison file limit",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
text.as_bytes().to_vec()
|
||||
} else {
|
||||
bytes
|
||||
};
|
||||
#[cfg(unix)]
|
||||
let executable = {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
@@ -1146,16 +1186,24 @@ impl Repository {
|
||||
}
|
||||
fn snapshot(&mut self, target: &RevisionTarget) -> Result<Snapshot, String> {
|
||||
if *target == RevisionTarget::Working {
|
||||
self.working_snapshot()
|
||||
self.working_snapshot(&[])
|
||||
} else {
|
||||
let (store, id) = self.resolve(target)?;
|
||||
self.commit_snapshot(&store, &id)
|
||||
}
|
||||
}
|
||||
pub fn compare(&mut self, before: &Revision, after: &Revision) -> Result<Comparison, String> {
|
||||
pub fn compare(
|
||||
&mut self,
|
||||
before: &Revision,
|
||||
after: &Revision,
|
||||
buffers: &[(PathBuf, String)],
|
||||
) -> Result<Comparison, String> {
|
||||
let left = self.snapshot(&before.target)?;
|
||||
let right = if before.target == after.target {
|
||||
let working_after = after.target == RevisionTarget::Working;
|
||||
let right = if before.target == after.target && (!working_after || buffers.is_empty()) {
|
||||
None
|
||||
} else if working_after {
|
||||
Some(self.working_snapshot(buffers)?)
|
||||
} else {
|
||||
Some(self.snapshot(&after.target)?)
|
||||
};
|
||||
@@ -1199,15 +1247,36 @@ impl Repository {
|
||||
removed.insert(j);
|
||||
}
|
||||
}
|
||||
let files = files
|
||||
let files: Vec<_> = files
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, f)| (!removed.contains(&i)).then_some(f))
|
||||
.collect();
|
||||
let editable_paths = if working_after {
|
||||
files
|
||||
.iter()
|
||||
.filter(|file| {
|
||||
right.get(&file.path).is_some_and(|data| {
|
||||
matches!(data.mode.as_str(), "100644" | "100755")
|
||||
&& !data.bytes.contains(&0)
|
||||
&& std::str::from_utf8(&data.bytes).is_ok()
|
||||
&& file
|
||||
.notice
|
||||
.as_deref()
|
||||
.is_none_or(|notice| !notice.contains("Binary or non-UTF-8"))
|
||||
})
|
||||
})
|
||||
.map(|file| file.path.clone())
|
||||
.collect()
|
||||
} else {
|
||||
BTreeSet::new()
|
||||
};
|
||||
Ok(Comparison {
|
||||
before: before.label.clone(),
|
||||
after: after.label.clone(),
|
||||
files,
|
||||
root: self.root.clone(),
|
||||
editable_paths,
|
||||
})
|
||||
}
|
||||
fn changed_file(
|
||||
@@ -1253,38 +1322,15 @@ impl Repository {
|
||||
eof_label(after)
|
||||
));
|
||||
}
|
||||
let before_path = self.temporary.0.join("before");
|
||||
let after_path = self.temporary.0.join("after");
|
||||
fs::write(&before_path, a).map_err(|e| e.to_string())?;
|
||||
fs::write(&after_path, b).map_err(|e| e.to_string())?;
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.args([
|
||||
"--no-pager",
|
||||
"diff",
|
||||
"--no-index",
|
||||
"--no-color",
|
||||
"--no-ext-diff",
|
||||
"--no-textconv",
|
||||
"--text",
|
||||
"--unified=0",
|
||||
"--",
|
||||
])
|
||||
.arg(&before_path)
|
||||
.arg(&after_path)
|
||||
.current_dir(&self.temporary.0);
|
||||
let output = run(cmd, vec![], &self.cancelled)?;
|
||||
if !matches!(output.code, Some(0 | 1)) {
|
||||
return Err(format!(
|
||||
"Cannot calculate diff hunks: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
let patch = text(output.stdout)?;
|
||||
let mut hunks = vec![];
|
||||
for line in patch.lines().filter(|l| l.starts_with("@@ ")) {
|
||||
hunks.push(parse_hunk(line)?);
|
||||
}
|
||||
(lines(a), lines(b), hunks)
|
||||
let before_lines = lines(a);
|
||||
let after_lines = lines(b);
|
||||
let hunks = line_hunks(
|
||||
&before_lines,
|
||||
a.ends_with(b"\n"),
|
||||
&after_lines,
|
||||
b.ends_with(b"\n"),
|
||||
);
|
||||
(before_lines, after_lines, hunks)
|
||||
};
|
||||
notices.dedup();
|
||||
Ok(ChangedFile {
|
||||
@@ -1293,6 +1339,8 @@ impl Repository {
|
||||
status,
|
||||
before: before_lines,
|
||||
after: after_lines,
|
||||
before_eof: a.ends_with(b"\n"),
|
||||
after_eof: b.ends_with(b"\n"),
|
||||
hunks,
|
||||
notice: if notices.is_empty() {
|
||||
None
|
||||
@@ -1318,35 +1366,6 @@ fn eof_label(file: Option<&FileData>) -> &'static str {
|
||||
Some(_) => "missing",
|
||||
}
|
||||
}
|
||||
fn parse_hunk(line: &str) -> Result<Hunk, String> {
|
||||
fn range(value: &str) -> Result<(usize, usize), String> {
|
||||
let (start, len) = value[1..].split_once(',').unwrap_or((&value[1..], "1"));
|
||||
let start: usize = start.parse().map_err(|_| "Invalid diff hunk start")?;
|
||||
let len: usize = len.parse().map_err(|_| "Invalid diff hunk length")?;
|
||||
Ok((
|
||||
if len == 0 {
|
||||
start
|
||||
} else {
|
||||
start
|
||||
.checked_sub(1)
|
||||
.ok_or("Invalid nonempty diff hunk start")?
|
||||
},
|
||||
len,
|
||||
))
|
||||
}
|
||||
let mut fields = line.split_whitespace();
|
||||
if fields.next() != Some("@@") {
|
||||
return Err("Invalid diff hunk header".into());
|
||||
}
|
||||
let (before_start, before_len) = range(fields.next().ok_or("Missing Before hunk range")?)?;
|
||||
let (after_start, after_len) = range(fields.next().ok_or("Missing After hunk range")?)?;
|
||||
Ok(Hunk {
|
||||
before_start,
|
||||
before_len,
|
||||
after_start,
|
||||
after_len,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
mod tests {
|
||||
|
||||
Reference in New Issue
Block a user