Current section
Files
Jump to
Current section
Files
lib/diff.ex
defmodule Diff do
alias Diff.Matrix
alias Diff.Diffable
@moduledoc """
Functions for performing diffs.
"""
defmodule Insert do
defstruct [:element, :index, :length]
end
defmodule Delete do
defstruct [:element, :index, :length]
end
defmodule Modified do
defstruct [:element, :old_element, :index, :length]
end
defmodule Unchanged do
defstruct [:element, :index, :length]
end
defmodule Ignored do
defstruct [:element, :index, :length]
end
@doc"""
Applies with patches with supplied annotation (top and tail)
This is used to generate visual diffs, etc
Shares the same code as patch
"""
def annotated_patch(original, patches, annotations, from_list_fn \\ fn(list) -> list end) do
apply_patches(original, patches, annotations, from_list_fn)
end
@doc """
Applies the patches from a previous diff to the given string.
Will return the patched version as a list unless a from_list_fn/1 is supplied.
This function will takes the patched list as input and outputs the result.
"""
def patch(original, patches, from_list_fn \\ fn(list) -> list end) do
apply_patches(original, patches, [], from_list_fn)
end
defp apply_patches(original, patches, annotations, from_list_fn) do
original = Diffable.to_list(original)
patchfn = fn(patch, {increment, changed}) ->
do_patch({increment, changed}, patch, annotations)
end
increment = 0
{_, returnlist} = Enum.reduce(patches, {increment, original}, patchfn)
from_list_fn.(returnlist)
end
defp do_patch({incr, original}, %Diff.Insert{element: element, index: index},
annotations) do
{ left, right } = Enum.split(original, index + incr)
{newelement, newincr} = annotate(element, :insert, annotations, incr)
return = left ++ newelement ++ right
{newincr, return}
end
defp do_patch({incr, original}, %Diff.Delete{ element: element, index: index,
length: length }, annotations) do
{ left, deleted } = Enum.split(original, index + incr)
{ actuallydeleted, right } = Enum.split(deleted, length)
case element do
^actuallydeleted ->
{newelement, newincr} = annotate(element, :deleted, annotations, incr)
return = left ++ newelement ++ right
{newincr, return}
_other ->
exit("failed delete")
end
end
defp do_patch({incr, original},
%Diff.Modified{ element: element, old_element: _,
index: index, length: length},
annotations) do
{ left, deleted } = Enum.split(original, index + incr)
{ _, right } = Enum.split(deleted, length)
{newelement, newincr} = annotate(element, :modified, annotations, incr)
return = left ++ newelement ++ right
{newincr, return}
end
defp do_patch({incr, original}, %Diff.Unchanged{}, _annotations) do
{incr, original}
end
defp do_patch({incr, original}, %Diff.Ignored{element: element, index: index},
annotations) do
{ left, right } = Enum.split(original, index + incr)
{newelement, newincr} = annotate(element, :ignored, annotations, incr)
return = left ++ newelement ++ right
{newincr, return}
end
@doc"""
Creates a list of changes from the orginal binary to the changed one.
Takes the following options:
* `:keep_unchanged` - Keeps unchanged binary parts in the returned patches
* `ignore` - Takes a regex and ignores matches
"""
def diff(original, changed, options \\ []) do
original = Diffable.to_list(original)
changed = Diffable.to_list(changed)
original_length = length(original)
changed_length = length(changed)
longest_common_subsequence(original, changed, original_length, changed_length)
|> build_diff(original, changed, original_length, changed_length, [], options)
|> build_changes(options)
end
defp longest_common_subsequence(x, y, x_length, y_length) do
matrix = Matrix.new(x_length + 1, y_length + 1)
# a reduction over a 2D array requires a closure inside an anonymous function
# sorry but there is nothing to be done about that
rowreductionFn = fn(i, matrix) ->
# setup the second closure
columnreductionFn = fn(j, matrix) ->
if Enum.fetch!(x, i-1) == Enum.fetch!(y, j-1) do
value = Matrix.get(matrix, i-1, j-1)
Matrix.put(matrix, i, j, value + 1)
else
original_value = Matrix.get(matrix, i, j-1)
changed_value = Matrix.get(matrix, i - 1, j)
Matrix.put(matrix, i, j, max(original_value, changed_value))
end
end
Enum.reduce(1..y_length, matrix, columnreductionFn)
end
_matrix = Enum.reduce(1..x_length, matrix, rowreductionFn)
end
defp build_diff(matrix, x, y, i, j, edits, options) do
cond do
i > 0 and j > 0 and Enum.fetch!(x, i-1) == Enum.fetch!(y, j-1) ->
newedits = if Dict.get(options, :keep_unchanged, false) do
edits ++ [{:unchanged, Enum.fetch!(x, i-1), i-1}]
else
edits
end
build_diff(matrix, x, y, i-1, j-1, newedits, options)
j > 0 and (i == 0 or Matrix.get(matrix, i, j-1) >= Matrix.get(matrix,i-1, j)) ->
newedit = {:insert, Enum.fetch!(y, j-1), j-1}
build_diff(matrix, x, y, i, j-1, edits ++ [newedit], options)
i > 0 and (j == 0 or Matrix.get(matrix, i, j-1) < Matrix.get(matrix, i-1, j)) ->
newdelete = {:delete, Enum.fetch!(x, i-1), j}
build_diff(matrix, x, y, i-1, j, edits ++ [newdelete], options)
true ->
edits |> Enum.reverse
end
end
defp build_changes(edits, options) do
# we now have a set of individual letter changes
# but if there is a series of inserts or deletes then
# we need to reduce them into single multichar changes
mergeindividualchangesFn = fn({type, char, index}, changes) ->
if changes == [] do
changes ++ [make_change(type, char, index)]
else
change = List.last(changes)
regex = Dict.get(options, :ignore)
cond do
regex && Regex.match?(regex, char) ->
changes ++ [make_change(:ignored, char, index)]
# one branch for deletes
is_type(change, type) && type == :delete && index == change.index ->
change = if regex && Regex.match?(regex, Enum.join(change.element)) do
%Ignored{ element: change.element, index: change.index,
length: change.length }
else
%{change | element: change.element ++ [char], length:
change.length + 1 }
end
List.replace_at(changes, length(changes)-1, change)
# a different branch for everyone else
is_type(change, type) && type != :delete && index == (change.index + change.length) ->
change = if regex && Regex.match?(regex, Enum.join(change.element)) do
%Ignored{ element: change.element, index: change.index,
length: change.length }
else
%{change | element: change.element ++ [char], length:
change.length + 1 }
end
List.replace_at(changes, length(changes)-1, change)
true ->
changes ++ [make_change(type, char, index)]
end
end
end
# if we change a single letter it will be a consecutive delete/insert
# this reduction merges them into a single modified statement
makemodifiedFn = fn(x, changes) ->
if changes == [] do
[x]
else
last_change = List.last(changes)
if is_type(last_change, :delete)
and is_type(x, :insert)
and last_change.index == x.index
and last_change.length == x.length do
last_change = %Modified{ element: x.element, old_element: last_change.element,
index: x.index, length: x.length }
List.replace_at(changes, length(changes) - 1, last_change)
else
changes ++ [x]
end
end
end
# Now do both these sets of reduction on the edits
Enum.reduce(edits, [], mergeindividualchangesFn)
|> Enum.reduce([], makemodifiedFn)
end
defp make_change(:insert, char, index) do
%Insert{ element: [char], index: index, length: 1 }
end
defp make_change(:delete, char, index) do
%Delete{ element: [char], index: index, length: 1 }
end
defp make_change(:unchanged, char, index) do
%Unchanged{ element: [char], index: index, length: 1 }
end
defp make_change(:ignored, char, index) do
%Ignored{ element: [char], index: index, length: 1 }
end
defp is_type(%Insert{}, :insert) do
true
end
defp is_type(%Delete{}, :delete) do
true
end
defp is_type(%Unchanged{}, :unchanged) do
true
end
defp is_type(%Ignored{}, :ignored) do
true
end
defp is_type(_, _) do
false
end
defp annotate(list, type, annotations, increment) do
annotation = for a <- annotations,
Map.get(a, type) != nil, do: Map.get(a, type)
case {type, annotation} do
{:deleted, []} -> {[], increment}
{_, []} -> {list, increment}
{:deleted, [annotation]} -> apply_deletion(list, annotation, increment)
{_, [annotation]} -> apply_annotation(list, annotation, increment)
end
end
defp apply_deletion(list, annotation, increment) do
{[annotation.before] ++ list ++ [annotation.after], increment + 2}
end
defp apply_annotation(list, annotation, increment) do
{[annotation.before] ++ list ++ [annotation.after], increment + 2}
end
end