Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,19 @@ jobs:
commits: '0'
- name: golang/go
repo_url: https://github.com/golang/go
commits: '200'
commits: '2000'
- name: git/git
repo_url: https://github.com/git/git
commits: '100'
commits: '1000'
- name: rust-lang/rust
repo_url: https://github.com/rust-lang/rust
commits: '30'
commits: '50'
- name: torvalds/linux
repo_url: https://github.com/torvalds/linux
commits: '30'
commits: '40'
- name: llvm/llvm-project
repo_url: https://github.com/llvm/llvm-project
commits: '30'
commits: '50'
name: replay (${{ matrix.name }})
uses: ./.github/workflows/replay.yml
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/replay.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
parse_mode: [unidiff]
parse_mode: [unidiff, gitdiff]
name: ${{ inputs.name && matrix.parse_mode || format('{0} ({1}, {2})', inputs.repo_url, matrix.parse_mode, inputs.commits) }}
steps:
- uses: actions/checkout@v6
Expand Down
8 changes: 8 additions & 0 deletions src/patch_set/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ pub(crate) enum PatchSetParseErrorKind {

/// Create patch missing modified path.
CreateMissingModifiedPath,

/// Invalid file mode string.
InvalidFileMode(String),

/// Invalid `diff --git` path.
InvalidDiffGitPath,
}

impl fmt::Display for PatchSetParseErrorKind {
Expand All @@ -81,6 +87,8 @@ impl fmt::Display for PatchSetParseErrorKind {
Self::BothDevNull => write!(f, "patch has both original and modified as /dev/null"),
Self::DeleteMissingOriginalPath => write!(f, "delete patch has no original path"),
Self::CreateMissingModifiedPath => write!(f, "create patch has no modified path"),
Self::InvalidFileMode(mode) => write!(f, "invalid file mode: {mode}"),
Self::InvalidDiffGitPath => write!(f, "invalid diff --git path"),
}
}
}
Expand Down
68 changes: 67 additions & 1 deletion src/patch_set/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,23 @@ use crate::utils::Text;
use crate::Patch;

pub use error::PatchSetParseError;
use error::PatchSetParseErrorKind;
pub use parse::PatchSet;

/// Options for parsing patch content.
///
/// Use [`ParseOptions::unidiff()`] to create options for the desired format.
/// Use [`ParseOptions::unidiff()`] or [`ParseOptions::gitdiff()`]
/// to create options for the desired format.
///
/// ## Binary Files
///
/// When parsing git diffs, binary file changes are detected by:
///
/// * `Binary files a/path and b/path differ` (`git diff` without `--binary` flag)
/// * `GIT binary patch` (from `git diff --binary`)
///
/// Note that this is not a documented Git behavior,
/// so the implementation here is subject to change if Git changes.
///
/// ## Example
///
Expand Down Expand Up @@ -48,6 +60,8 @@ pub struct ParseOptions {
pub(crate) enum Format {
/// Standard unified diff format.
UniDiff,
/// Git extended diff format.
GitDiff,
}

impl ParseOptions {
Expand All @@ -68,6 +82,22 @@ impl ParseOptions {
format: Format::UniDiff,
}
}

/// Parse as [git extended diff format][git-diff-format].
///
/// Supports all features of [`unidiff()`](Self::unidiff) plus:
///
/// * `diff --git` headers
/// * Extended headers (`new file mode`, `deleted file mode`, etc.)
/// * Rename/copy detection (`rename from`/`rename to`, `copy from`/`copy to`)
/// * Binary file detection (emitted a marker by defualt)
///
/// [git-diff-format]: https://git-scm.com/docs/diff-format
pub fn gitdiff() -> Self {
Self {
format: Format::GitDiff,
}
}
}

/// File mode extracted from git extended headers.
Expand All @@ -83,11 +113,27 @@ pub enum FileMode {
Gitlink,
}

impl std::str::FromStr for FileMode {
type Err = PatchSetParseError;

fn from_str(mode: &str) -> Result<Self, Self::Err> {
match mode {
"100644" => Ok(Self::Regular),
"100755" => Ok(Self::Executable),
"120000" => Ok(Self::Symlink),
"160000" => Ok(Self::Gitlink),
_ => Err(PatchSetParseErrorKind::InvalidFileMode(mode.to_owned()).into()),
}
}
}

/// The kind of patch content in a [`FilePatch`].
#[derive(Clone, PartialEq, Eq)]
pub enum PatchKind<'a, T: ToOwned + ?Sized> {
/// Text patch with hunks.
Text(Patch<'a, T>),
/// Binary patch (literal or delta encoded, or marker-only).
Binary,
}

impl<T: ?Sized, O> std::fmt::Debug for PatchKind<'_, T>
Expand All @@ -98,6 +144,7 @@ where
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PatchKind::Text(patch) => f.debug_tuple("Text").field(patch).finish(),
PatchKind::Binary => f.write_str("Binary"),
}
}
}
Expand All @@ -107,8 +154,14 @@ impl<'a, T: ToOwned + ?Sized> PatchKind<'a, T> {
pub fn as_text(&self) -> Option<&Patch<'a, T>> {
match self {
PatchKind::Text(patch) => Some(patch),
PatchKind::Binary => None,
}
}

/// Returns `true` if this is a binary diff.
pub fn is_binary(&self) -> bool {
matches!(self, PatchKind::Binary)
}
}

/// A single file's patch with operation metadata.
Expand Down Expand Up @@ -154,6 +207,19 @@ impl<'a, T: ToOwned + ?Sized> FilePatch<'a, T> {
}
}

fn new_binary(
operation: FileOperation<'a, T>,
old_mode: Option<FileMode>,
new_mode: Option<FileMode>,
) -> Self {
Self {
operation,
kind: PatchKind::Binary,
old_mode,
new_mode,
}
}

/// Returns the file operation for this patch.
pub fn operation(&self) -> &FileOperation<'a, T> {
&self.operation
Expand Down
Loading