radicle-surf: refactor diff

This is an overhaul of the diff module. The major changes are as
follows.

It shouldn't be possible for an added or deleted file to have any
other kind line changes other than additions or deletions. So there
are now 3 different kinds of changes, Addition, Deletion, or
Modification.

Hunk and Hunks now take a generic type parameter to allow for the
three types of changes. As a consequence a FileDiff does the same.

For the actual file representations, they fill in their parameters as
Addition, Deletion, or Modification.

During this refactor some renaming was also made:
* CreateFile -> Added -- as well as the created field being renamed
to added.
* DeleteFile -> Deleted
* MoveFile -> Moved
* CopyFile -> Copied
* ModifiedFile -> Modified

The Stats are also now carried in the Diff instead of being
calculated. They can be retrieved from the `git2::Diff`.

The final change was to remove the `source::Commit::stats` field in
favour of accessing the `Diff::stats` instead.

The above changes mean that the serialization of `Diff` and `Commit`
are not backwards compatible.

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
Fintan Halpenny
2022-11-30 13:23:09 +00:00
parent c0545f4fac
commit c316ce8d70
8 changed files with 607 additions and 586 deletions
+291 -136
View File
@@ -17,16 +17,34 @@
use std::convert::TryFrom;
use crate::diff::{self, Diff, EofNewLine, Hunk, Hunks, Line, LineDiff};
use crate::diff::{self, Addition, Deletion, Diff, EofNewLine, Hunk, Hunks, Line, Modification};
pub mod error {
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq)]
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum LineDiff {
pub enum Addition {
#[error(transparent)]
Git(#[from] git2::Error),
#[error("the new line number was missing for an added line")]
MissingNewLineNo,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Deletion {
#[error(transparent)]
Git(#[from] git2::Error),
#[error("the new line number was missing for an deleted line")]
MissingOldLineNo,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Modification {
/// A Git `DiffLine` is invalid.
#[error(
"invalid `git2::DiffLine` which contains no line numbers for either side of the diff"
@@ -34,19 +52,23 @@ pub mod error {
Invalid,
}
#[derive(Debug, Error, PartialEq)]
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Hunk {
#[error(transparent)]
Git(#[from] git2::Error),
#[error(transparent)]
Line(#[from] LineDiff),
Line(#[from] Modification),
}
/// A Git diff error.
#[derive(Debug, PartialEq, Error)]
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Diff {
#[error(transparent)]
Addition(#[from] Addition),
#[error(transparent)]
Deletion(#[from] Deletion),
/// A Git delta type isn't currently handled.
#[error("git delta type is not handled")]
DeltaUnhandled(git2::Delta),
@@ -55,7 +77,7 @@ pub mod error {
#[error(transparent)]
Hunk(#[from] Hunk),
#[error(transparent)]
Line(#[from] LineDiff),
Line(#[from] Modification),
/// A patch is unavailable.
#[error("couldn't retrieve patch for {0}")]
PatchUnavailable(PathBuf),
@@ -65,15 +87,116 @@ pub mod error {
}
}
impl<'a> TryFrom<git2::DiffLine<'a>> for LineDiff {
type Error = error::LineDiff;
impl TryFrom<git2::Patch<'_>> for Hunks<Modification> {
type Error = error::Hunk;
fn try_from(patch: git2::Patch) -> Result<Self, Self::Error> {
let mut hunks = Vec::new();
for h in 0..patch.num_hunks() {
let (hunk, hunk_lines) = patch.hunk(h)?;
let header = Line(hunk.header().to_owned());
let mut lines: Vec<Modification> = Vec::new();
for l in 0..hunk_lines {
let line = patch.line_in_hunk(h, l)?;
let line = Modification::try_from(line)?;
lines.push(line);
}
hunks.push(Hunk { header, lines });
}
Ok(Hunks(hunks))
}
}
impl TryFrom<git2::Patch<'_>> for Hunks<Addition> {
type Error = error::Addition;
fn try_from(patch: git2::Patch) -> Result<Self, Self::Error> {
let mut hunks = Vec::with_capacity(patch.num_hunks());
for h in 0..patch.num_hunks() {
let (hunk, hunk_lines) = patch.hunk(h)?;
let header = Line(hunk.header().to_owned());
let mut lines: Vec<Addition> = Vec::with_capacity(hunk_lines);
for l in 0..hunk_lines {
let line = patch.line_in_hunk(h, l)?;
lines.push(Addition::try_from(line)?);
}
hunks.push(Hunk { header, lines })
}
Ok(hunks.into())
}
}
impl TryFrom<git2::Patch<'_>> for Hunks<Deletion> {
type Error = git2::Error;
fn try_from(patch: git2::Patch) -> Result<Self, Self::Error> {
let mut hunks = Vec::with_capacity(patch.num_hunks());
for h in 0..patch.num_hunks() {
let (hunk, hunk_lines) = patch.hunk(h)?;
let header = Line(hunk.header().to_owned());
let mut lines: Vec<Deletion> = Vec::with_capacity(hunk_lines);
for l in 0..hunk_lines {
let line = patch.line_in_hunk(h, l)?;
lines.push(Deletion::try_from(line).expect("TODO"));
}
hunks.push(Hunk { header, lines })
}
Ok(hunks.into())
}
}
impl<'a> TryFrom<git2::DiffLine<'a>> for Modification {
type Error = error::Modification;
fn try_from(line: git2::DiffLine) -> Result<Self, Self::Error> {
match (line.old_lineno(), line.new_lineno()) {
(None, Some(n)) => Ok(Self::addition(line.content().to_owned(), n)),
(Some(n), None) => Ok(Self::deletion(line.content().to_owned(), n)),
(Some(l), Some(r)) => Ok(Self::context(line.content().to_owned(), l, r)),
(None, None) => Err(error::LineDiff::Invalid),
(None, None) => Err(error::Modification::Invalid),
}
}
}
impl TryFrom<git2::DiffLine<'_>> for Addition {
type Error = error::Addition;
fn try_from(line: git2::DiffLine) -> Result<Self, Self::Error> {
debug_assert!(
line.old_lineno().is_none(),
"trying to build Addition with a modified line"
);
Ok(Self {
line: line.content().to_owned().into(),
line_no: line.new_lineno().ok_or(error::Addition::MissingNewLineNo)?,
})
}
}
impl TryFrom<git2::DiffLine<'_>> for Deletion {
type Error = error::Deletion;
fn try_from(line: git2::DiffLine) -> Result<Self, Self::Error> {
debug_assert!(
line.new_lineno().is_none(),
"trying to build Deletion with a modified line"
);
Ok(Self {
line: line.content().to_owned().into(),
line_no: line.old_lineno().ok_or(error::Deletion::MissingOldLineNo)?,
})
}
}
impl From<git2::DiffStats> for diff::Stats {
fn from(stats: git2::DiffStats) -> Self {
Self {
files_changed: stats.files_changed(),
insertions: stats.insertions(),
deletions: stats.deletions(),
}
}
}
@@ -82,137 +205,18 @@ impl<'a> TryFrom<git2::Diff<'a>> for Diff {
type Error = error::Diff;
fn try_from(git_diff: git2::Diff) -> Result<Diff, Self::Error> {
use git2::{Delta, Patch};
use git2::Delta;
let mut diff = Diff::new();
diff.stats = git_diff.stats()?.into();
for (idx, delta) in git_diff.deltas().enumerate() {
match delta.status() {
Delta::Added => {
let diff_file = delta.new_file();
let path = diff_file
.path()
.ok_or(error::Diff::PathUnavailable)?
.to_path_buf();
let patch = Patch::from_diff(&git_diff, idx)?;
if let Some(patch) = patch {
diff.add_created_file(
path,
diff::FileDiff::Plain {
hunks: Hunks::try_from(patch)?,
},
);
} else {
diff.add_created_file(
path,
diff::FileDiff::Plain {
hunks: Hunks::default(),
},
);
}
},
Delta::Deleted => {
let diff_file = delta.old_file();
let path = diff_file
.path()
.ok_or(error::Diff::PathUnavailable)?
.to_path_buf();
let patch = Patch::from_diff(&git_diff, idx)?;
if let Some(patch) = patch {
diff.add_deleted_file(
path,
diff::FileDiff::Plain {
hunks: Hunks::try_from(patch)?,
},
);
} else {
diff.add_deleted_file(
path,
diff::FileDiff::Plain {
hunks: Hunks::default(),
},
);
}
},
Delta::Modified => {
let diff_file = delta.new_file();
let path = diff_file
.path()
.ok_or(error::Diff::PathUnavailable)?
.to_path_buf();
let patch = Patch::from_diff(&git_diff, idx)?;
if let Some(patch) = patch {
let mut hunks: Vec<Hunk> = Vec::new();
let mut old_missing_eof = false;
let mut new_missing_eof = false;
for h in 0..patch.num_hunks() {
let (hunk, hunk_lines) = patch.hunk(h)?;
let header = Line(hunk.header().to_owned());
let mut lines: Vec<LineDiff> = Vec::new();
for l in 0..hunk_lines {
let line = patch.line_in_hunk(h, l)?;
match line.origin_value() {
git2::DiffLineType::ContextEOFNL => {
new_missing_eof = true;
old_missing_eof = true;
continue;
},
git2::DiffLineType::AddEOFNL => {
old_missing_eof = true;
continue;
},
git2::DiffLineType::DeleteEOFNL => {
new_missing_eof = true;
continue;
},
_ => {},
}
let line = LineDiff::try_from(line)?;
lines.push(line);
}
hunks.push(Hunk { header, lines });
}
let eof = match (old_missing_eof, new_missing_eof) {
(true, true) => Some(EofNewLine::BothMissing),
(true, false) => Some(EofNewLine::OldMissing),
(false, true) => Some(EofNewLine::NewMissing),
(false, false) => None,
};
diff.add_modified_file(path, hunks, eof);
} else if diff_file.is_binary() {
diff.add_modified_binary_file(path);
} else {
return Err(error::Diff::PatchUnavailable(path));
}
},
Delta::Renamed => {
let old = delta
.old_file()
.path()
.ok_or(error::Diff::PathUnavailable)?;
let new = delta
.new_file()
.path()
.ok_or(error::Diff::PathUnavailable)?;
diff.add_moved_file(old.to_path_buf(), new.to_path_buf());
},
Delta::Copied => {
let old = delta
.old_file()
.path()
.ok_or(error::Diff::PathUnavailable)?;
let new = delta
.new_file()
.path()
.ok_or(error::Diff::PathUnavailable)?;
diff.add_copied_file(old.to_path_buf(), new.to_path_buf());
},
Delta::Added => created(&mut diff, &git_diff, idx, &delta)?,
Delta::Deleted => deleted(&mut diff, &git_diff, idx, &delta)?,
Delta::Modified => modified(&mut diff, &git_diff, idx, &delta)?,
Delta::Renamed => renamed(&mut diff, &delta)?,
Delta::Copied => copied(&mut diff, &delta)?,
status => {
return Err(error::Diff::DeltaUnhandled(status));
},
@@ -222,3 +226,154 @@ impl<'a> TryFrom<git2::Diff<'a>> for Diff {
Ok(diff)
}
}
fn created(
diff: &mut Diff,
git_diff: &git2::Diff<'_>,
idx: usize,
delta: &git2::DiffDelta<'_>,
) -> Result<(), error::Diff> {
let diff_file = delta.new_file();
let path = diff_file
.path()
.ok_or(error::Diff::PathUnavailable)?
.to_path_buf();
let patch = git2::Patch::from_diff(git_diff, idx)?;
if let Some(patch) = patch {
diff.added(
path,
diff::FileDiff::Plain {
hunks: Hunks::try_from(patch)?,
},
);
} else {
diff.added(
path,
diff::FileDiff::Plain {
hunks: Hunks::default(),
},
);
}
Ok(())
}
fn deleted(
diff: &mut Diff,
git_diff: &git2::Diff<'_>,
idx: usize,
delta: &git2::DiffDelta<'_>,
) -> Result<(), error::Diff> {
let diff_file = delta.old_file();
let path = diff_file
.path()
.ok_or(error::Diff::PathUnavailable)?
.to_path_buf();
let patch = git2::Patch::from_diff(git_diff, idx)?;
if let Some(patch) = patch {
diff.deleted(
path,
diff::FileDiff::Plain {
hunks: Hunks::try_from(patch)?,
},
);
} else {
diff.deleted(
path,
diff::FileDiff::Plain {
hunks: Hunks::default(),
},
);
}
Ok(())
}
fn modified(
diff: &mut Diff,
git_diff: &git2::Diff<'_>,
idx: usize,
delta: &git2::DiffDelta<'_>,
) -> Result<(), error::Diff> {
let diff_file = delta.new_file();
let path = diff_file
.path()
.ok_or(error::Diff::PathUnavailable)?
.to_path_buf();
let patch = git2::Patch::from_diff(git_diff, idx)?;
if let Some(patch) = patch {
let mut hunks: Vec<Hunk<Modification>> = Vec::new();
let mut old_missing_eof = false;
let mut new_missing_eof = false;
for h in 0..patch.num_hunks() {
let (hunk, hunk_lines) = patch.hunk(h)?;
let header = Line(hunk.header().to_owned());
let mut lines: Vec<Modification> = Vec::new();
for l in 0..hunk_lines {
let line = patch.line_in_hunk(h, l)?;
match line.origin_value() {
git2::DiffLineType::ContextEOFNL => {
new_missing_eof = true;
old_missing_eof = true;
continue;
},
git2::DiffLineType::AddEOFNL => {
old_missing_eof = true;
continue;
},
git2::DiffLineType::DeleteEOFNL => {
new_missing_eof = true;
continue;
},
_ => {},
}
let line = Modification::try_from(line)?;
lines.push(line);
}
hunks.push(Hunk { header, lines });
}
let eof = match (old_missing_eof, new_missing_eof) {
(true, true) => Some(EofNewLine::BothMissing),
(true, false) => Some(EofNewLine::OldMissing),
(false, true) => Some(EofNewLine::NewMissing),
(false, false) => None,
};
diff.modified(path, hunks, eof);
Ok(())
} else if diff_file.is_binary() {
diff.modified_binary(path);
Ok(())
} else {
Err(error::Diff::PatchUnavailable(path))
}
}
fn renamed(diff: &mut Diff, delta: &git2::DiffDelta<'_>) -> Result<(), error::Diff> {
let old = delta
.old_file()
.path()
.ok_or(error::Diff::PathUnavailable)?;
let new = delta
.new_file()
.path()
.ok_or(error::Diff::PathUnavailable)?;
diff.moved(old.to_path_buf(), new.to_path_buf());
Ok(())
}
fn copied(diff: &mut Diff, delta: &git2::DiffDelta<'_>) -> Result<(), error::Diff> {
let old = delta
.old_file()
.path()
.ok_or(error::Diff::PathUnavailable)?;
let new = delta
.new_file()
.path()
.ok_or(error::Diff::PathUnavailable)?;
diff.copied(old.to_path_buf(), new.to_path_buf());
Ok(())
}