Files
ELYTerminal/src/git/diff.rs
T

108 lines
4.1 KiB
Rust

use std::{ffi::OsStr, path::Path};
use anyhow::{Context as _, bail, ensure};
use ely_gpui_component::git::{ChangeSection, DiffLine, LineKind, Stretch, mark_words};
use super::{Change, git, line, said, warnings};
/// `change`'s diff as `section` holds it, in Ely's stretches; none for a binary file.
pub fn diff(
top: &Path,
change: &Change,
section: ChangeSection,
) -> anyhow::Result<Option<Vec<Stretch>>> {
let (path, shown) = (change.path.as_os_str(), change.path.display());
let flags: &[&str] = match section {
ChangeSection::Staged => &["--cached", "-M", "--"],
ChangeSection::Unstaged => &["--"],
// HEAD is our side: a file we deleted reads as new.
ChangeSection::Conflicted => &["HEAD", "--"],
ChangeSection::Untracked if path.as_encoded_bytes().ends_with(b"/") => {
bail!("git: {shown} is a folder")
}
ChangeSection::Untracked => &["--no-index", "--", "/dev/null"],
ChangeSection::Ignored => bail!("git: {shown} is ignored"),
};
let mut args: Vec<&OsStr> = ["diff", "--no-color", "--no-ext-diff", "--no-textconv"]
.into_iter()
.chain(flags.iter().copied())
.map(OsStr::new)
.collect();
args.push(path);
args.extend(change.from.as_deref().map(Path::as_os_str));
let output = git(top, &args, &[])?;
// Without an index git exits 1 for a difference and a failure alike; only a difference prints.
let differ = section == ChangeSection::Untracked
&& output.status.code() == Some(1)
&& !output.stdout.is_empty();
ensure!(
output.status.success() || differ,
"git: `git {}` failed in {}: {}",
line(&args),
top.display(),
said(&output)
);
warnings(top, &args, &output);
hunks(&String::from_utf8_lossy(&output.stdout))
}
/// A hunk header's first old and new line numbers.
fn starts(header: &str) -> Option<(usize, usize)> {
let mut ranges = header.strip_prefix("@@ -")?.split(' ');
let first = |range: &str| range.split(',').next()?.parse().ok();
let old = first(ranges.next()?)?;
let new = first(ranges.next()?.strip_prefix('+')?)?;
Some((old, new))
}
/// Git's unified diff as Ely's stretches, changed words marked; none for a binary file.
pub(super) fn hunks(text: &str) -> anyhow::Result<Option<Vec<Stretch>>> {
let (mut stretches, mut at, mut inside) = (Vec::new(), (0, 0), false);
for line in text.lines() {
if line.starts_with("diff ") {
inside = false;
} else if line.starts_with("@@") {
at = starts(line).with_context(|| format!("git: hunk header {line:?}"))?;
let header = line.to_string().into();
stretches.push(Stretch::Hunk {
header,
lines: Vec::new(),
});
inside = true;
} else if !inside {
if line.starts_with("Binary files ") {
return Ok(None);
}
} else if let Some(Stretch::Hunk { lines, .. }) = stretches.last_mut() {
let (kind, text) = match line.as_bytes().first() {
// `diff.suppressBlankEmpty` drops a blank line's space.
None => (LineKind::Same, ""),
Some(b' ') => (LineKind::Same, &line[1..]),
Some(b'-') => (LineKind::Removed, &line[1..]),
Some(b'+') => (LineKind::Added, &line[1..]),
Some(b'\\') => continue,
Some(_) => bail!("git: diff line {line:?} is malformed"),
};
let old = (kind != LineKind::Added).then_some(at.0);
let new = (kind != LineKind::Removed).then_some(at.1);
at = (
at.0 + usize::from(old.is_some()),
at.1 + usize::from(new.is_some()),
);
lines.push(DiffLine {
kind,
old,
new,
text: text.to_string().into(),
words: Vec::new(),
});
}
}
for stretch in &mut stretches {
if let Stretch::Hunk { lines, .. } = stretch {
mark_words(lines);
}
}
Ok(Some(stretches))
}