mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-11-04 02:40:53 +08:00
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:
+291
-136
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user