DiffAlgorithm.java
/*
* Copyright (C) 2010, Google Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.diff;
/**
* Compares two {@link org.eclipse.jgit.diff.Sequence}s to create an
* {@link org.eclipse.jgit.diff.EditList} of changes.
* <p>
* An algorithm's {@code diff} method must be callable from concurrent threads
* without data collisions. This permits some algorithms to use a singleton
* pattern, with concurrent invocations using the same singleton. Other
* algorithms may support parameterization, in which case the caller can create
* a unique instance per thread.
*/
public abstract class DiffAlgorithm {
/**
* Supported diff algorithm
*/
public enum SupportedAlgorithm {
/**
* Myers diff algorithm
*/
MYERS,
/**
* Histogram diff algorithm
*/
HISTOGRAM
}
/**
* Get diff algorithm
*
* @param alg
* the diff algorithm for which an implementation should be
* returned
* @return an implementation of the specified diff algorithm
*/
public static DiffAlgorithm getAlgorithm(SupportedAlgorithm alg) {
switch (alg) {
case MYERS:
return MyersDiff.INSTANCE;
case HISTOGRAM:
return new HistogramDiff();
default:
throw new IllegalArgumentException();
}
}
/**
* Compare two sequences and identify a list of edits between them.
*
* @param cmp
* the comparator supplying the element equivalence function.
* @param a
* the first (also known as old or pre-image) sequence. Edits
* returned by this algorithm will reference indexes using the
* 'A' side: {@link org.eclipse.jgit.diff.Edit#getBeginA()},
* {@link org.eclipse.jgit.diff.Edit#getEndA()}.
* @param b
* the second (also known as new or post-image) sequence. Edits
* returned by this algorithm will reference indexes using the
* 'B' side: {@link org.eclipse.jgit.diff.Edit#getBeginB()},
* {@link org.eclipse.jgit.diff.Edit#getEndB()}.
* @return a modifiable edit list comparing the two sequences. If empty, the
* sequences are identical according to {@code cmp}'s rules. The
* result list is never null.
*/
public <S extends Sequence> EditList diff(
SequenceComparator<? super S> cmp, S a, S b) {
Edit region = cmp.reduceCommonStartEnd(a, b, coverEdit(a, b));
switch (region.getType()) {
case INSERT:
case DELETE:
return EditList.singleton(region);
case REPLACE: {
if (region.getLengthA() == 1 && region.getLengthB() == 1)
return EditList.singleton(region);
SubsequenceComparator<S> cs = new SubsequenceComparator<>(cmp);
Subsequence<S> as = Subsequence.a(a, region);
Subsequence<S> bs = Subsequence.b(b, region);
EditList e = Subsequence.toBase(diffNonCommon(cs, as, bs), as, bs);
return normalize(cmp, e, a, b);
}
case EMPTY:
return new EditList(0);
default:
throw new IllegalStateException();
}
}
private static <S extends Sequence> Edit coverEdit(S a, S b) {
return new Edit(0, a.size(), 0, b.size());
}
/**
* Reorganize an {@link EditList} for better diff consistency.
* <p>
* {@code DiffAlgorithms} may return {@link Edit.Type#INSERT} or
* {@link Edit.Type#DELETE} edits that can be "shifted". For
* example, the deleted section
* <pre>
* -a
* -b
* -c
* a
* b
* c
* </pre>
* can be shifted down by 1, 2 or 3 locations.
* <p>
* To avoid later merge issues, we shift such edits to a
* consistent location. {@code normalize} uses a simple strategy of
* shifting such edits to their latest possible location.
* <p>
* This strategy may not always produce an aesthetically pleasing
* diff. For instance, it works well with
* <pre>
* function1 {
* ...
* }
*
* +function2 {
* + ...
* +}
* +
* function3 {
* ...
* }
* </pre>
* but less so for
* <pre>
* #
* # comment1
* #
* function1() {
* }
*
* #
* +# comment3
* +#
* +function3() {
* +}
* +
* +#
* # comment2
* #
* function2() {
* }
* </pre>
* <a href="https://github.com/mhagger/diff-slider-tools">More
* sophisticated strategies</a> are possible, say by calculating a
* suitable "aesthetic cost" for each possible position and using
* the lowest cost, but {@code normalize} just shifts edits
* to the end as much as possible.
*
* @param <S>
* type of sequence being compared.
* @param cmp
* the comparator supplying the element equivalence function.
* @param e
* a modifiable edit list comparing the provided sequences.
* @param a
* the first (also known as old or pre-image) sequence.
* @param b
* the second (also known as new or post-image) sequence.
* @return a modifiable edit list with edit regions shifted to their
* latest possible location. The result list is never null.
* @since 4.7
*/
private static <S extends Sequence> EditList normalize(
SequenceComparator<? super S> cmp, EditList e, S a, S b) {
Edit prev = null;
for (int i = e.size() - 1; i >= 0; i--) {
Edit cur = e.get(i);
Edit.Type curType = cur.getType();
int maxA = (prev == null) ? a.size() : prev.beginA;
int maxB = (prev == null) ? b.size() : prev.beginB;
if (curType == Edit.Type.INSERT) {
while (cur.endA < maxA && cur.endB < maxB
&& cmp.equals(b, cur.beginB, b, cur.endB)) {
cur.shift(1);
}
} else if (curType == Edit.Type.DELETE) {
while (cur.endA < maxA && cur.endB < maxB
&& cmp.equals(a, cur.beginA, a, cur.endA)) {
cur.shift(1);
}
}
prev = cur;
}
return e;
}
/**
* Compare two sequences and identify a list of edits between them.
*
* This method should be invoked only after the two sequences have been
* proven to have no common starting or ending elements. The expected
* elimination of common starting and ending elements is automatically
* performed by the {@link #diff(SequenceComparator, Sequence, Sequence)}
* method, which invokes this method using
* {@link org.eclipse.jgit.diff.Subsequence}s.
*
* @param cmp
* the comparator supplying the element equivalence function.
* @param a
* the first (also known as old or pre-image) sequence. Edits
* returned by this algorithm will reference indexes using the
* 'A' side: {@link org.eclipse.jgit.diff.Edit#getBeginA()},
* {@link org.eclipse.jgit.diff.Edit#getEndA()}.
* @param b
* the second (also known as new or post-image) sequence. Edits
* returned by this algorithm will reference indexes using the
* 'B' side: {@link org.eclipse.jgit.diff.Edit#getBeginB()},
* {@link org.eclipse.jgit.diff.Edit#getEndB()}.
* @return a modifiable edit list comparing the two sequences.
*/
public abstract <S extends Sequence> EditList diffNonCommon(
SequenceComparator<? super S> cmp, S a, S b);
}