From f7e233e4500e257a8d1f09d69c0966be266b6f1e Mon Sep 17 00:00:00 2001 From: Mathieu Cartaud Date: Mon, 22 May 2017 10:33:52 +0200 Subject: [PATCH] Support -merge attribute in binary macro The merger is now able to react to the use of the merge attribute. The value unset and the custom value 'binary' are handled (-merge and merge=binary) Since the specification of the merge attribute states that when the attribute is unset, ours version must be kept in case of a conflict, we don't overwrite the file but keep the local version. Bug: 517128 Change-Id: Ib5fbf17bdaf727bc5d0e106ce88f2620d9f87a6f Signed-off-by: Mathieu Cartaud --- .../attributes/merge/disabled_checked.gif | Bin 0 -> 106 bytes .../jgit/attributes/merge/enabled_checked.gif | Bin 0 -> 874 bytes .../merge/MergeGitAttributeTest.java | 585 ++++++++++++++++++ org.eclipse.jgit/.settings/.api_filters | 8 + .../eclipse/jgit/attributes/Attributes.java | 24 +- .../src/org/eclipse/jgit/lib/Constants.java | 16 +- .../org/eclipse/jgit/merge/ResolveMerger.java | 28 +- 7 files changed, 651 insertions(+), 10 deletions(-) create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/attributes/merge/disabled_checked.gif create mode 100644 org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/attributes/merge/enabled_checked.gif create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/attributes/merge/disabled_checked.gif b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/attributes/merge/disabled_checked.gif new file mode 100644 index 0000000000000000000000000000000000000000..47b9e321da2e01222a3a13bc70fb3fcae393fe59 GIT binary patch literal 106 zcmZ?wbhEHbqNKMQOXkKU?p=z3 eeKC^3PmWF&Pvnv~F!=z(eEa%mGEoW&4AubPeH7dP literal 0 HcmV?d00001 diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java new file mode 100644 index 000000000..665f47b2f --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java @@ -0,0 +1,585 @@ +/* + * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr) + * 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.attributes.merge; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.function.Consumer; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeResult; +import org.eclipse.jgit.api.MergeResult.MergeStatus; +import org.eclipse.jgit.api.errors.CheckoutConflictException; +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidMergeHeadsException; +import org.eclipse.jgit.api.errors.NoFilepatternException; +import org.eclipse.jgit.api.errors.NoHeadException; +import org.eclipse.jgit.api.errors.NoMessageException; +import org.eclipse.jgit.api.errors.WrongRepositoryStateException; +import org.eclipse.jgit.attributes.Attribute; +import org.eclipse.jgit.attributes.Attributes; +import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.junit.Ignore; +import org.junit.Test; + +public class MergeGitAttributeTest extends RepositoryTestCase { + + private static final String REFS_HEADS_RIGHT = "refs/heads/right"; + + private static final String REFS_HEADS_MASTER = "refs/heads/master"; + + private static final String REFS_HEADS_LEFT = "refs/heads/left"; + + private static final String DISABLE_CHECK_BRANCH = "refs/heads/disabled_checked"; + + private static final String ENABLE_CHECKED_BRANCH = "refs/heads/enabled_checked"; + + private static final String ENABLED_CHECKED_GIF = "enabled_checked.gif"; + + public Git createRepositoryBinaryConflict(Consumer initialCommit, + Consumer leftCommit, Consumer rightCommit) + throws NoFilepatternException, GitAPIException, NoWorkTreeException, + IOException { + // Set up a git whith conflict commits on images + Git git = new Git(db); + + // First commit + initialCommit.accept(git); + git.add().addFilepattern(".").call(); + RevCommit firstCommit = git.commit().setAll(true) + .setMessage("initial commit adding git attribute file").call(); + + // Create branch and add an icon Checked_Boxe (enabled_checked) + createBranch(firstCommit, REFS_HEADS_LEFT); + checkoutBranch(REFS_HEADS_LEFT); + leftCommit.accept(git); + git.add().addFilepattern(".").call(); + git.commit().setMessage("Left").call(); + + // Create a second branch from master Unchecked_Boxe + checkoutBranch(REFS_HEADS_MASTER); + createBranch(firstCommit, REFS_HEADS_RIGHT); + checkoutBranch(REFS_HEADS_RIGHT); + rightCommit.accept(git); + git.add().addFilepattern(".").call(); + git.commit().setMessage("Right").call(); + + checkoutBranch(REFS_HEADS_LEFT); + return git; + + } + + @Test + public void mergeTextualFile_NoAttr() throws NoWorkTreeException, + NoFilepatternException, GitAPIException, IOException { + try (Git git = createRepositoryBinaryConflict(g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + })) { + checkoutBranch(REFS_HEADS_LEFT); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve(REFS_HEADS_RIGHT)) + .call(); + assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus()); + + assertNull(mergeResult.getConflicts()); + + // Check that the image was not modified (not conflict marker added) + String result = read( + writeTrashFile("res.cat", "A\n" + "E\n" + "C\n" + "F\n")); + assertEquals(result, read(git.getRepository().getWorkTree().toPath() + .resolve("main.cat").toFile())); + } + } + + @Test + public void mergeTextualFile_UnsetMerge_Conflict() + throws NoWorkTreeException, NoFilepatternException, GitAPIException, + IOException { + try (Git git = createRepositoryBinaryConflict(g -> { + try { + writeTrashFile(".gitattributes", "*.cat -merge"); + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + })) { + // Check that the merge attribute is unset + assertAddMergeAttributeUnset(REFS_HEADS_LEFT, "main.cat"); + assertAddMergeAttributeUnset(REFS_HEADS_RIGHT, "main.cat"); + + checkoutBranch(REFS_HEADS_LEFT); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + + String catContent = read(git.getRepository().getWorkTree().toPath() + .resolve("main.cat").toFile()); + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve(REFS_HEADS_RIGHT)) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + assertEquals(catContent, read(git.getRepository().getWorkTree() + .toPath().resolve("main.cat").toFile())); + } + } + + @Test + public void mergeTextualFile_UnsetMerge_NoConflict() + throws NoWorkTreeException, NoFilepatternException, GitAPIException, + IOException { + try (Git git = createRepositoryBinaryConflict(g -> { + try { + writeTrashFile(".gitattributes", "*.txt -merge"); + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + })) { + // Check that the merge attribute is unset + assertAddMergeAttributeUndefined(REFS_HEADS_LEFT, "main.cat"); + assertAddMergeAttributeUndefined(REFS_HEADS_RIGHT, "main.cat"); + + checkoutBranch(REFS_HEADS_LEFT); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve(REFS_HEADS_RIGHT)) + .call(); + assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + String result = read( + writeTrashFile("res.cat", "A\n" + "E\n" + "C\n" + "F\n")); + assertEquals(result, read(git.getRepository().getWorkTree() + .toPath().resolve("main.cat").toFile())); + } + } + + @Test + public void mergeTextualFile_SetBinaryMerge_Conflict() + throws NoWorkTreeException, NoFilepatternException, GitAPIException, + IOException { + try (Git git = createRepositoryBinaryConflict(g -> { + try { + writeTrashFile(".gitattributes", "*.cat merge=binary"); + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + })) { + // Check that the merge attribute is set to binary + assertAddMergeAttributeCustom(REFS_HEADS_LEFT, "main.cat", + "binary"); + assertAddMergeAttributeCustom(REFS_HEADS_RIGHT, "main.cat", + "binary"); + + checkoutBranch(REFS_HEADS_LEFT); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + + String catContent = read(git.getRepository().getWorkTree().toPath() + .resolve("main.cat").toFile()); + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve(REFS_HEADS_RIGHT)) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + assertEquals(catContent, read(git.getRepository().getWorkTree() + .toPath().resolve("main.cat").toFile())); + } + } + + /* + * This test is commented because JGit add conflict markers in binary files. + * cf. https://www.eclipse.org/forums/index.php/t/1086511/ + */ + @Test + @Ignore + public void mergeBinaryFile_NoAttr_Conflict() throws IllegalStateException, + IOException, NoHeadException, ConcurrentRefUpdateException, + CheckoutConflictException, InvalidMergeHeadsException, + WrongRepositoryStateException, NoMessageException, GitAPIException { + + RevCommit disableCheckedCommit; + FileInputStream mergeResultFile = null; + // Set up a git with conflict commits on images + try (Git git = new Git(db)) { + // First commit + write(new File(db.getWorkTree(), ".gitattributes"), ""); + git.add().addFilepattern(".gitattributes").call(); + RevCommit firstCommit = git.commit() + .setMessage("initial commit adding git attribute file") + .call(); + + // Create branch and add an icon Checked_Boxe (enabled_checked) + createBranch(firstCommit, ENABLE_CHECKED_BRANCH); + checkoutBranch(ENABLE_CHECKED_BRANCH); + copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + git.commit().setMessage("enabled_checked commit").call(); + + // Create a second branch from master Unchecked_Boxe + checkoutBranch(REFS_HEADS_MASTER); + createBranch(firstCommit, DISABLE_CHECK_BRANCH); + checkoutBranch(DISABLE_CHECK_BRANCH); + copy("disabled_checked.gif", ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + disableCheckedCommit = git.commit() + .setMessage("disabled_checked commit").call(); + + // Check that the merge attribute is unset + assertAddMergeAttributeUndefined(ENABLE_CHECKED_BRANCH, + ENABLED_CHECKED_GIF); + assertAddMergeAttributeUndefined(DISABLE_CHECK_BRANCH, + ENABLED_CHECKED_GIF); + + checkoutBranch(ENABLE_CHECKED_BRANCH); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + MergeResult mergeResult = git.merge().include(disableCheckedCommit) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (no conflict marker added) + mergeResultFile = new FileInputStream( + db.getWorkTree().toPath().resolve(ENABLED_CHECKED_GIF) + .toFile()); + assertTrue(contentEquals( + getClass().getResourceAsStream(ENABLED_CHECKED_GIF), + mergeResultFile)); + } finally { + if (mergeResultFile != null) { + mergeResultFile.close(); + } + } + } + + @Test + public void mergeBinaryFile_UnsetMerge_Conflict() + throws IllegalStateException, + IOException, NoHeadException, ConcurrentRefUpdateException, + CheckoutConflictException, InvalidMergeHeadsException, + WrongRepositoryStateException, NoMessageException, GitAPIException { + + RevCommit disableCheckedCommit; + FileInputStream mergeResultFile = null; + // Set up a git whith conflict commits on images + try (Git git = new Git(db)) { + // First commit + write(new File(db.getWorkTree(), ".gitattributes"), "*.gif -merge"); + git.add().addFilepattern(".gitattributes").call(); + RevCommit firstCommit = git.commit() + .setMessage("initial commit adding git attribute file") + .call(); + + // Create branch and add an icon Checked_Boxe (enabled_checked) + createBranch(firstCommit, ENABLE_CHECKED_BRANCH); + checkoutBranch(ENABLE_CHECKED_BRANCH); + copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + git.commit().setMessage("enabled_checked commit").call(); + + // Create a second branch from master Unchecked_Boxe + checkoutBranch(REFS_HEADS_MASTER); + createBranch(firstCommit, DISABLE_CHECK_BRANCH); + checkoutBranch(DISABLE_CHECK_BRANCH); + copy("disabled_checked.gif", ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + disableCheckedCommit = git.commit() + .setMessage("disabled_checked commit").call(); + + // Check that the merge attribute is unset + assertAddMergeAttributeUnset(ENABLE_CHECKED_BRANCH, + ENABLED_CHECKED_GIF); + assertAddMergeAttributeUnset(DISABLE_CHECK_BRANCH, + ENABLED_CHECKED_GIF); + + checkoutBranch(ENABLE_CHECKED_BRANCH); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + MergeResult mergeResult = git.merge().include(disableCheckedCommit) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + mergeResultFile = new FileInputStream(db.getWorkTree().toPath() + .resolve(ENABLED_CHECKED_GIF).toFile()); + assertTrue(contentEquals( + getClass().getResourceAsStream(ENABLED_CHECKED_GIF), + mergeResultFile)); + } finally { + if (mergeResultFile != null) { + mergeResultFile.close(); + } + } + } + + @Test + public void mergeBinaryFile_SetMerge_Conflict() + throws IllegalStateException, IOException, NoHeadException, + ConcurrentRefUpdateException, CheckoutConflictException, + InvalidMergeHeadsException, WrongRepositoryStateException, + NoMessageException, GitAPIException { + + RevCommit disableCheckedCommit; + FileInputStream mergeResultFile = null; + // Set up a git whith conflict commits on images + try (Git git = new Git(db)) { + // First commit + write(new File(db.getWorkTree(), ".gitattributes"), "*.gif merge"); + git.add().addFilepattern(".gitattributes").call(); + RevCommit firstCommit = git.commit() + .setMessage("initial commit adding git attribute file") + .call(); + + // Create branch and add an icon Checked_Boxe (enabled_checked) + createBranch(firstCommit, ENABLE_CHECKED_BRANCH); + checkoutBranch(ENABLE_CHECKED_BRANCH); + copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + git.commit().setMessage("enabled_checked commit").call(); + + // Create a second branch from master Unchecked_Boxe + checkoutBranch(REFS_HEADS_MASTER); + createBranch(firstCommit, DISABLE_CHECK_BRANCH); + checkoutBranch(DISABLE_CHECK_BRANCH); + copy("disabled_checked.gif", ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + disableCheckedCommit = git.commit() + .setMessage("disabled_checked commit").call(); + + // Check that the merge attribute is set + assertAddMergeAttributeSet(ENABLE_CHECKED_BRANCH, + ENABLED_CHECKED_GIF); + assertAddMergeAttributeSet(DISABLE_CHECK_BRANCH, + ENABLED_CHECKED_GIF); + + checkoutBranch(ENABLE_CHECKED_BRANCH); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + MergeResult mergeResult = git.merge().include(disableCheckedCommit) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + mergeResultFile = new FileInputStream(db.getWorkTree().toPath() + .resolve(ENABLED_CHECKED_GIF).toFile()); + assertFalse(contentEquals( + getClass().getResourceAsStream(ENABLED_CHECKED_GIF), + mergeResultFile)); + } finally { + if (mergeResultFile != null) { + mergeResultFile.close(); + } + } + } + + /* + * Copied from org.apache.commons.io.IOUtils + */ + private boolean contentEquals(InputStream input1, InputStream input2) + throws IOException { + if (input1 == input2) { + return true; + } + if (!(input1 instanceof BufferedInputStream)) { + input1 = new BufferedInputStream(input1); + } + if (!(input2 instanceof BufferedInputStream)) { + input2 = new BufferedInputStream(input2); + } + + int ch = input1.read(); + while (-1 != ch) { + final int ch2 = input2.read(); + if (ch != ch2) { + return false; + } + ch = input1.read(); + } + + final int ch2 = input2.read(); + return ch2 == -1; + } + + private void assertAddMergeAttributeUnset(String branch, String fileName) + throws IllegalStateException, IOException { + checkoutBranch(branch); + + try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) { + treeWaklEnableChecked.addTree(new FileTreeIterator(db)); + treeWaklEnableChecked.setFilter(PathFilter.create(fileName)); + + assertTrue(treeWaklEnableChecked.next()); + Attributes attributes = treeWaklEnableChecked.getAttributes(); + Attribute mergeAttribute = attributes.get("merge"); + assertNotNull(mergeAttribute); + assertEquals(Attribute.State.UNSET, mergeAttribute.getState()); + } + } + + private void assertAddMergeAttributeSet(String branch, String fileName) + throws IllegalStateException, IOException { + checkoutBranch(branch); + + try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) { + treeWaklEnableChecked.addTree(new FileTreeIterator(db)); + treeWaklEnableChecked.setFilter(PathFilter.create(fileName)); + + assertTrue(treeWaklEnableChecked.next()); + Attributes attributes = treeWaklEnableChecked.getAttributes(); + Attribute mergeAttribute = attributes.get("merge"); + assertNotNull(mergeAttribute); + assertEquals(Attribute.State.SET, mergeAttribute.getState()); + } + } + + private void assertAddMergeAttributeUndefined(String branch, + String fileName) throws IllegalStateException, IOException { + checkoutBranch(branch); + + try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) { + treeWaklEnableChecked.addTree(new FileTreeIterator(db)); + treeWaklEnableChecked.setFilter(PathFilter.create(fileName)); + + assertTrue(treeWaklEnableChecked.next()); + Attributes attributes = treeWaklEnableChecked.getAttributes(); + Attribute mergeAttribute = attributes.get("merge"); + assertNull(mergeAttribute); + } + } + + private void assertAddMergeAttributeCustom(String branch, String fileName, + String value) throws IllegalStateException, IOException { + checkoutBranch(branch); + + try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) { + treeWaklEnableChecked.addTree(new FileTreeIterator(db)); + treeWaklEnableChecked.setFilter(PathFilter.create(fileName)); + + assertTrue(treeWaklEnableChecked.next()); + Attributes attributes = treeWaklEnableChecked.getAttributes(); + Attribute mergeAttribute = attributes.get("merge"); + assertNotNull(mergeAttribute); + assertEquals(Attribute.State.CUSTOM, mergeAttribute.getState()); + assertEquals(value, mergeAttribute.getValue()); + } + } + + private void copy(String resourcePath, String resourceNewName, + String pathInRepo) throws IOException { + InputStream input = getClass().getResourceAsStream(resourcePath); + Files.copy(input, db.getWorkTree().toPath().resolve(pathInRepo) + .resolve(resourceNewName)); + } + +} diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters index 671988c45..da7c122f1 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -24,4 +24,12 @@ + + + + + + + + diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attributes.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attributes.java index 0810e3168..d3826b3d9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attributes.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attributes.java @@ -1,5 +1,6 @@ /* - * Copyright (C) 2015, Ivan Motsch + * Copyright (C) 2015, Ivan Motsch , + * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr) * * This program and the accompanying materials are made available * under the terms of the Eclipse Distribution License v1.0 which @@ -48,6 +49,7 @@ import java.util.Map; import org.eclipse.jgit.attributes.Attribute.State; +import org.eclipse.jgit.lib.Constants; /** * Represents a set of attributes for a path @@ -170,6 +172,26 @@ public String getValue(String key) { return a != null ? a.getValue() : null; } + /** + * Test if the given attributes implies to handle the related entry as a + * binary file (i.e. if the entry has an -merge or a merge=binary attribute) + * or if it can be content merged. + * + * @return true if the entry can be content merged, + * false otherwise + * @since 4.9 + */ + public boolean canBeContentMerged() { + if (isUnset(Constants.ATTR_MERGE)) { + return false; + } else if (isCustom(Constants.ATTR_MERGE) + && getValue(Constants.ATTR_MERGE) + .equals(Constants.ATTR_BUILTIN_BINARY_MERGER)) { + return false; + } + return true; + } + @Override public String toString() { StringBuilder buf = new StringBuilder(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java index ff80672f8..bb7316dc5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java @@ -1,7 +1,7 @@ /* * Copyright (C) 2008, Google Inc. * Copyright (C) 2008, Robin Rosenberg - * Copyright (C) 2006-2012, Shawn O. Pearce + * Copyright (C) 2006-2017, Shawn O. Pearce * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -428,6 +428,20 @@ public final class Constants { */ public static final String HOOKS = "hooks"; + /** + * Merge attribute. + * + * @since 4.9 + */ + public static final String ATTR_MERGE = "merge"; //$NON-NLS-1$ + + /** + * Binary value for custom merger. + * + * @since 4.9 + */ + public static final String ATTR_BUILTIN_BINARY_MERGER = "binary"; //$NON-NLS-1$ + /** * Create a new digest function for objects. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java index 1aac352d7..e77ad953d 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -2,6 +2,7 @@ * Copyright (C) 2010, Christian Halstrick , * Copyright (C) 2010-2012, Matthias Sohn * Copyright (C) 2012, Research In Motion Limited + * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr) * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -67,6 +68,7 @@ import java.util.List; import java.util.Map; +import org.eclipse.jgit.attributes.Attributes; import org.eclipse.jgit.diff.DiffAlgorithm; import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm; import org.eclipse.jgit.diff.RawText; @@ -429,9 +431,10 @@ private DirCacheEntry keep(DirCacheEntry e) { } /** - * Processes one path and tries to merge. This method will do all do all - * trivial (not content) merges and will also detect if a merge will fail. - * The merge will fail when one of the following is true + * Processes one path and tries to merge taking git attributes in account. + * This method will do all trivial (not content) merges and will also detect + * if a merge will fail. The merge will fail when one of the following is + * true *
    *
  • the index entry does not match the entry in ours. When merging one * branch into the current HEAD, ours will point to HEAD and theirs will @@ -463,6 +466,8 @@ private DirCacheEntry keep(DirCacheEntry e) { * @param ignoreConflicts * see * {@link ResolveMerger#mergeTrees(AbstractTreeIterator, RevTree, RevTree, boolean)} + * @param attributes + * the attributes defined for this entry * @return false if the merge will fail because the index entry * didn't match ours or the working-dir file was dirty and a * conflict occurred @@ -470,12 +475,12 @@ private DirCacheEntry keep(DirCacheEntry e) { * @throws IncorrectObjectTypeException * @throws CorruptObjectException * @throws IOException - * @since 3.5 + * @since 4.9 */ protected boolean processEntry(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, DirCacheBuildIterator index, WorkingTreeIterator work, - boolean ignoreConflicts) + boolean ignoreConflicts, Attributes attributes) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { enterSubtree = true; @@ -627,7 +632,8 @@ protected boolean processEntry(CanonicalTreeParser base, return false; // Don't attempt to resolve submodule link conflicts - if (isGitLink(modeO) || isGitLink(modeT)) { + if (isGitLink(modeO) || isGitLink(modeT) + || !attributes.canBeContentMerged()) { add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0); add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0); add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, 0, 0); @@ -636,8 +642,9 @@ protected boolean processEntry(CanonicalTreeParser base, } MergeResult result = contentMerge(base, ours, theirs); - if (ignoreConflicts) + if (ignoreConflicts) { result.setContainsConflicts(false); + } updateIndex(base, ours, theirs, result); if (result.containsConflicts() && !ignoreConflicts) unmergedPaths.add(tw.getPathString()); @@ -760,6 +767,7 @@ private void updateIndex(CanonicalTreeParser base, MergeResult result) throws FileNotFoundException, IOException { File mergedFile = !inCore ? writeMergedFile(result) : null; + if (result.containsConflicts()) { // A conflict occurred, the file will contain conflict markers // the index will be populated with the three stages and the @@ -1091,6 +1099,8 @@ protected boolean mergeTrees(AbstractTreeIterator baseTree, protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts) throws IOException { boolean hasWorkingTreeIterator = tw.getTreeCount() > T_FILE; + boolean hasAttributeNodeProvider = treeWalk + .getAttributesNodeProvider() != null; while (treeWalk.next()) { if (!processEntry( treeWalk.getTree(T_BASE, CanonicalTreeParser.class), @@ -1098,7 +1108,9 @@ protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts) treeWalk.getTree(T_THEIRS, CanonicalTreeParser.class), treeWalk.getTree(T_INDEX, DirCacheBuildIterator.class), hasWorkingTreeIterator ? treeWalk.getTree(T_FILE, - WorkingTreeIterator.class) : null, ignoreConflicts)) { + WorkingTreeIterator.class) : null, + ignoreConflicts, hasAttributeNodeProvider + ? treeWalk.getAttributes() : new Attributes())) { cleanUp(); return false; }