mod hunks;
use std::io;
use std::sync::atomic::{AtomicBool, Ordering};
pub use hunks::write_hunks;
use crate::content_diff::{
BinaryMode, BlobContent, ContentDiff, ContentReadError, DiffError, DiffLimits, diff,
};
use crate::display::quote_path;
use crate::rewrites::Kind;
use crate::tree_diff::Entry;
use crate::{
ChangeStatus, EntryMode, ObjectId, ObjectReadError, Objects, ReadLimits, TreeChange, TreeValue,
};
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct Options {
pub context_lines: usize,
pub abbrev_len: Option<usize>,
pub binary: BinaryMode,
}
impl Options {
#[must_use]
pub fn with_context_lines(mut self, context_lines: usize) -> Self {
self.context_lines = context_lines;
self
}
#[must_use]
pub fn with_abbrev_len(mut self, abbrev_len: Option<usize>) -> Self {
self.abbrev_len = abbrev_len;
self
}
#[must_use]
pub fn with_binary(mut self, binary: BinaryMode) -> Self {
self.binary = binary;
self
}
}
impl Default for Options {
fn default() -> Self {
Self {
context_lines: 3,
abbrev_len: None,
binary: BinaryMode::Auto,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
#[non_exhaustive]
pub struct Limits {
pub read: ReadLimits,
pub diff: DiffLimits,
}
impl Limits {
#[must_use]
pub fn with_read(mut self, read: ReadLimits) -> Self {
self.read = read;
self
}
#[must_use]
pub fn with_diff(mut self, diff: DiffLimits) -> Self {
self.diff = diff;
self
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("writing patch")]
Write(#[from] io::Error),
#[error("reading content of {}", quote_path(.path))]
Read {
path: Vec<u8>,
#[source]
source: Box<ContentReadError>,
},
#[error("comparing content of {}", quote_path(.path))]
Diff {
path: Vec<u8>,
#[source]
source: DiffError,
},
#[error("abbreviating {id}")]
Abbreviate {
id: ObjectId,
#[source]
source: Box<ObjectReadError>,
},
#[error("patch rendering cancelled")]
Cancelled,
}
impl Objects {
pub fn write_patch(&self, out: impl io::Write, entries: &[Entry]) -> Result<(), Error> {
self.write_patch_with(
out,
entries,
Options::default(),
Limits::default(),
&AtomicBool::new(false),
)
}
pub fn write_patch_with(
&self,
mut out: impl io::Write,
entries: &[Entry],
options: Options,
limits: Limits,
cancel: &AtomicBool,
) -> Result<(), Error> {
let mut renderer = Renderer {
objects: self,
out: &mut out,
options,
limits,
abbrev_len: options.abbrev_len.unwrap_or_else(|| self.auto_abbrev_len()),
cancel,
};
for entry in entries {
if cancel.load(Ordering::Relaxed) {
return Err(Error::Cancelled);
}
renderer.entry(entry)?;
}
Ok(())
}
}
struct Rewrite {
kind: Kind,
similarity: u8,
}
struct Renderer<'a, W> {
objects: &'a Objects,
out: &'a mut W,
options: Options,
limits: Limits,
abbrev_len: usize,
cancel: &'a AtomicBool,
}
impl<W: io::Write> Renderer<'_, W> {
fn entry(&mut self, entry: &Entry) -> Result<(), Error> {
match entry {
Entry::Change(change) => match (change.old, change.new) {
(Some(old), Some(new)) if change.status() == ChangeStatus::TypeChanged => {
let path = &change.path;
self.file((path, path), Some(old), None, None)?;
self.file((path, path), None, Some(new), None)
}
(old, new) => self.file((&change.path, &change.path), old, new, None),
},
Entry::Rewrite {
kind,
similarity,
source,
target,
old,
new,
} => self.file(
(source, target),
Some(*old),
Some(*new),
Some(Rewrite {
kind: *kind,
similarity: *similarity,
}),
),
}
}
fn file(
&mut self,
(old_path, new_path): (&[u8], &[u8]),
old: Option<TreeValue>,
new: Option<TreeValue>,
rewrite: Option<Rewrite>,
) -> Result<(), Error> {
let old_name = quote_path(&[b"a/", old_path].concat()).to_string();
let new_name = quote_path(&[b"b/", new_path].concat()).to_string();
let out = &mut *self.out;
writeln!(out, "diff --git {old_name} {new_name}")?;
match (old, new) {
(None, Some(new)) => writeln!(out, "new file mode {}", new.mode.octal())?,
(Some(old), None) => writeln!(out, "deleted file mode {}", old.mode.octal())?,
(Some(old), Some(new)) if old.mode != new.mode => writeln!(
out,
"old mode {}\nnew mode {}",
old.mode.octal(),
new.mode.octal()
)?,
_ => {}
}
if let Some(Rewrite { kind, similarity }) = rewrite {
let verb = match kind {
Kind::Rename => "rename",
Kind::Copy => "copy",
};
writeln!(out, "similarity index {similarity}%")?;
writeln!(out, "{verb} from {}", quote_path(old_path))?;
writeln!(out, "{verb} to {}", quote_path(new_path))?;
}
let (old_id, new_id) = (old.map(|value| value.id), new.map(|value| value.id));
if old_id == new_id {
return Ok(());
}
let old_abbrev = self.abbreviate(old_id)?;
let new_abbrev = self.abbreviate(new_id)?;
let out = &mut *self.out;
write!(out, "index {old_abbrev}..{new_abbrev}")?;
match (old, new) {
(Some(old), Some(new)) if old.mode == new.mode => {
writeln!(out, " {}", new.mode.octal())?;
}
_ => writeln!(out)?,
}
let content = self.read(new_path, old, new)?;
let content_diff = diff(
&content.old,
&content.new,
self.options.binary,
self.limits.diff,
self.cancel,
)
.map_err(|source| match source {
DiffError::Cancelled => Error::Cancelled,
source => Error::Diff {
path: new_path.to_vec(),
source,
},
})?;
let old_name = if old.is_some() {
&old_name
} else {
"/dev/null"
};
let new_name = if new.is_some() {
&new_name
} else {
"/dev/null"
};
let out = &mut *self.out;
match content_diff {
ContentDiff::Unchanged => {}
ContentDiff::Binary { .. } => {
writeln!(out, "Binary files {old_name} and {new_name} differ")?;
}
ContentDiff::Text(text) => {
let tab = |name: &str| if name.contains(' ') { "\t" } else { "" };
writeln!(out, "--- {old_name}{}", tab(old_name))?;
writeln!(out, "+++ {new_name}{}", tab(new_name))?;
write_hunks(out, &text, self.options.context_lines)?;
}
}
Ok(())
}
fn abbreviate(&self, id: Option<ObjectId>) -> Result<String, Error> {
match id {
Some(id) => self
.objects
.abbreviate(id, self.abbrev_len)
.map(|prefix| prefix.to_string())
.map_err(|source| Error::Abbreviate {
id,
source: Box::new(source),
}),
None => {
let width = self.objects.object_format().digest_len() * 2;
Ok("0".repeat(self.abbrev_len.clamp(4, width)))
}
}
}
fn read(
&self,
path: &[u8],
old: Option<TreeValue>,
new: Option<TreeValue>,
) -> Result<BlobContent, Error> {
let blobs = TreeChange {
path: path.to_vec(),
old: old.filter(|value| value.mode != EntryMode::Gitlink),
new: new.filter(|value| value.mode != EntryMode::Gitlink),
};
let mut content = BlobContent::read(
self.objects,
&blobs,
self.limits.read,
self.limits.diff.max_input_bytes,
self.cancel,
)
.map_err(|source| match source {
ContentReadError::Cancelled => Error::Cancelled,
source => Error::Read {
path: path.to_vec(),
source: Box::new(source),
},
})?;
let subproject = |value: Option<TreeValue>| {
value
.filter(|value| value.mode == EntryMode::Gitlink)
.map(|value| format!("Subproject commit {}\n", value.id).into_bytes())
};
if let Some(line) = subproject(old) {
content.old = line;
}
if let Some(line) = subproject(new) {
content.new = line;
}
Ok(content)
}
}
#[cfg(test)]
mod tests;