use crate::cli::UI;
use anyhow::Result;
use git2::DiffOptions;
use std::path::Path;
pub struct DiffDisplayOptions {
pub cached: bool,
pub stat_only: bool,
pub name_only: bool,
pub name_status: bool,
pub commit_spec: Option<String>,
pub paths: Vec<String>,
pub ignore_whitespace: bool,
}
pub fn execute(path: &Path, opts: &DiffDisplayOptions, _ui: &UI) -> Result<()> {
let repo = crate::ops::open_repo(path)?;
let mut diff_opts = DiffOptions::new();
if opts.ignore_whitespace {
diff_opts.ignore_whitespace(true);
}
for p in &opts.paths {
diff_opts.pathspec(p);
}
let diff = if let Some(spec) = opts.commit_spec.as_deref() {
if spec.contains("...") {
let parts: Vec<&str> = spec.splitn(2, "...").collect();
let left = repo.revparse_single(parts[0])?.peel_to_tree()?;
let right = repo.revparse_single(parts[1])?.peel_to_tree()?;
let merge_base_oid = repo.merge_base(
repo.revparse_single(parts[0])?.id(),
repo.revparse_single(parts[1])?.id(),
)?;
let base_tree = repo.find_commit(merge_base_oid)?.tree()?;
let _ = left; repo.diff_tree_to_tree(Some(&base_tree), Some(&right), Some(&mut diff_opts))?
} else if spec.contains("..") {
let parts: Vec<&str> = spec.splitn(2, "..").collect();
let left = repo.revparse_single(parts[0])?.peel_to_tree()?;
let right = repo.revparse_single(parts[1])?.peel_to_tree()?;
repo.diff_tree_to_tree(Some(&left), Some(&right), Some(&mut diff_opts))?
} else {
let tree = repo.revparse_single(spec)?.peel_to_tree()?;
repo.diff_tree_to_workdir(Some(&tree), Some(&mut diff_opts))?
}
} else if opts.cached {
let head_tree = repo.head().ok().and_then(|h| h.peel_to_tree().ok());
repo.diff_tree_to_index(head_tree.as_ref(), None, Some(&mut diff_opts))?
} else {
repo.diff_index_to_workdir(None, Some(&mut diff_opts))?
};
if opts.stat_only {
let stats = diff.stats()?;
let buf = stats.to_buf(git2::DiffStatsFormat::FULL, 80)?;
print!("{}", buf.as_str().unwrap_or(""));
return Ok(());
}
if opts.name_only || opts.name_status {
for delta in diff.deltas() {
let path = delta
.new_file()
.path()
.or_else(|| delta.old_file().path())
.map(|p| p.display().to_string())
.unwrap_or_default();
if opts.name_status {
let status_char = match delta.status() {
git2::Delta::Added => 'A',
git2::Delta::Deleted => 'D',
git2::Delta::Modified => 'M',
git2::Delta::Renamed => 'R',
git2::Delta::Copied => 'C',
git2::Delta::Typechange => 'T',
_ => '?',
};
println!("{}\t{}", status_char, path);
} else {
println!("{}", path);
}
}
return Ok(());
}
display_diff(&diff, _ui)
}
pub fn display_diff(diff: &git2::Diff, _ui: &UI) -> Result<()> {
diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
let prefix = match line.origin() {
'+' => "+",
'-' => "-",
' ' => " ",
_ => "",
};
let content = std::str::from_utf8(line.content()).unwrap_or("");
print!("{}{}", prefix, strip_control_chars(content));
true
})?;
Ok(())
}
pub fn execute_compact(
path: &Path,
cached: bool,
commit_spec: Option<&str>,
paths: &[String],
ignore_whitespace: bool,
) -> Result<String> {
use crate::cli::compact::CompactDiffFormatter;
let repo = crate::ops::open_repo(path)?;
let mut opts = DiffOptions::new();
if ignore_whitespace {
opts.ignore_whitespace(true);
}
for p in paths {
opts.pathspec(p);
}
let diff = if let Some(spec) = commit_spec {
if spec.contains("...") {
let parts: Vec<&str> = spec.splitn(2, "...").collect();
let right = repo.revparse_single(parts[1])?.peel_to_tree()?;
let merge_base_oid = repo.merge_base(
repo.revparse_single(parts[0])?.id(),
repo.revparse_single(parts[1])?.id(),
)?;
let base_tree = repo.find_commit(merge_base_oid)?.tree()?;
repo.diff_tree_to_tree(Some(&base_tree), Some(&right), Some(&mut opts))?
} else if spec.contains("..") {
let parts: Vec<&str> = spec.splitn(2, "..").collect();
let left = repo.revparse_single(parts[0])?.peel_to_tree()?;
let right = repo.revparse_single(parts[1])?.peel_to_tree()?;
repo.diff_tree_to_tree(Some(&left), Some(&right), Some(&mut opts))?
} else {
let tree = repo.revparse_single(spec)?.peel_to_tree()?;
repo.diff_tree_to_workdir(Some(&tree), Some(&mut opts))?
}
} else if cached {
let head_tree = repo.head().ok().and_then(|h| h.peel_to_tree().ok());
repo.diff_tree_to_index(head_tree.as_ref(), None, Some(&mut opts))?
} else {
repo.diff_index_to_workdir(None, Some(&mut opts))?
};
let stats = diff.stats()?;
let stat_buf = stats.to_buf(git2::DiffStatsFormat::SHORT, 80)?;
let mut output = stat_buf.as_str().unwrap_or("").to_string();
let formatter = std::cell::RefCell::new(CompactDiffFormatter::new());
diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
let mut fmt = formatter.borrow_mut();
let content = std::str::from_utf8(line.content()).unwrap_or("");
match line.origin() {
'F' => {
if let Some(path) = _delta
.new_file()
.path()
.or_else(|| _delta.old_file().path())
{
fmt.begin_file(&path.display().to_string());
}
}
'H' => {
fmt.begin_hunk(content);
}
'+' | '-' | ' ' => {
fmt.add_line(line.origin(), content);
}
_ => {}
}
true
})?;
let compact_diff = formatter.into_inner().finish();
output.push_str(&compact_diff);
Ok(output.trim_end().to_string())
}
fn strip_control_chars(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\x1b' {
if let Some(next) = chars.next() {
if next == '[' {
for c2 in chars.by_ref() {
if ('\x40'..='\x7e').contains(&c2) {
break;
}
}
}
}
} else if c == '\n' || c == '\t' || c == '\r' {
result.push(c);
} else if c.is_control() {
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_control_chars_plain() {
assert_eq!(strip_control_chars("hello world\n"), "hello world\n");
}
#[test]
fn test_strip_control_chars_ansi() {
assert_eq!(strip_control_chars("\x1b[31mred\x1b[0m"), "red");
assert_eq!(
strip_control_chars("\x1b[1;32mbold green\x1b[0m"),
"bold green"
);
}
#[test]
fn test_strip_control_chars_bel() {
assert_eq!(strip_control_chars("hello\x07world"), "helloworld");
}
#[test]
fn test_strip_control_chars_preserves_whitespace() {
assert_eq!(strip_control_chars("a\tb\nc\r\n"), "a\tb\nc\r\n");
}
}