diff options
5 files changed, 288 insertions, 54 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java index 8a33425b1d..d4a3d62dad 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergeAlgorithmTest.java @@ -51,11 +51,25 @@ import java.io.IOException; 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. @@ -174,28 +188,55 @@ public class MergeAlgorithmTest { } @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")); } /** @@ -225,7 +266,7 @@ public class MergeAlgorithmTest { 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); @@ -241,13 +282,14 @@ public class MergeAlgorithmTest { 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))); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/ResolveMergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/ResolveMergerTest.java index dd06168c30..478a93b770 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/ResolveMergerTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/ResolveMergerTest.java @@ -184,7 +184,7 @@ public class ResolveMergerTest extends RepositoryTestCase { 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)); } @@ -561,7 +561,7 @@ public class ResolveMergerTest extends RepositoryTestCase { 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); @@ -697,7 +697,7 @@ public class ResolveMergerTest extends RepositoryTestCase { 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)); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/EolAwareOutputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/EolAwareOutputStream.java new file mode 100644 index 0000000000..3fd2374f37 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/EolAwareOutputStream.java @@ -0,0 +1,87 @@ +/* + * 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'); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java index eed1dcfa88..977f95341f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatter.java @@ -49,7 +49,6 @@ import java.util.ArrayList; 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 @@ -78,47 +77,7 @@ public class MergeFormatter { */ 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(); } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java new file mode 100644 index 0000000000..0345921bdb --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java @@ -0,0 +1,146 @@ +/* + * 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 |