Skip to main content

Crate diffy

Crate diffy 

Source
Expand description

Tools for finding and manipulating differences between files

§Overview

This library is intended to be a collection of tools used to find and manipulate differences between files inspired by LibXDiff and GNU Diffutils. Version control systems like Git and Mercurial generally communicate differences between two versions of a file using a diff or patch.

The current diff implementation is based on the Myers’ diff algorithm.

The documentation generally refers to “files” in many places but none of the apis explicitly operate on on-disk files. Instead this library requires that the text being operated on resides in-memory and as such if you want to perform operations on files, it is up to the user to load the contents of those files into memory before passing their contents to the apis provided by this library.

§Cargo Feature Flags

This crate is no_std by default. Enable Cargo features as needed:

  • std for std::io::Write-based formatting impls
  • color for ANSI-colored patch formatting
  • binary for applying parsed git binary patches

§UTF-8 and Non-UTF-8

This library has support for working with both utf8 and non-utf8 texts. Most of the API’s have two different variants, one for working with utf8 str texts (e.g. create_patch) and one for working with bytes [u8] which may or may not be utf8 (e.g. create_patch_bytes).

§Creating a Patch

A Patch between two texts can be created by doing the following:

use diffy::create_patch;

let original = "The Way of Kings\nWords of Radiance\n";
let modified = "The Way of Kings\nWords of Radiance\nOathbringer\n";

let patch = create_patch(original, modified);

A Patch can the be output in the Unified Format either by using its Display impl or by using a PatchFormatter to output the diff with color (requires the color feature).

// Without color
print!("{}", patch);

With the color feature enabled:

use diffy::PatchFormatter;
let f = PatchFormatter::new().with_color();
print!("{}", f.fmt_patch(&patch));
--- original
+++ modified
@@ -1,2 +1,3 @@
 The Way of Kings
 Words of Radiance
+Oathbringer

§Applying a Patch

Once you have a Patch you can apply it to a base image in order to recover the new text. Each hunk will be applied to the base image in sequence. Similarly to GNU patch, this implementation can detect when line numbers specified in the patch are incorrect and will attempt to find the correct place to apply each hunk by iterating forward and backward from the given position until all context lines from a hunk match the base image.

use diffy::Patch;
use diffy::apply;

let s = "\
--- a/skybreaker-ideals
+++ b/skybreaker-ideals
@@ -10,6 +10,8 @@
 First:
     Life before death,
     strength before weakness,
     journey before destination.
 Second:
-    I will put the law before all else.
+    I swear to seek justice,
+    to let it guide me,
+    until I find a more perfect Ideal.
";

let patch = Patch::from_str(s).unwrap();

let base_image = "\
First:
    Life before death,
    strength before weakness,
    journey before destination.
Second:
    I will put the law before all else.
";

let expected = "\
First:
    Life before death,
    strength before weakness,
    journey before destination.
Second:
    I swear to seek justice,
    to let it guide me,
    until I find a more perfect Ideal.
";

assert_eq!(apply(base_image, &patch).unwrap(), expected);

§Parsing Multi-File Patches

The patch_set module provides support for parsing unified diffs that contain changes to multiple files, such as git diff and git format-patch output. PatchSet is a streaming iterator, so callers can process file patches one at a time.

Use ParseOptions::gitdiff for git-style diffs or ParseOptions::unidiff for plain unified diffs.

use diffy::apply;
use diffy::patch_set::FileOperation;
use diffy::patch_set::ParseOptions;
use diffy::patch_set::PatchSet;

let input = "\
diff --git a/alpha.txt b/alpha.txt
index 1111111..2222222 100644
--- a/alpha.txt
+++ b/alpha.txt
@@ -1 +1 @@
-alpha
+ALPHA
diff --git a/beta.txt b/beta.txt
new file mode 100644
--- /dev/null
+++ b/beta.txt
@@ -0,0 +1 @@
+beta
";

let mut patches = PatchSet::parse(input, ParseOptions::gitdiff());

let first = patches.next().unwrap().unwrap();
let second = patches.next().unwrap().unwrap();
assert!(patches.next().is_none());

match first.operation().strip_prefix(1) {
    FileOperation::Modify { original, modified } => {
        assert_eq!(original, "alpha.txt");
        assert_eq!(modified, "alpha.txt");
    }
    operation => panic!("unexpected operation: {operation:?}"),
}

let text_patch = first.patch().as_text().unwrap();
assert_eq!(apply("alpha\n", text_patch).unwrap(), "ALPHA\n");

match second.operation().strip_prefix(1) {
    FileOperation::Create(path) => assert_eq!(path, "beta.txt"),
    operation => panic!("unexpected operation: {operation:?}"),
}

With the binary Cargo feature enabled, parsed multi-file patches can also contain BinaryPatch values. You can apply them with BinaryPatch::apply.

§Performing a Three-way Merge

Two files A and B can be merged together given a common ancestor or original file O to produce a file C similarly to how diff3 performs a three-way merge.

    --- A ---
  /           \
 /             \
O               C
 \             /
  \           /
    --- B ---

If files A and B modified different regions of the original file O (or the same region in the same way) then the files can be merged without conflict.

use diffy::merge;

let original = "the final empire\nThe Well of Ascension\nThe hero of ages\n";
let a = "The Final Empire\nThe Well of Ascension\nThe Hero of Ages\n";
let b = "The Final Empire\nThe Well of Ascension\nThe hero of ages\n";
let expected = "\
The Final Empire
The Well of Ascension
The Hero of Ages
";

assert_eq!(merge(original, a, b).unwrap(), expected);

If both files A and B modified the same region of the original file O (and those modifications are different), it would result in a conflict as it is not clear which modifications should be used in the merged result.

use diffy::merge;

let original = "The Final Empire\nThe Well of Ascension\nThe hero of ages\n";
let a = "The Final Empire\nThe Well of Ascension\nThe Hero of Ages\nSecret History\n";
let b = "The Final Empire\nThe Well of Ascension\nThe hero of ages\nThe Alloy of Law\n";
let expected = "\
The Final Empire
The Well of Ascension
<<<<<<< ours
The Hero of Ages
Secret History
||||||| original
The hero of ages
=======
The hero of ages
The Alloy of Law
>>>>>>> theirs
";

assert_eq!(merge(original, a, b).unwrap_err(), expected);

Modules§

binary
Git binary diffs support.
patch_set
Utilities for parsing unified diff patches containing multiple files.

Structs§

ApplyError
An error returned when applying a Patch fails
DiffOptions
A collection of options for modifying the way a diff is performed
Hunk
Represents a group of differing lines between two files
HunkRange
The range of lines in a file for a particular Hunk.
MergeOptions
A collection of options for modifying the way a merge is performed
ParsePatchError
An error returned when parsing a Patch using Patch::from_str fails.
Patch
Representation of all the differences between two files
PatchFormatter
Formats patches for display or writing into byte streams.

Enums§

ConflictStyle
Style used when rendering a conflict
Line
A line in either the old file, new file, or both.

Functions§

apply
Apply a Patch to a base image
apply_bytes
Apply a non-utf8 Patch to a base image
create_patch
Create a patch between two texts.
create_patch_bytes
Create a patch between two potentially non-utf8 texts
merge
Merge two files given a common ancestor.
merge_bytes
Perform a 3-way merge between potentially non-utf8 texts