mod binary;
mod drivers;
mod function;
mod hunks;
use std::io;
use std::sync::atomic::{AtomicBool, Ordering};
pub use drivers::DiffAttributes;
use drivers::{Content, Driver};
pub use function::{FunctionLines, PatternError};
pub use hunks::{write_hunks, write_hunks_with};
use crate::content_diff::{
BinaryMode, BlobContent, ContentDiff, ContentReadError, DiffError, DiffLimits,
diff_with_options,
};
use crate::display::quote_path;
use crate::rewrites::Kind;
use crate::tree_diff::Entry;
use crate::{
ChangeStatus, EntryMode, ObjectId, ObjectKind, 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,
pub binary_patch: bool,
pub indent_heuristic: bool,
pub external: External,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum External {
#[default]
Refuse,
Ignore,
}
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_indent_heuristic(mut self, enabled: bool) -> Self {
self.indent_heuristic = enabled;
self
}
#[must_use]
pub fn with_binary(mut self, binary: BinaryMode) -> Self {
self.binary = binary;
self
}
#[must_use]
pub fn with_binary_patch(mut self, enabled: bool) -> Self {
self.binary_patch = enabled;
self
}
#[must_use]
pub fn with_external(mut self, external: External) -> Self {
self.external = external;
self
}
}
impl Default for Options {
fn default() -> Self {
Self {
context_lines: 3,
abbrev_len: None,
binary: BinaryMode::Auto,
binary_patch: false,
indent_heuristic: true,
external: External::Refuse,
}
}
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct Limits {
pub read: ReadLimits,
pub diff: DiffLimits,
pub binary_delta_work: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
read: ReadLimits::default(),
diff: DiffLimits::default(),
binary_delta_work: 64 * 1024 * 1024,
}
}
}
impl Limits {
#[must_use]
pub fn with_binary_delta_work(mut self, binary_delta_work: usize) -> Self {
self.binary_delta_work = binary_delta_work;
self
}
#[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("patch output exceeds byte limit")]
OutputLimit,
#[error("collecting patch statistics")]
Stat(#[from] crate::diffstat::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,
#[error("reading attributes of {}", quote_path(.path))]
Attributes {
path: Vec<u8>,
#[source]
source: Box<crate::attributes::LoadError>,
},
#[error("compiling the function pattern of diff driver {}", String::from_utf8_lossy(.driver))]
Function {
driver: Vec<u8>,
#[source]
source: PatternError,
},
#[error(
"diff driver {} of {} configures {kind}, which girt does not run",
String::from_utf8_lossy(.driver),
quote_path(.path)
)]
External {
path: Vec<u8>,
driver: Vec<u8>,
kind: &'static str,
},
#[error("patch needs objects the repository does not store")]
MissingObjects(#[source] crate::MissingObjectsError),
}
impl Objects {
pub fn write_patch(&self, out: impl io::Write, entries: &[Entry]) -> Result<(), Error> {
self.write_patch_with(
out,
entries,
Options::default(),
None,
Limits::default(),
&AtomicBool::new(false),
)
}
pub fn write_patch_with(
&self,
mut out: impl io::Write,
entries: &[Entry],
options: Options,
mut attributes: Option<&mut DiffAttributes>,
limits: Limits,
cancel: &AtomicBool,
) -> Result<(), Error> {
require_patch_blobs(self, entries, attributes.as_deref_mut())?;
let mut renderer = Renderer {
objects: self,
out: &mut out,
options,
attributes,
stats: None,
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(())
}
}
fn require_patch_blobs(
objects: &Objects,
entries: &[Entry],
attributes: Option<&mut DiffAttributes>,
) -> Result<(), Error> {
if !objects.partial_clone {
return Ok(());
}
let mut seen = crate::IdSet::default();
let mut missing = Vec::new();
for entry in entries {
let (old, new, type_changed) = match entry {
Entry::Change(change) => (
change.old,
change.new,
change.status() == ChangeStatus::TypeChanged,
),
Entry::Rewrite { old, new, .. } => (Some(*old), Some(*new), false),
};
if !type_changed && old.map(|value| value.id) == new.map(|value| value.id) {
continue;
}
for value in [old, new].into_iter().flatten() {
if matches!(value.mode, EntryMode::Gitlink | EntryMode::Tree) || !seen.insert(value.id)
{
continue;
}
let stored = objects.contains(value.id).map_err(|source| Error::Read {
path: entry_path(entry).to_vec(),
source: Box::new(ContentReadError::Read {
id: value.id,
source,
}),
})?;
if !stored {
missing.push((value.id, ObjectKind::Blob));
}
}
}
if let Some(attributes) = attributes {
let paths = entries.iter().flat_map(|entry| match entry {
Entry::Change(change) => [Some(change.path.as_slice()), None],
Entry::Rewrite { source, target, .. } => [Some(source.as_slice()), Some(target)],
});
let blobs = attributes
.missing_blobs(paths.flatten())
.map_err(|source| Error::Attributes {
path: Vec::new(),
source: Box::new(source),
})?;
for id in blobs {
if seen.insert(id) {
missing.push((id, ObjectKind::Blob));
}
}
}
if missing.is_empty() {
Ok(())
} else {
Err(Error::MissingObjects(crate::MissingObjectsError::new(
missing,
)))
}
}
fn entry_path(entry: &Entry) -> &[u8] {
match entry {
Entry::Change(change) => &change.path,
Entry::Rewrite { target, .. } => target,
}
}
struct Rewrite {
kind: Kind,
similarity: u8,
}
struct Renderer<'a, W> {
objects: &'a Objects,
out: &'a mut W,
options: Options,
attributes: Option<&'a mut DiffAttributes>,
stats: Option<Vec<crate::diffstat::FileStat>>,
limits: Limits,
abbrev_len: usize,
cancel: &'a AtomicBool,
}
impl<W: io::Write> Renderer<'_, W> {
fn entry(&mut self, entry: &Entry) -> Result<(), Error> {
let (path, source, summary) = match entry {
Entry::Change(change) => {
let summary = match (change.old, change.new) {
(Some(old), Some(new)) if change.status() == ChangeStatus::TypeChanged => {
let path = &change.path;
let removed = self.file((path, path), Some(old), None, None)?;
let added = self.file((path, path), None, Some(new), None)?;
removed.combine(added)?
}
(old, new) => self.file((&change.path, &change.path), old, new, None)?,
};
(&change.path, None, summary)
}
Entry::Rewrite {
kind,
similarity,
source,
target,
old,
new,
} => {
let summary = self.file(
(source, target),
Some(*old),
Some(*new),
Some(Rewrite {
kind: *kind,
similarity: *similarity,
}),
)?;
(target, Some(source), summary)
}
};
if let Some(stats) = &mut self.stats {
stats.push(crate::diffstat::FileStat {
path: path.clone(),
source_path: source.cloned(),
counts: summary.counts(),
});
}
Ok(())
}
fn file(
&mut self,
(old_path, new_path): (&[u8], &[u8]),
old: Option<TreeValue>,
new: Option<TreeValue>,
rewrite: Option<Rewrite>,
) -> Result<FileSummary, Error> {
let old_driver = self.driver(old_path, old)?;
let new_driver = self.driver(new_path, new)?;
if self.options.external == External::Refuse
&& let Some(driver) = [&old_driver, &new_driver]
.into_iter()
.find(|driver| driver.external.is_some())
{
return Err(Error::External {
path: new_path.to_vec(),
driver: driver.name.clone().unwrap_or_default(),
kind: driver.external.unwrap_or_default(),
});
}
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(FileSummary::default());
}
let content = self.read(new_path, old, new)?;
let binary = match (self.options.binary, old_driver.content, new_driver.content) {
(BinaryMode::Auto, Content::Auto, Content::Auto) | (BinaryMode::Text, ..) => {
self.options.binary
}
(BinaryMode::Auto, old_content, new_content) => {
let binary = |content: Content, bytes: &[u8]| match content {
Content::Auto => bytes.contains(&0),
Content::Text => false,
Content::Binary => true,
};
if binary(old_content, &content.old) || binary(new_content, &content.new) {
BinaryMode::Binary
} else {
BinaryMode::Text
}
}
(mode, ..) => mode,
};
let content_diff = diff_with_options(
&content.old,
&content.new,
crate::content_diff::Options::default()
.with_binary(binary)
.with_indent_heuristic(self.options.indent_heuristic),
self.limits.diff,
self.cancel,
)
.map_err(|source| match source {
DiffError::Cancelled => Error::Cancelled,
source => Error::Diff {
path: new_path.to_vec(),
source,
},
})?;
let summary = FileSummary::new(&content_diff, content.old.len(), content.new.len());
let full = self.options.binary_patch && matches!(content_diff, ContentDiff::Binary { .. });
let old_abbrev = self.abbreviate(old_id, full)?;
let new_abbrev = self.abbreviate(new_id, full)?;
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 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 { .. } => {
if self.options.binary_patch {
binary::write(
out,
&content.old,
&content.new,
self.limits.binary_delta_work,
self.cancel,
)?;
} else {
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))?;
let function = old_driver
.function
.as_ref()
.or(new_driver.function.as_ref());
let default = FunctionLines::default();
write_hunks_with(
out,
&text,
self.options.context_lines,
function.unwrap_or(&default),
)?;
}
}
Ok(summary)
}
fn driver(&mut self, path: &[u8], value: Option<TreeValue>) -> Result<Driver, Error> {
match (&mut self.attributes, value) {
(Some(attributes), Some(value)) if value.mode != EntryMode::Gitlink => {
attributes.driver(path)
}
_ => Ok(Driver::default()),
}
}
fn abbreviate(&self, id: Option<ObjectId>, full: bool) -> Result<String, Error> {
if full {
return Ok(id.map_or_else(
|| "0".repeat(self.objects.object_format().digest_len() * 2),
|id| id.to_string(),
));
}
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;
#[derive(Default)]
struct FileSummary {
added: u64,
removed: u64,
old_bytes: u64,
new_bytes: u64,
binary: bool,
}
impl FileSummary {
fn new(diff: &ContentDiff<'_>, old: usize, new: usize) -> Self {
let mut summary = Self {
old_bytes: old as u64,
new_bytes: new as u64,
..Self::default()
};
match diff {
ContentDiff::Binary { .. } => summary.binary = true,
ContentDiff::Text(text) => {
summary.added = text
.edits()
.iter()
.map(|edit| edit.new_lines.len() as u64)
.sum();
summary.removed = text
.edits()
.iter()
.map(|edit| edit.old_lines.len() as u64)
.sum();
}
ContentDiff::Unchanged => {}
}
summary
}
fn combine(self, other: Self) -> Result<Self, Error> {
let sum = |a: u64, b: u64| {
a.checked_add(b)
.ok_or(Error::Stat(crate::diffstat::Error::Limit("counts")))
};
Ok(Self {
added: sum(self.added, other.added)?,
removed: sum(self.removed, other.removed)?,
old_bytes: sum(self.old_bytes, other.old_bytes)?,
new_bytes: sum(self.new_bytes, other.new_bytes)?,
binary: self.binary || other.binary,
})
}
fn counts(self) -> crate::diffstat::Counts {
if self.binary {
crate::diffstat::Counts::Binary {
old_bytes: self.old_bytes,
new_bytes: self.new_bytes,
}
} else {
crate::diffstat::Counts::Text {
added: self.added,
removed: self.removed,
}
}
}
}
pub(crate) struct MailBounds {
pub patch: Limits,
pub stat: crate::diffstat::Limits,
pub output: Option<usize>,
}
pub(crate) struct MailPatch {
pub patch: Vec<u8>,
pub stat: crate::diffstat::Diffstat,
pub summary: Vec<u8>,
}
pub(crate) fn mail_patch(
objects: &Objects,
entries: &[Entry],
options: Options,
mut attributes: Option<&mut DiffAttributes>,
bounds: MailBounds,
cancel: &AtomicBool,
) -> Result<MailPatch, Error> {
if cancel.load(Ordering::Relaxed) {
return Err(Error::Cancelled);
}
if entries.len() > bounds.stat.max_files {
return Err(crate::diffstat::Error::Limit("files").into());
}
let mut path_bytes = 0usize;
for entry in entries {
let paths = match entry {
Entry::Change(change) => (change.path.as_slice(), &[][..]),
Entry::Rewrite { source, target, .. } => (source.as_slice(), target.as_slice()),
};
path_bytes = path_bytes
.checked_add(paths.0.len())
.and_then(|n| n.checked_add(paths.1.len()))
.ok_or(crate::diffstat::Error::Limit("path bytes"))?;
if path_bytes > bounds.stat.max_path_bytes {
return Err(crate::diffstat::Error::Limit("path bytes").into());
}
}
let mut output = BoundedOutput {
bytes: Vec::new(),
limit: bounds.output,
exceeded: false,
};
require_patch_blobs(objects, entries, attributes.as_deref_mut())?;
let result = {
let mut renderer = Renderer {
objects,
out: &mut output,
options,
attributes,
stats: Some(Vec::new()),
limits: bounds.patch,
abbrev_len: options
.abbrev_len
.unwrap_or_else(|| objects.auto_abbrev_len()),
cancel,
};
let result = entries.iter().try_for_each(|entry| {
if cancel.load(Ordering::Relaxed) {
return Err(Error::Cancelled);
}
renderer.entry(entry)
});
result.map(|()| renderer.stats.take().unwrap_or_default())
};
if output.exceeded {
return Err(Error::OutputLimit);
}
let records = result?;
let summary_limit = bounds
.output
.map(|limit| limit.saturating_sub(output.bytes.len()));
let summary = change_summary(entries, &records, summary_limit)?;
let stat = crate::diffstat::Diffstat::new(records, bounds.stat)?;
Ok(MailPatch {
patch: output.bytes,
stat,
summary,
})
}
struct BoundedOutput {
bytes: Vec<u8>,
limit: Option<usize>,
exceeded: bool,
}
impl io::Write for BoundedOutput {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
let Some(limit) = self.limit else {
return Ok(bytes.len());
};
if self
.bytes
.len()
.checked_add(bytes.len())
.is_none_or(|size| size > limit)
{
self.exceeded = true;
return Err(io::Error::new(
io::ErrorKind::FileTooLarge,
"patch output bound",
));
}
self.bytes.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn change_summary(
entries: &[Entry],
records: &[crate::diffstat::FileStat],
limit: Option<usize>,
) -> Result<Vec<u8>, Error> {
use io::Write as _;
let Some(limit) = limit else {
return Ok(Vec::new());
};
let mut out = BoundedOutput {
bytes: Vec::new(),
limit: Some(limit),
exceeded: false,
};
let result = (|| -> io::Result<()> {
for (entry, record) in entries.iter().zip(records) {
match entry {
Entry::Change(change) => match (change.old, change.new) {
(None, Some(new)) => writeln!(
out,
" create mode {} {}",
new.mode.octal(),
quote_path(&change.path)
)?,
(Some(old), None) => writeln!(
out,
" delete mode {} {}",
old.mode.octal(),
quote_path(&change.path)
)?,
(Some(old), Some(new)) if old.mode != new.mode => writeln!(
out,
" mode change {} => {} {}",
old.mode.octal(),
new.mode.octal(),
quote_path(&change.path)
)?,
_ => {}
},
Entry::Rewrite {
kind,
similarity,
old,
new,
..
} => {
let verb = match kind {
Kind::Rename => "rename",
Kind::Copy => "copy",
};
writeln!(
out,
" {verb} {} ({similarity}%)",
crate::diffstat::text_path(record)
)?;
if old.mode != new.mode {
writeln!(
out,
" mode change {} => {}",
old.mode.octal(),
new.mode.octal()
)?;
}
}
}
}
Ok(())
})();
if out.exceeded {
return Err(Error::OutputLimit);
}
result?;
Ok(out.bytes)
}