use std::{
borrow::Cow,
ops::Range,
path::{Path, PathBuf},
string::FromUtf8Error,
};
#[cfg(feature = "serde")]
use serde::{ser, ser::SerializeStruct, Serialize, Serializer};
use git_ext::Oid;
pub mod git;
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Diff {
files: Vec<FileDiff>,
stats: Stats,
}
impl Diff {
pub(crate) fn new() -> Self {
Diff::default()
}
pub fn files(&self) -> impl Iterator<Item = &FileDiff> {
self.files.iter()
}
pub fn into_files(self) -> Vec<FileDiff> {
self.files
}
pub fn added(&self) -> impl Iterator<Item = &Added> {
self.files().filter_map(|x| match x {
FileDiff::Added(a) => Some(a),
_ => None,
})
}
pub fn deleted(&self) -> impl Iterator<Item = &Deleted> {
self.files().filter_map(|x| match x {
FileDiff::Deleted(a) => Some(a),
_ => None,
})
}
pub fn moved(&self) -> impl Iterator<Item = &Moved> {
self.files().filter_map(|x| match x {
FileDiff::Moved(a) => Some(a),
_ => None,
})
}
pub fn modified(&self) -> impl Iterator<Item = &Modified> {
self.files().filter_map(|x| match x {
FileDiff::Modified(a) => Some(a),
_ => None,
})
}
pub fn copied(&self) -> impl Iterator<Item = &Copied> {
self.files().filter_map(|x| match x {
FileDiff::Copied(a) => Some(a),
_ => None,
})
}
pub fn stats(&self) -> &Stats {
&self.stats
}
fn update_stats(&mut self, diff: &DiffContent) {
self.stats.files_changed += 1;
if let DiffContent::Plain { hunks, .. } = diff {
for h in hunks.iter() {
for l in &h.lines {
match l {
Modification::Addition(_) => self.stats.insertions += 1,
Modification::Deletion(_) => self.stats.deletions += 1,
_ => (),
}
}
}
}
}
pub fn insert_modified(
&mut self,
path: PathBuf,
diff: DiffContent,
old: DiffFile,
new: DiffFile,
) {
self.update_stats(&diff);
let diff = FileDiff::Modified(Modified {
path,
diff,
old,
new,
});
self.files.push(diff);
}
pub fn insert_moved(
&mut self,
old_path: PathBuf,
new_path: PathBuf,
old: DiffFile,
new: DiffFile,
content: DiffContent,
) {
self.update_stats(&DiffContent::Empty);
let diff = FileDiff::Moved(Moved {
old_path,
new_path,
old,
new,
diff: content,
});
self.files.push(diff);
}
pub fn insert_copied(
&mut self,
old_path: PathBuf,
new_path: PathBuf,
old: DiffFile,
new: DiffFile,
content: DiffContent,
) {
self.update_stats(&DiffContent::Empty);
let diff = FileDiff::Copied(Copied {
old_path,
new_path,
old,
new,
diff: content,
});
self.files.push(diff);
}
pub fn insert_added(&mut self, path: PathBuf, diff: DiffContent, new: DiffFile) {
self.update_stats(&diff);
let diff = FileDiff::Added(Added { path, diff, new });
self.files.push(diff);
}
pub fn insert_deleted(&mut self, path: PathBuf, diff: DiffContent, old: DiffFile) {
self.update_stats(&diff);
let diff = FileDiff::Deleted(Deleted { path, diff, old });
self.files.push(diff);
}
}
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Added {
pub path: PathBuf,
pub diff: DiffContent,
pub new: DiffFile,
}
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Deleted {
pub path: PathBuf,
pub diff: DiffContent,
pub old: DiffFile,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Moved {
pub old_path: PathBuf,
pub old: DiffFile,
pub new_path: PathBuf,
pub new: DiffFile,
pub diff: DiffContent,
}
#[cfg(feature = "serde")]
impl Serialize for Moved {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if self.old == self.new {
let mut state = serializer.serialize_struct("Moved", 3)?;
state.serialize_field("oldPath", &self.old_path)?;
state.serialize_field("newPath", &self.new_path)?;
state.serialize_field("current", &self.new)?;
state.end()
} else {
let mut state = serializer.serialize_struct("Moved", 5)?;
state.serialize_field("oldPath", &self.old_path)?;
state.serialize_field("newPath", &self.new_path)?;
state.serialize_field("old", &self.old)?;
state.serialize_field("new", &self.new)?;
state.serialize_field("diff", &self.diff)?;
state.end()
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Copied {
pub old_path: PathBuf,
pub new_path: PathBuf,
pub old: DiffFile,
pub new: DiffFile,
pub diff: DiffContent,
}
#[cfg(feature = "serde")]
impl Serialize for Copied {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if self.old == self.new {
let mut state = serializer.serialize_struct("Copied", 3)?;
state.serialize_field("oldPath", &self.old_path)?;
state.serialize_field("newPath", &self.new_path)?;
state.serialize_field("current", &self.new)?;
state.end()
} else {
let mut state = serializer.serialize_struct("Copied", 5)?;
state.serialize_field("oldPath", &self.old_path)?;
state.serialize_field("newPath", &self.new_path)?;
state.serialize_field("old", &self.old)?;
state.serialize_field("new", &self.new)?;
state.serialize_field("diff", &self.diff)?;
state.end()
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EofNewLine {
OldMissing,
NewMissing,
BothMissing,
NoneMissing,
}
impl Default for EofNewLine {
fn default() -> Self {
Self::NoneMissing
}
}
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Modified {
pub path: PathBuf,
pub diff: DiffContent,
pub old: DiffFile,
pub new: DiffFile,
}
#[cfg_attr(
feature = "serde",
derive(Serialize),
serde(tag = "type", rename_all = "camelCase")
)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DiffContent {
Binary,
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
Plain {
hunks: Hunks<Modification>,
stats: FileStats,
eof: EofNewLine,
},
Empty,
}
impl DiffContent {
pub fn eof(&self) -> Option<EofNewLine> {
match self {
Self::Plain { eof, .. } => Some(eof.clone()),
_ => None,
}
}
pub fn stats(&self) -> Option<&FileStats> {
match &self {
DiffContent::Plain { stats, .. } => Some(stats),
DiffContent::Empty => None,
DiffContent::Binary => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
pub enum FileMode {
Blob,
BlobExecutable,
Tree,
Link,
Commit,
}
impl From<FileMode> for u32 {
fn from(m: FileMode) -> Self {
git2::FileMode::from(m).into()
}
}
impl From<FileMode> for i32 {
fn from(m: FileMode) -> Self {
git2::FileMode::from(m).into()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
pub struct DiffFile {
pub oid: Oid,
pub mode: FileMode,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(Serialize),
serde(tag = "status", rename_all = "camelCase")
)]
pub enum FileDiff {
Added(Added),
Deleted(Deleted),
Modified(Modified),
Moved(Moved),
Copied(Copied),
}
impl FileDiff {
pub fn path(&self) -> &Path {
match self {
FileDiff::Added(x) => x.path.as_path(),
FileDiff::Deleted(x) => x.path.as_path(),
FileDiff::Modified(x) => x.path.as_path(),
FileDiff::Moved(x) => x.new_path.as_path(),
FileDiff::Copied(x) => x.new_path.as_path(),
}
}
}
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct FileStats {
pub additions: usize,
pub deletions: usize,
}
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Stats {
pub files_changed: usize,
pub insertions: usize,
pub deletions: usize,
}
#[cfg_attr(feature = "serde", derive(Serialize), serde(rename_all = "camelCase"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Hunk<T> {
pub header: Line,
pub lines: Vec<T>,
pub old: Range<u32>,
pub new: Range<u32>,
}
#[cfg_attr(feature = "serde", derive(Serialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Hunks<T>(pub Vec<Hunk<T>>);
impl<T> Default for Hunks<T> {
fn default() -> Self {
Self(Default::default())
}
}
impl<T> Hunks<T> {
pub fn iter(&self) -> impl Iterator<Item = &Hunk<T>> {
self.0.iter()
}
}
impl<T> From<Vec<Hunk<T>>> for Hunks<T> {
fn from(hunks: Vec<Hunk<T>>) -> Self {
Self(hunks)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Line(pub(crate) Vec<u8>);
impl Line {
pub fn as_bytes(&self) -> &[u8] {
self.0.as_slice()
}
pub fn from_utf8(self) -> Result<String, FromUtf8Error> {
String::from_utf8(self.0)
}
pub fn from_utf8_lossy(&self) -> Cow<str> {
String::from_utf8_lossy(&self.0)
}
}
impl From<Vec<u8>> for Line {
fn from(v: Vec<u8>) -> Self {
Self(v)
}
}
impl From<String> for Line {
fn from(s: String) -> Self {
Self(s.into_bytes())
}
}
#[cfg(feature = "serde")]
impl Serialize for Line {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = std::str::from_utf8(&self.0).map_err(ser::Error::custom)?;
serializer.serialize_str(s)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Modification {
Addition(Addition),
Deletion(Deletion),
Context {
line: Line,
line_no_old: u32,
line_no_new: u32,
},
}
#[cfg(feature = "serde")]
impl Serialize for Modification {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeMap as _;
match self {
Modification::Addition(addition) => {
let mut map = serializer.serialize_map(Some(3))?;
map.serialize_entry("line", &addition.line)?;
map.serialize_entry("lineNo", &addition.line_no)?;
map.serialize_entry("type", "addition")?;
map.end()
}
Modification::Deletion(deletion) => {
let mut map = serializer.serialize_map(Some(3))?;
map.serialize_entry("line", &deletion.line)?;
map.serialize_entry("lineNo", &deletion.line_no)?;
map.serialize_entry("type", "deletion")?;
map.end()
}
Modification::Context {
line,
line_no_old,
line_no_new,
} => {
let mut map = serializer.serialize_map(Some(4))?;
map.serialize_entry("line", line)?;
map.serialize_entry("lineNoOld", line_no_old)?;
map.serialize_entry("lineNoNew", line_no_new)?;
map.serialize_entry("type", "context")?;
map.end()
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Addition {
pub line: Line,
pub line_no: u32,
}
#[cfg(feature = "serde")]
impl Serialize for Addition {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeStruct as _;
let mut s = serializer.serialize_struct("Addition", 3)?;
s.serialize_field("line", &self.line)?;
s.serialize_field("lineNo", &self.line_no)?;
s.serialize_field("type", "addition")?;
s.end()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Deletion {
pub line: Line,
pub line_no: u32,
}
#[cfg(feature = "serde")]
impl Serialize for Deletion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeStruct as _;
let mut s = serializer.serialize_struct("Deletion", 3)?;
s.serialize_field("line", &self.line)?;
s.serialize_field("lineNo", &self.line_no)?;
s.serialize_field("type", "deletion")?;
s.end()
}
}
impl Modification {
pub fn addition(line: impl Into<Line>, line_no: u32) -> Self {
Self::Addition(Addition {
line: line.into(),
line_no,
})
}
pub fn deletion(line: impl Into<Line>, line_no: u32) -> Self {
Self::Deletion(Deletion {
line: line.into(),
line_no,
})
}
pub fn context(line: impl Into<Line>, line_no_old: u32, line_no_new: u32) -> Self {
Self::Context {
line: line.into(),
line_no_old,
line_no_new,
}
}
}