blob: 80607351ae1c24e8064778fa3840ae41b3b57322 [file] [log] [blame]
/*
* Copyright (C) 2009, Christian Halstrick <christian.halstrick@sap.com> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.merge;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.diff.DiffAlgorithm;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.diff.EditList;
import org.eclipse.jgit.diff.HistogramDiff;
import org.eclipse.jgit.diff.Sequence;
import org.eclipse.jgit.diff.SequenceComparator;
import org.eclipse.jgit.merge.MergeChunk.ConflictState;
/**
* Provides the merge algorithm which does a three-way merge on content provided
* as RawText. By default {@link org.eclipse.jgit.diff.HistogramDiff} is used as
* diff algorithm.
*/
public final class MergeAlgorithm {
private final DiffAlgorithm diffAlg;
@NonNull
private ContentMergeStrategy strategy = ContentMergeStrategy.CONFLICT;
/**
* Creates a new MergeAlgorithm which uses
* {@link org.eclipse.jgit.diff.HistogramDiff} as diff algorithm
*/
public MergeAlgorithm() {
this(new HistogramDiff());
}
/**
* Creates a new MergeAlgorithm
*
* @param diff
* the diff algorithm used by this merge
*/
public MergeAlgorithm(DiffAlgorithm diff) {
this.diffAlg = diff;
}
/**
* Retrieves the {@link ContentMergeStrategy}.
*
* @return the {@link ContentMergeStrategy} in effect
* @since 5.12
*/
@NonNull
public ContentMergeStrategy getContentMergeStrategy() {
return strategy;
}
/**
* Sets the {@link ContentMergeStrategy}.
*
* @param strategy
* {@link ContentMergeStrategy} to set; if {@code null}, set
* {@link ContentMergeStrategy#CONFLICT}
* @since 5.12
*/
public void setContentMergeStrategy(ContentMergeStrategy strategy) {
this.strategy = strategy == null ? ContentMergeStrategy.CONFLICT
: strategy;
}
// An special edit which acts as a sentinel value by marking the end the
// list of edits
private static final Edit END_EDIT = new Edit(Integer.MAX_VALUE,
Integer.MAX_VALUE);
@SuppressWarnings("ReferenceEquality")
private static boolean isEndEdit(Edit edit) {
return edit == END_EDIT;
}
/**
* Does the three way merge between a common base and two sequences.
*
* @param cmp comparison method for this execution.
* @param base the common base sequence
* @param ours the first sequence to be merged
* @param theirs the second sequence to be merged
* @return the resulting content
*/
public <S extends Sequence> MergeResult<S> merge(
SequenceComparator<S> cmp, S base, S ours, S theirs) {
List<S> sequences = new ArrayList<>(3);
sequences.add(base);
sequences.add(ours);
sequences.add(theirs);
MergeResult<S> result = new MergeResult<>(sequences);
if (ours.size() == 0) {
if (theirs.size() != 0) {
EditList theirsEdits = diffAlg.diff(cmp, base, theirs);
if (!theirsEdits.isEmpty()) {
// we deleted, they modified
switch (strategy) {
case OURS:
result.add(1, 0, 0, ConflictState.NO_CONFLICT);
break;
case THEIRS:
result.add(2, 0, theirs.size(),
ConflictState.NO_CONFLICT);
break;
default:
// Let their complete content conflict with empty text
result.add(1, 0, 0,
ConflictState.FIRST_CONFLICTING_RANGE);
result.add(2, 0, theirs.size(),
ConflictState.NEXT_CONFLICTING_RANGE);
break;
}
} else {
// we deleted, they didn't modify -> Let our deletion win
result.add(1, 0, 0, ConflictState.NO_CONFLICT);
}
} else {
// we and they deleted -> return a single chunk of nothing
result.add(1, 0, 0, ConflictState.NO_CONFLICT);
}
return result;
} else if (theirs.size() == 0) {
EditList oursEdits = diffAlg.diff(cmp, base, ours);
if (!oursEdits.isEmpty()) {
// we modified, they deleted
switch (strategy) {
case OURS:
result.add(1, 0, ours.size(), ConflictState.NO_CONFLICT);
break;
case THEIRS:
result.add(2, 0, 0, ConflictState.NO_CONFLICT);
break;
default:
// Let our complete content conflict with empty text
result.add(1, 0, ours.size(),
ConflictState.FIRST_CONFLICTING_RANGE);
result.add(2, 0, 0, ConflictState.NEXT_CONFLICTING_RANGE);
break;
}
} else {
// they deleted, we didn't modify -> Let their deletion win
result.add(2, 0, 0, ConflictState.NO_CONFLICT);
}
return result;
}
EditList oursEdits = diffAlg.diff(cmp, base, ours);
Iterator<Edit> baseToOurs = oursEdits.iterator();
EditList theirsEdits = diffAlg.diff(cmp, base, theirs);
Iterator<Edit> baseToTheirs = theirsEdits.iterator();
int current = 0; // points to the next line (first line is 0) of base
// which was not handled yet
Edit oursEdit = nextEdit(baseToOurs);
Edit theirsEdit = nextEdit(baseToTheirs);
// iterate over all edits from base to ours and from base to theirs
// leave the loop when there are no edits more for ours or for theirs
// (or both)
while (!isEndEdit(theirsEdit) || !isEndEdit(oursEdit)) {
if (oursEdit.getEndA() < theirsEdit.getBeginA()) {
// something was changed in ours not overlapping with any change
// from theirs. First add the common part in front of the edit
// then the edit.
if (current != oursEdit.getBeginA()) {
result.add(0, current, oursEdit.getBeginA(),
ConflictState.NO_CONFLICT);
}
result.add(1, oursEdit.getBeginB(), oursEdit.getEndB(),
ConflictState.NO_CONFLICT);
current = oursEdit.getEndA();
oursEdit = nextEdit(baseToOurs);
} else if (theirsEdit.getEndA() < oursEdit.getBeginA()) {
// something was changed in theirs not overlapping with any
// from ours. First add the common part in front of the edit
// then the edit.
if (current != theirsEdit.getBeginA()) {
result.add(0, current, theirsEdit.getBeginA(),
ConflictState.NO_CONFLICT);
}
result.add(2, theirsEdit.getBeginB(), theirsEdit.getEndB(),
ConflictState.NO_CONFLICT);
current = theirsEdit.getEndA();
theirsEdit = nextEdit(baseToTheirs);
} else {
// here we found a real overlapping modification
// if there is a common part in front of the conflict add it
if (oursEdit.getBeginA() != current
&& theirsEdit.getBeginA() != current) {
result.add(0, current, Math.min(oursEdit.getBeginA(),
theirsEdit.getBeginA()), ConflictState.NO_CONFLICT);
}
// set some initial values for the ranges in A and B which we
// want to handle
int oursBeginB = oursEdit.getBeginB();
int theirsBeginB = theirsEdit.getBeginB();
// harmonize the start of the ranges in A and B
if (oursEdit.getBeginA() < theirsEdit.getBeginA()) {
theirsBeginB -= theirsEdit.getBeginA()
- oursEdit.getBeginA();
} else {
oursBeginB -= oursEdit.getBeginA() - theirsEdit.getBeginA();
}
// combine edits:
// Maybe an Edit on one side corresponds to multiple Edits on
// the other side. Then we have to combine the Edits of the
// other side - so in the end we can merge together two single
// edits.
//
// It is important to notice that this combining will extend the
// ranges of our conflict always downwards (towards the end of
// the content). The starts of the conflicting ranges in ours
// and theirs are not touched here.
//
// This combining is an iterative process: after we have
// combined some edits we have to do the check again. The
// combined edits could now correspond to multiple edits on the
// other side.
//
// Example: when this combining algorithm works on the following
// edits
// oursEdits=((0-5,0-5),(6-8,6-8),(10-11,10-11)) and
// theirsEdits=((0-1,0-1),(2-3,2-3),(5-7,5-7))
// it will merge them into
// oursEdits=((0-8,0-8),(10-11,10-11)) and
// theirsEdits=((0-7,0-7))
//
// Since the only interesting thing to us is how in ours and
// theirs the end of the conflicting range is changing we let
// oursEdit and theirsEdit point to the last conflicting edit
Edit nextOursEdit = nextEdit(baseToOurs);
Edit nextTheirsEdit = nextEdit(baseToTheirs);
for (;;) {
if (oursEdit.getEndA() >= nextTheirsEdit.getBeginA()) {
theirsEdit = nextTheirsEdit;
nextTheirsEdit = nextEdit(baseToTheirs);
} else if (theirsEdit.getEndA() >= nextOursEdit.getBeginA()) {
oursEdit = nextOursEdit;
nextOursEdit = nextEdit(baseToOurs);
} else {
break;
}
}
// harmonize the end of the ranges in A and B
int oursEndB = oursEdit.getEndB();
int theirsEndB = theirsEdit.getEndB();
if (oursEdit.getEndA() < theirsEdit.getEndA()) {
oursEndB += theirsEdit.getEndA() - oursEdit.getEndA();
} else {
theirsEndB += oursEdit.getEndA() - theirsEdit.getEndA();
}
// A conflicting region is found. Strip off common lines in
// in the beginning and the end of the conflicting region
// Determine the minimum length of the conflicting areas in OURS
// and THEIRS. Also determine how much bigger the conflicting
// area in THEIRS is compared to OURS. All that is needed to
// limit the search for common areas at the beginning or end
// (the common areas cannot be bigger then the smaller
// conflicting area. The delta is needed to know whether the
// complete conflicting area is common in OURS and THEIRS.
int minBSize = oursEndB - oursBeginB;
int BSizeDelta = minBSize - (theirsEndB - theirsBeginB);
if (BSizeDelta > 0)
minBSize -= BSizeDelta;
int commonPrefix = 0;
while (commonPrefix < minBSize
&& cmp.equals(ours, oursBeginB + commonPrefix, theirs,
theirsBeginB + commonPrefix))
commonPrefix++;
minBSize -= commonPrefix;
int commonSuffix = 0;
while (commonSuffix < minBSize
&& cmp.equals(ours, oursEndB - commonSuffix - 1, theirs,
theirsEndB - commonSuffix - 1))
commonSuffix++;
minBSize -= commonSuffix;
// Add the common lines at start of conflict
if (commonPrefix > 0)
result.add(1, oursBeginB, oursBeginB + commonPrefix,
ConflictState.NO_CONFLICT);
// Add the conflict (Only if there is a conflict left to report)
if (minBSize > 0 || BSizeDelta != 0) {
switch (strategy) {
case OURS:
result.add(1, oursBeginB + commonPrefix,
oursEndB - commonSuffix,
ConflictState.NO_CONFLICT);
break;
case THEIRS:
result.add(2, theirsBeginB + commonPrefix,
theirsEndB - commonSuffix,
ConflictState.NO_CONFLICT);
break;
default:
result.add(1, oursBeginB + commonPrefix,
oursEndB - commonSuffix,
ConflictState.FIRST_CONFLICTING_RANGE);
result.add(2, theirsBeginB + commonPrefix,
theirsEndB - commonSuffix,
ConflictState.NEXT_CONFLICTING_RANGE);
break;
}
}
// Add the common lines at end of conflict
if (commonSuffix > 0)
result.add(1, oursEndB - commonSuffix, oursEndB,
ConflictState.NO_CONFLICT);
current = Math.max(oursEdit.getEndA(), theirsEdit.getEndA());
oursEdit = nextOursEdit;
theirsEdit = nextTheirsEdit;
}
}
// maybe we have a common part behind the last edit: copy it to the
// result
if (current < base.size()) {
result.add(0, current, base.size(), ConflictState.NO_CONFLICT);
}
return result;
}
/**
* Helper method which returns the next Edit for an Iterator over Edits.
* When there are no more edits left this method will return the constant
* END_EDIT.
*
* @param it
* the iterator for which the next edit should be returned
* @return the next edit from the iterator or END_EDIT if there no more
* edits
*/
private static Edit nextEdit(Iterator<Edit> it) {
return (it.hasNext() ? it.next() : END_EDIT);
}
}