import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.lib.Constants;
+import org.junit.Assume;
import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.runner.RunWith;
+@RunWith(Theories.class)
public class MergeAlgorithmTest {
MergeFormatter fmt=new MergeFormatter();
+ private final boolean newlineAtEnd;
+
+ @DataPoints
+ public static boolean[] newlineAtEndDataPoints = { false, true };
+
+ public MergeAlgorithmTest(boolean newlineAtEnd) {
+ this.newlineAtEnd = newlineAtEnd;
+ }
+
/**
* Check for a conflict where the second text was changed similar to the
* first one, but the second texts modification covers one more line.
}
@Test
- public void testSeperateModifications() throws IOException {
+ public void testSeparateModifications() throws IOException {
assertEquals(t("aZcYe"), merge("abcde", "aZcde", "abcYe"));
}
+ @Test
+ public void testBlankLines() throws IOException {
+ assertEquals(t("aZc\nYe"), merge("abc\nde", "aZc\nde", "abc\nYe"));
+ }
+
/**
* Test merging two contents which do one similar modification and one
- * insertion is only done by one side. Between modification and insertion is
- * a block which is common between the two contents and the common base
+ * insertion is only done by one side, in the middle. Between modification
+ * and insertion is a block which is common between the two contents and the
+ * common base
*
* @throws IOException
*/
@Test
public void testTwoSimilarModsAndOneInsert() throws IOException {
- assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ"));
assertEquals(t("aBcDde"), merge("abcde", "aBcde", "aBcDde"));
- assertEquals(t("IAJ"), merge("iA", "IA", "IAJ"));
- assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ"));
assertEquals(t("IAAAJCAB"), merge("iACAB", "IACAB", "IAAAJCAB"));
assertEquals(t("HIAAAJCAB"), merge("HiACAB", "HIACAB", "HIAAAJCAB"));
assertEquals(t("AGADEFHIAAAJCAB"),
merge("AGADEFHiACAB", "AGADEFHIACAB", "AGADEFHIAAAJCAB"));
+ }
+ /**
+ * Test merging two contents which do one similar modification and one
+ * insertion is only done by one side, at the end. Between modification and
+ * insertion is a block which is common between the two contents and the
+ * common base
+ *
+ * @throws IOException
+ */
+ @Test
+ public void testTwoSimilarModsAndOneInsertAtEnd() throws IOException {
+ Assume.assumeTrue(newlineAtEnd);
+ assertEquals(t("IAAJ"), merge("iA", "IA", "IAAJ"));
+ assertEquals(t("IAJ"), merge("iA", "IA", "IAJ"));
+ assertEquals(t("IAAAJ"), merge("iA", "IA", "IAAAJ"));
+ }
+
+ @Test
+ public void testTwoSimilarModsAndOneInsertAtEndNoNewlineAtEnd()
+ throws IOException {
+ Assume.assumeFalse(newlineAtEnd);
+ assertEquals(t("I<A=AAJ>"), merge("iA", "IA", "IAAJ"));
+ assertEquals(t("I<A=AJ>"), merge("iA", "IA", "IAJ"));
+ assertEquals(t("I<A=AAAJ>"), merge("iA", "IA", "IAAAJ"));
}
/**
return new String(bo.toByteArray(), Constants.CHARACTER_ENCODING);
}
- public static String t(String text) {
+ public String t(String text) {
StringBuilder r = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
break;
default:
r.append(c);
- r.append('\n');
+ if (newlineAtEnd || i < text.length() - 1)
+ r.append('\n');
}
}
return r.toString();
}
- public static RawText T(String text) {
+ public RawText T(String text) {
return new RawText(Constants.encode(t(text)));
}
}
MergeResult mergeRes = git.merge().setStrategy(strategy)
.include(masterCommit).call();
assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
- assertEquals("[d/1, mode:100644, content:1master\n2\n3side\n]",
+ assertEquals("[d/1, mode:100644, content:1master\n2\n3side]",
indexState(CONTENT));
}
assertEquals(MergeStrategy.RECURSIVE, strategy);
assertEquals(MergeResult.MergeStatus.MERGED,
mergeResult.getMergeStatus());
- assertEquals("1master2\n2\n3side2\n", read("1"));
+ assertEquals("1master2\n2\n3side2", read("1"));
} catch (JGitInternalException e) {
assertEquals(MergeStrategy.RESOLVE, strategy);
assertTrue(e.getCause() instanceof NoMergeBaseException);
assertEquals(
"[0, mode:100644, content:master]" //
+ "[1, mode:100644, content:side]" //
- + "[2, mode:100644, content:1master\n2\n3side\n]" //
+ + "[2, mode:100644, content:1master\n2\n3side]" //
+ "[3, mode:100644, stage:1, content:orig][3, mode:100644, stage:2, content:side][3, mode:100644, stage:3, content:master]" //
+ "[4, mode:100644, content:orig]", //
indexState(CONTENT));
--- /dev/null
+/*
+ * Copyright (C) 2014, André de Oliveira <andre.oliveira@liferay.com>
+ *
+ * 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.merge;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An output stream which is aware of newlines and can be asked to begin a new
+ * line if not already in one.
+ */
+class EolAwareOutputStream extends OutputStream {
+ private final OutputStream out;
+
+ private boolean bol = true;
+
+ /**
+ * Initialize a new EOL aware stream.
+ *
+ * @param out
+ * stream to output all writes to.
+ */
+ EolAwareOutputStream(OutputStream out) {
+ this.out = out;
+ }
+
+ /**
+ * Begin a new line if not already in one.
+ *
+ * @exception IOException
+ * if an I/O error occurs.
+ */
+ void beginln() throws IOException {
+ if (!bol)
+ write('\n');
+ }
+
+ /** @return true if a new line has just begun. */
+ boolean isBeginln() {
+ return bol;
+ }
+
+ @Override
+ public void write(int val) throws IOException {
+ out.write(val);
+ bol = (val == '\n');
+ }
+}
import java.util.List;
import org.eclipse.jgit.diff.RawText;
-import org.eclipse.jgit.merge.MergeChunk.ConflictState;
/**
* A class to convert merge results into a Git conformant textual presentation
*/
public void formatMerge(OutputStream out, MergeResult<RawText> res,
List<String> seqName, String charsetName) throws IOException {
- String lastConflictingName = null; // is set to non-null whenever we are
- // in a conflict
- boolean threeWayMerge = (res.getSequences().size() == 3);
- for (MergeChunk chunk : res) {
- RawText seq = res.getSequences().get(chunk.getSequenceIndex());
- if (lastConflictingName != null
- && chunk.getConflictState() != ConflictState.NEXT_CONFLICTING_RANGE) {
- // found the end of an conflict
- out.write((">>>>>>> " + lastConflictingName + "\n").getBytes(charsetName)); //$NON-NLS-1$ //$NON-NLS-2$
- lastConflictingName = null;
- }
- if (chunk.getConflictState() == ConflictState.FIRST_CONFLICTING_RANGE) {
- // found the start of an conflict
- out.write(("<<<<<<< " + seqName.get(chunk.getSequenceIndex()) + //$NON-NLS-1$
- "\n").getBytes(charsetName)); //$NON-NLS-1$
- lastConflictingName = seqName.get(chunk.getSequenceIndex());
- } else if (chunk.getConflictState() == ConflictState.NEXT_CONFLICTING_RANGE) {
- // found another conflicting chunk
-
- /*
- * In case of a non-three-way merge I'll add the name of the
- * conflicting chunk behind the equal signs. I also append the
- * name of the last conflicting chunk after the ending
- * greater-than signs. If somebody knows a better notation to
- * present non-three-way merges - feel free to correct here.
- */
- lastConflictingName = seqName.get(chunk.getSequenceIndex());
- out.write((threeWayMerge ? "=======\n" : "======= " //$NON-NLS-1$ //$NON-NLS-2$
- + lastConflictingName + "\n").getBytes(charsetName)); //$NON-NLS-1$
- }
- // the lines with conflict-metadata are written. Now write the chunk
- for (int i = chunk.getBegin(); i < chunk.getEnd(); i++) {
- seq.writeLine(out, i);
- out.write('\n');
- }
- }
- // one possible leftover: if the merge result ended with a conflict we
- // have to close the last conflict here
- if (lastConflictingName != null) {
- out.write((">>>>>>> " + lastConflictingName + "\n").getBytes(charsetName)); //$NON-NLS-1$ //$NON-NLS-2$
- }
+ new MergeFormatterPass(out, res, seqName, charsetName).formatMerge();
}
/**
--- /dev/null
+/*
+ * Copyright (C) 2009, Christian Halstrick <christian.halstrick@sap.com>
+ * Copyright (C) 2014, André de Oliveira <andre.oliveira@liferay.com>
+ * 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.merge;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.merge.MergeChunk.ConflictState;
+
+class MergeFormatterPass {
+
+ private final EolAwareOutputStream out;
+
+ private final MergeResult<RawText> res;
+
+ private final List<String> seqName;
+
+ private final String charsetName;
+
+ private final boolean threeWayMerge;
+
+ private String lastConflictingName; // is set to non-null whenever we are in
+ // a conflict
+
+ MergeFormatterPass(OutputStream out, MergeResult<RawText> res, List<String> seqName,
+ String charsetName) {
+ this.out = new EolAwareOutputStream(out);
+ this.res = res;
+ this.seqName = seqName;
+ this.charsetName = charsetName;
+ this.threeWayMerge = (res.getSequences().size() == 3);
+ }
+
+ void formatMerge() throws IOException {
+ boolean missingNewlineAtEnd = false;
+ for (MergeChunk chunk : res) {
+ RawText seq = res.getSequences().get(chunk.getSequenceIndex());
+ writeConflictMetadata(chunk);
+ // the lines with conflict-metadata are written. Now write the chunk
+ for (int i = chunk.getBegin(); i < chunk.getEnd(); i++)
+ writeLine(seq, i);
+ missingNewlineAtEnd = seq.isMissingNewlineAtEnd();
+ }
+ // one possible leftover: if the merge result ended with a conflict we
+ // have to close the last conflict here
+ if (lastConflictingName != null)
+ writeConflictEnd();
+ if (!missingNewlineAtEnd)
+ out.beginln();
+ }
+
+ private void writeConflictMetadata(MergeChunk chunk) throws IOException {
+ if (lastConflictingName != null
+ && chunk.getConflictState() != ConflictState.NEXT_CONFLICTING_RANGE) {
+ // found the end of an conflict
+ writeConflictEnd();
+ }
+ if (chunk.getConflictState() == ConflictState.FIRST_CONFLICTING_RANGE) {
+ // found the start of an conflict
+ writeConflictStart(chunk);
+ } else if (chunk.getConflictState() == ConflictState.NEXT_CONFLICTING_RANGE) {
+ // found another conflicting chunk
+ writeConflictChange(chunk);
+ }
+ }
+
+ private void writeConflictEnd() throws IOException {
+ writeln(">>>>>>> " + lastConflictingName); //$NON-NLS-1$
+ lastConflictingName = null;
+ }
+
+ private void writeConflictStart(MergeChunk chunk) throws IOException {
+ lastConflictingName = seqName.get(chunk.getSequenceIndex());
+ writeln("<<<<<<< " + lastConflictingName); //$NON-NLS-1$
+ }
+
+ private void writeConflictChange(MergeChunk chunk) throws IOException {
+ /*
+ * In case of a non-three-way merge I'll add the name of the conflicting
+ * chunk behind the equal signs. I also append the name of the last
+ * conflicting chunk after the ending greater-than signs. If somebody
+ * knows a better notation to present non-three-way merges - feel free
+ * to correct here.
+ */
+ lastConflictingName = seqName.get(chunk.getSequenceIndex());
+ writeln(threeWayMerge ? "=======" : "======= " //$NON-NLS-1$ //$NON-NLS-2$
+ + lastConflictingName);
+ }
+
+ private void writeln(String s) throws IOException {
+ out.beginln();
+ out.write((s + "\n").getBytes(charsetName)); //$NON-NLS-1$
+ }
+
+ private void writeLine(RawText seq, int i) throws IOException {
+ out.beginln();
+ seq.writeLine(out, i);
+ // still BOL? It was a blank line. But writeLine won't lf, so we do.
+ if (out.isBeginln())
+ out.write('\n');
+ }
+}
\ No newline at end of file