diff --git a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java index 8964310e4..3e83c8ef4 100644 --- a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java +++ b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021, Thomas Wolf and others + * Copyright (C) 2021, 2022 Thomas Wolf 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 @@ -67,6 +67,27 @@ public void setUp() throws Exception { config.save(); } + @Test + public void testBranchSwitch() throws Exception { + git.branchCreate().setName("abranch").call(); + git.checkout().setName("abranch").call(); + File aFile = writeTrashFile("a.bin", "aaa"); + writeTrashFile(".gitattributes", "a.bin filter=lfs"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("acommit").call(); + git.checkout().setName("master").call(); + git.branchCreate().setName("bbranch").call(); + git.checkout().setName("bbranch").call(); + File bFile = writeTrashFile("b.bin", "bbb"); + writeTrashFile(".gitattributes", "b.bin filter=lfs"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("bcommit").call(); + git.checkout().setName("abranch").call(); + checkFile(aFile, "aaa"); + git.checkout().setName("bbranch").call(); + checkFile(bFile, "bbb"); + } + @Test public void checkoutNonLfsPointer() throws Exception { String content = "size_t\nsome_function(void* ptr);\n"; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java index 36f94fbd2..89d31c3e8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016, Christian Halstrick and others + * Copyright (C) 2016, 2022 Christian Halstrick 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 @@ -10,12 +10,17 @@ package org.eclipse.jgit.util; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.HashSet; +import java.util.Set; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.attributes.FilterCommand; import org.eclipse.jgit.attributes.FilterCommandFactory; @@ -86,6 +91,14 @@ public void setUp() throws Exception { secondCommit = git.commit().setMessage("Second commit").call(); } + @Override + public void tearDown() throws Exception { + Set existingFilters = new HashSet<>( + FilterCommandRegistry.getRegisteredFilterCommands()); + existingFilters.forEach(FilterCommandRegistry::unregister); + super.tearDown(); + } + @Test public void testBuiltinCleanFilter() throws IOException, GitAPIException { @@ -217,4 +230,133 @@ public void testBuiltinCleanAndSmudgeFilter() throws IOException, GitAPIExceptio config.save(); } + @Test + public void testBranchSwitch() throws Exception { + String builtinCommandPrefix = "jgit://builtin/test/"; + FilterCommandRegistry.register(builtinCommandPrefix + "smudge", + new TestCommandFactory('s')); + FilterCommandRegistry.register(builtinCommandPrefix + "clean", + new TestCommandFactory('c')); + StoredConfig config = git.getRepository().getConfig(); + config.setString("filter", "test", "smudge", + builtinCommandPrefix + "smudge"); + config.setString("filter", "test", "clean", + builtinCommandPrefix + "clean"); + config.save(); + // We're on the test branch + File aFile = writeTrashFile("a.txt", "a"); + writeTrashFile(".gitattributes", "a.txt filter=test"); + File cFile = writeTrashFile("cc/c.txt", "C"); + writeTrashFile("cc/.gitattributes", "c.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On test").call(); + git.checkout().setName("master").call(); + git.branchCreate().setName("other").call(); + git.checkout().setName("other").call(); + writeTrashFile("b.txt", "b"); + writeTrashFile(".gitattributes", "b.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On other").call(); + git.checkout().setName("test").call(); + checkFile(aFile, "scsa"); + checkFile(cFile, "scsC"); + } + + @Test + public void testCheckoutSingleFile() throws Exception { + String builtinCommandPrefix = "jgit://builtin/test/"; + FilterCommandRegistry.register(builtinCommandPrefix + "smudge", + new TestCommandFactory('s')); + FilterCommandRegistry.register(builtinCommandPrefix + "clean", + new TestCommandFactory('c')); + StoredConfig config = git.getRepository().getConfig(); + config.setString("filter", "test", "smudge", + builtinCommandPrefix + "smudge"); + config.setString("filter", "test", "clean", + builtinCommandPrefix + "clean"); + config.save(); + // We're on the test branch + File aFile = writeTrashFile("a.txt", "a"); + File attributes = writeTrashFile(".gitattributes", "a.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On test").call(); + git.checkout().setName("master").call(); + git.branchCreate().setName("other").call(); + git.checkout().setName("other").call(); + writeTrashFile("b.txt", "b"); + writeTrashFile(".gitattributes", "b.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On other").call(); + git.checkout().setName("master").call(); + assertFalse(aFile.exists()); + assertFalse(attributes.exists()); + git.checkout().setStartPoint("test").addPath("a.txt").call(); + checkFile(aFile, "scsa"); + } + + @Test + public void testCheckoutSingleFile2() throws Exception { + String builtinCommandPrefix = "jgit://builtin/test/"; + FilterCommandRegistry.register(builtinCommandPrefix + "smudge", + new TestCommandFactory('s')); + FilterCommandRegistry.register(builtinCommandPrefix + "clean", + new TestCommandFactory('c')); + StoredConfig config = git.getRepository().getConfig(); + config.setString("filter", "test", "smudge", + builtinCommandPrefix + "smudge"); + config.setString("filter", "test", "clean", + builtinCommandPrefix + "clean"); + config.save(); + // We're on the test branch + File aFile = writeTrashFile("a.txt", "a"); + File attributes = writeTrashFile(".gitattributes", "a.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On test").call(); + git.checkout().setName("master").call(); + git.branchCreate().setName("other").call(); + git.checkout().setName("other").call(); + writeTrashFile("b.txt", "b"); + writeTrashFile(".gitattributes", "b.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On other").call(); + git.checkout().setName("master").call(); + assertFalse(aFile.exists()); + assertFalse(attributes.exists()); + writeTrashFile(".gitattributes", ""); + git.checkout().setStartPoint("test").addPath("a.txt").call(); + checkFile(aFile, "scsa"); + } + + @Test + public void testMerge() throws Exception { + String builtinCommandPrefix = "jgit://builtin/test/"; + FilterCommandRegistry.register(builtinCommandPrefix + "smudge", + new TestCommandFactory('s')); + FilterCommandRegistry.register(builtinCommandPrefix + "clean", + new TestCommandFactory('c')); + StoredConfig config = git.getRepository().getConfig(); + config.setString("filter", "test", "smudge", + builtinCommandPrefix + "smudge"); + config.setString("filter", "test", "clean", + builtinCommandPrefix + "clean"); + config.save(); + // We're on the test branch. Set up two branches that are expected to + // merge cleanly. + File aFile = writeTrashFile("a.txt", "a"); + writeTrashFile(".gitattributes", "a.txt filter=test"); + git.add().addFilepattern(".").call(); + RevCommit aCommit = git.commit().setMessage("On test").call(); + git.checkout().setName("master").call(); + assertFalse(aFile.exists()); + git.branchCreate().setName("other").call(); + git.checkout().setName("other").call(); + writeTrashFile("b/b.txt", "b"); + writeTrashFile("b/.gitattributes", "b.txt filter=test"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("On other").call(); + MergeResult result = git.merge().include(aCommit).call(); + assertEquals(MergeResult.MergeStatus.MERGED, result.getMergeStatus()); + checkFile(aFile, "scsa"); + } + } diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters index e026e31dc..00b89a4b3 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -39,6 +39,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java index 638dd827e..7ec78597f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java @@ -1,43 +1,11 @@ /* - * Copyright (C) 2015, Ivan Motsch + * Copyright (C) 2015, 2022 Ivan Motsch and others * - * 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 + * 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. * - * 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. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.attributes; @@ -46,6 +14,7 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; +import java.util.function.Supplier; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.attributes.Attribute.State; @@ -84,6 +53,8 @@ public class AttributesHandler { private final TreeWalk treeWalk; + private final Supplier attributesTree; + private final AttributesNode globalNode; private final AttributesNode infoNode; @@ -98,22 +69,41 @@ public class AttributesHandler { * @param treeWalk * a {@link org.eclipse.jgit.treewalk.TreeWalk} * @throws java.io.IOException + * @deprecated since 6.1, use {@link #AttributesHandler(TreeWalk, Supplier)} + * instead */ + @Deprecated public AttributesHandler(TreeWalk treeWalk) throws IOException { + this(treeWalk, () -> treeWalk.getTree(CanonicalTreeParser.class)); + } + + /** + * Create an {@link org.eclipse.jgit.attributes.AttributesHandler} with + * default rules as well as merged rules from global, info and worktree root + * attributes + * + * @param treeWalk + * a {@link org.eclipse.jgit.treewalk.TreeWalk} + * @param attributesTree + * the tree to read .gitattributes from + * @throws java.io.IOException + * @since 6.1 + */ + public AttributesHandler(TreeWalk treeWalk, + Supplier attributesTree) throws IOException { this.treeWalk = treeWalk; - AttributesNodeProvider attributesNodeProvider =treeWalk.getAttributesNodeProvider(); + this.attributesTree = attributesTree; + AttributesNodeProvider attributesNodeProvider = treeWalk + .getAttributesNodeProvider(); this.globalNode = attributesNodeProvider != null ? attributesNodeProvider.getGlobalAttributesNode() : null; this.infoNode = attributesNodeProvider != null ? attributesNodeProvider.getInfoAttributesNode() : null; AttributesNode rootNode = attributesNode(treeWalk, - rootOf( - treeWalk.getTree(WorkingTreeIterator.class)), - rootOf( - treeWalk.getTree(DirCacheIterator.class)), - rootOf(treeWalk - .getTree(CanonicalTreeParser.class))); + rootOf(treeWalk.getTree(WorkingTreeIterator.class)), + rootOf(treeWalk.getTree(DirCacheIterator.class)), + rootOf(attributesTree.get())); expansions.put(BINARY_RULE_KEY, BINARY_RULE_ATTRIBUTES); for (AttributesNode node : new AttributesNode[] { globalNode, rootNode, @@ -152,7 +142,7 @@ public Attributes getAttributes() throws IOException { isDirectory, treeWalk.getTree(WorkingTreeIterator.class), treeWalk.getTree(DirCacheIterator.class), - treeWalk.getTree(CanonicalTreeParser.class), + attributesTree.get(), attributes); // Gets the attributes located in the global attribute file diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java index c904a782d..3d50a8215 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java @@ -4,7 +4,8 @@ * Copyright (C) 2008, Roger C. Soares * Copyright (C) 2006, Shawn O. Pearce * Copyright (C) 2010, Chrisian Halstrick - * Copyright (C) 2019-2020, Andre Bossert + * Copyright (C) 2019, 2020, Andre Bossert + * Copyright (C) 2017, 2022, Thomas Wolf 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 @@ -299,7 +300,7 @@ public void preScanTwoTrees() throws CorruptObjectException, IOException { walk = new NameConflictTreeWalk(repo); builder = dc.builder(); - addTree(walk, headCommitTree); + walk.setHead(addTree(walk, headCommitTree)); addTree(walk, mergeCommitTree); int dciPos = walk.addTree(new DirCacheBuildIterator(builder)); walk.addTree(workingTree); @@ -315,13 +316,6 @@ public void preScanTwoTrees() throws CorruptObjectException, IOException { } } - private void addTree(TreeWalk tw, ObjectId id) throws MissingObjectException, IncorrectObjectTypeException, IOException { - if (id == null) - tw.addTree(new EmptyTreeIterator()); - else - tw.addTree(id); - } - /** * Scan index and merge tree (no HEAD). Used e.g. for initial checkout when * there is no head yet. @@ -341,7 +335,7 @@ public void prescanOneTree() builder = dc.builder(); walk = new NameConflictTreeWalk(repo); - addTree(walk, mergeCommitTree); + walk.setHead(addTree(walk, mergeCommitTree)); int dciPos = walk.addTree(new DirCacheBuildIterator(builder)); walk.addTree(workingTree); workingTree.setDirCacheIterator(walk, dciPos); @@ -356,6 +350,14 @@ public void prescanOneTree() conflicts.removeAll(removed); } + private int addTree(TreeWalk tw, ObjectId id) throws MissingObjectException, + IncorrectObjectTypeException, IOException { + if (id == null) { + return tw.addTree(new EmptyTreeIterator()); + } + return tw.addTree(id); + } + /** * Processing an entry in the context of {@link #prescanOneTree()} when only * one tree is given @@ -382,17 +384,14 @@ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i, // failOnConflict is false. Putting something to conflicts // would mean we delete it. Instead we want the mergeCommit // content to be checked out. - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); } } else - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); } else if (f == null || !m.idEqual(i)) { // The working tree file is missing or the merge content differs // from index content - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); } else if (i.getDirCacheEntry() != null) { // The index contains a file (and not a folder) if (f.isModified(i.getDirCacheEntry(), true, @@ -400,8 +399,7 @@ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i, || i.getDirCacheEntry().getStage() != 0) // The working tree file is dirty or the index contains a // conflict - update(m.getEntryPathString(), m.getEntryObjectId(), - m.getEntryFileMode()); + update(m); else { // update the timestamp of the index with the one from the // file if not set, as we are sure to be in sync here. @@ -802,7 +800,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m, if (f != null && isModifiedSubtree_IndexWorkingtree(name)) { conflict(name, dce, h, m); // 1 } else { - update(name, mId, mMode); // 2 + update(1, name, mId, mMode); // 2 } break; @@ -828,7 +826,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m, // are found later break; case 0xD0F: // 19 - update(name, mId, mMode); + update(1, name, mId, mMode); break; case 0xDF0: // conflict without a rule case 0x0FD: // 15 @@ -839,7 +837,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m, if (isModifiedSubtree_IndexWorkingtree(name)) conflict(name, dce, h, m); // 8 else - update(name, mId, mMode); // 7 + update(1, name, mId, mMode); // 7 } else conflict(name, dce, h, m); // 9 break; @@ -859,7 +857,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m, break; case 0x0DF: // 16 17 if (!isModifiedSubtree_IndexWorkingtree(name)) - update(name, mId, mMode); + update(1, name, mId, mMode); else conflict(name, dce, h, m); break; @@ -929,7 +927,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m, // At least one of Head, Index, Merge is not empty // -> only Merge contains something for this path. Use it! // Potentially update the file - update(name, mId, mMode); // 1 + update(1, name, mId, mMode); // 1 else if (m == null) // Nothing in Merge // Something in Head @@ -947,7 +945,7 @@ else if (m == null) // find in Merge. Potentially updates the file. if (equalIdAndMode(hId, hMode, mId, mMode)) { if (initialCheckout || force) { - update(name, mId, mMode); + update(1, name, mId, mMode); } else { keep(name, dce, f); } @@ -1131,7 +1129,7 @@ && isModified_IndexTree(name, iId, iMode, mId, mMode, // TODO check that we don't overwrite some unsaved // file content - update(name, mId, mMode); + update(1, name, mId, mMode); } else if (dce != null && (f != null && f.isModified(dce, true, this.walk.getObjectReader()))) { @@ -1150,7 +1148,7 @@ && isModified_IndexTree(name, iId, iMode, mId, mMode, // -> Standard case when switching between branches: // Nothing new in index but something different in // Merge. Update index and file - update(name, mId, mMode); + update(1, name, mId, mMode); } } else { // Head differs from index or merge is same as index @@ -1237,12 +1235,17 @@ private void remove(String path) { removed.add(path); } - private void update(String path, ObjectId mId, FileMode mode) - throws IOException { + private void update(CanonicalTreeParser tree) throws IOException { + update(0, tree.getEntryPathString(), tree.getEntryObjectId(), + tree.getEntryFileMode()); + } + + private void update(int index, String path, ObjectId mId, + FileMode mode) throws IOException { if (!FileMode.TREE.equals(mode)) { updated.put(path, new CheckoutMetadata( - walk.getEolStreamType(CHECKOUT_OP), - walk.getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE))); + walk.getCheckoutEolStreamType(index), + walk.getSmudgeCommand(index))); DirCacheEntry entry = new DirCacheEntry(path, DirCacheEntry.STAGE_0); entry.setObjectId(mId); 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 776766286..b9ab1d1b7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -3,7 +3,7 @@ * Copyright (C) 2010-2012, Matthias Sohn * Copyright (C) 2012, Research In Motion Limited * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr) - * Copyright (C) 2018, Thomas Wolf and others + * Copyright (C) 2018, 2022 Thomas Wolf 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 @@ -276,11 +276,15 @@ public enum MergeFailureReason { private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT; /** - * Keeps {@link CheckoutMetadata} for {@link #checkout()} and - * {@link #cleanUp()}. + * Keeps {@link CheckoutMetadata} for {@link #checkout()}. */ private Map checkoutMetadata; + /** + * Keeps {@link CheckoutMetadata} for {@link #cleanUp()}. + */ + private Map cleanupMetadata; + private static MergeAlgorithm getMergeAlgorithm(Config config) { SupportedAlgorithm diffAlg = config.getEnum( CONFIG_DIFF_SECTION, null, CONFIG_KEY_ALGORITHM, @@ -383,12 +387,14 @@ protected boolean mergeImpl() throws IOException { } if (!inCore) { checkoutMetadata = new HashMap<>(); + cleanupMetadata = new HashMap<>(); } try { return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1], false); } finally { checkoutMetadata = null; + cleanupMetadata = null; if (implicitDirCache) { dircache.unlock(); } @@ -447,7 +453,7 @@ protected void cleanUp() throws NoWorkTreeException, DirCacheEntry entry = dc.getEntry(mpath); if (entry != null) { DirCacheCheckout.checkoutEntry(db, entry, reader, false, - checkoutMetadata.get(mpath)); + cleanupMetadata.get(mpath)); } mpathsIt.remove(); } @@ -501,22 +507,26 @@ private DirCacheEntry keep(DirCacheEntry e) { * Remembers the {@link CheckoutMetadata} for the given path; it may be * needed in {@link #checkout()} or in {@link #cleanUp()}. * + * @param map + * to add the metadata to * @param path * of the current node * @param attributes - * for the current node + * to use for determining the metadata * @throws IOException * if the smudge filter cannot be determined - * @since 5.1 + * @since 6.1 */ - protected void addCheckoutMetadata(String path, Attributes attributes) + protected void addCheckoutMetadata(Map map, + String path, Attributes attributes) throws IOException { - if (checkoutMetadata != null) { + if (map != null) { EolStreamType eol = EolStreamTypeUtil.detectStreamType( - OperationType.CHECKOUT_OP, workingTreeOptions, attributes); + OperationType.CHECKOUT_OP, workingTreeOptions, + attributes); CheckoutMetadata data = new CheckoutMetadata(eol, - tw.getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE)); - checkoutMetadata.put(path, data); + tw.getSmudgeCommand(attributes)); + map.put(path, data); } } @@ -529,15 +539,17 @@ protected void addCheckoutMetadata(String path, Attributes attributes) * @param entry * to add * @param attributes - * for the current entry + * the {@link Attributes} of the trees * @throws IOException * if the {@link CheckoutMetadata} cannot be determined - * @since 5.1 + * @since 6.1 */ protected void addToCheckout(String path, DirCacheEntry entry, - Attributes attributes) throws IOException { + Attributes[] attributes) + throws IOException { toBeCheckedOut.put(path, entry); - addCheckoutMetadata(path, attributes); + addCheckoutMetadata(cleanupMetadata, path, attributes[T_OURS]); + addCheckoutMetadata(checkoutMetadata, path, attributes[T_THEIRS]); } /** @@ -549,7 +561,7 @@ protected void addToCheckout(String path, DirCacheEntry entry, * @param isFile * whether it is a file * @param attributes - * for the entry + * to use for determining the {@link CheckoutMetadata} * @throws IOException * if the {@link CheckoutMetadata} cannot be determined * @since 5.1 @@ -558,7 +570,7 @@ protected void addDeletion(String path, boolean isFile, Attributes attributes) throws IOException { toBeDeleted.add(path); if (isFile) { - addCheckoutMetadata(path, attributes); + addCheckoutMetadata(cleanupMetadata, path, attributes); } } @@ -599,7 +611,7 @@ protected void addDeletion(String path, boolean isFile, * see * {@link org.eclipse.jgit.merge.ResolveMerger#mergeTrees(AbstractTreeIterator, RevTree, RevTree, boolean)} * @param attributes - * the attributes defined for this entry + * the {@link Attributes} for the three trees * @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 @@ -607,12 +619,12 @@ protected void addDeletion(String path, boolean isFile, * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException * @throws org.eclipse.jgit.errors.CorruptObjectException * @throws java.io.IOException - * @since 4.9 + * @since 6.1 */ protected boolean processEntry(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, DirCacheBuildIterator index, WorkingTreeIterator work, - boolean ignoreConflicts, Attributes attributes) + boolean ignoreConflicts, Attributes[] attributes) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { enterSubtree = true; @@ -729,7 +741,7 @@ protected boolean processEntry(CanonicalTreeParser base, // Base, ours, and theirs all contain a folder: don't delete return true; } - addDeletion(tw.getPathString(), nonTree(modeO), attributes); + addDeletion(tw.getPathString(), nonTree(modeO), attributes[T_OURS]); return true; } @@ -772,7 +784,7 @@ protected boolean processEntry(CanonicalTreeParser base, if (nonTree(modeO) && nonTree(modeT)) { // Check worktree before modifying files boolean worktreeDirty = isWorktreeDirty(work, ourDce); - if (!attributes.canBeContentMerged() && worktreeDirty) { + if (!attributes[T_OURS].canBeContentMerged() && worktreeDirty) { return false; } @@ -791,7 +803,7 @@ protected boolean processEntry(CanonicalTreeParser base, mergeResults.put(tw.getPathString(), result); unmergedPaths.add(tw.getPathString()); return true; - } else if (!attributes.canBeContentMerged()) { + } else if (!attributes[T_OURS].canBeContentMerged()) { // File marked as binary switch (getContentMergeStrategy()) { case OURS: @@ -842,13 +854,16 @@ protected boolean processEntry(CanonicalTreeParser base, if (ignoreConflicts) { result.setContainsConflicts(false); } - updateIndex(base, ours, theirs, result, attributes); + updateIndex(base, ours, theirs, result, attributes[T_OURS]); String currentPath = tw.getPathString(); if (result.containsConflicts() && !ignoreConflicts) { unmergedPaths.add(currentPath); } modifiedFiles.add(currentPath); - addCheckoutMetadata(currentPath, attributes); + addCheckoutMetadata(cleanupMetadata, currentPath, + attributes[T_OURS]); + addCheckoutMetadata(checkoutMetadata, currentPath, + attributes[T_THEIRS]); } else if (modeO != modeT) { // OURS or THEIRS has been deleted if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw @@ -881,7 +896,8 @@ protected boolean processEntry(CanonicalTreeParser base, // markers). But also stage 0 of the index is filled // with that content. result.setContainsConflicts(false); - updateIndex(base, ours, theirs, result, attributes); + updateIndex(base, ours, theirs, result, + attributes[T_OURS]); } else { add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0); @@ -896,11 +912,9 @@ protected boolean processEntry(CanonicalTreeParser base, if (isWorktreeDirty(work, ourDce)) { return false; } - if (nonTree(modeT)) { - if (e != null) { - addToCheckout(tw.getPathString(), e, - attributes); - } + if (nonTree(modeT) && e != null) { + addToCheckout(tw.getPathString(), e, + attributes); } } @@ -945,14 +959,16 @@ private static MergeResult createGitLinksMergeResult( */ private MergeResult contentMerge(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, - Attributes attributes, ContentMergeStrategy strategy) + Attributes[] attributes, ContentMergeStrategy strategy) throws BinaryBlobException, IOException { + // TW: The attributes here are used to determine the LFS smudge filter. + // Is doing a content merge on LFS items really a good idea?? RawText baseText = base == null ? RawText.EMPTY_TEXT - : getRawText(base.getEntryObjectId(), attributes); + : getRawText(base.getEntryObjectId(), attributes[T_BASE]); RawText ourText = ours == null ? RawText.EMPTY_TEXT - : getRawText(ours.getEntryObjectId(), attributes); + : getRawText(ours.getEntryObjectId(), attributes[T_OURS]); RawText theirsText = theirs == null ? RawText.EMPTY_TEXT - : getRawText(theirs.getEntryObjectId(), attributes); + : getRawText(theirs.getEntryObjectId(), attributes[T_THEIRS]); mergeAlgorithm.setContentMergeStrategy(strategy); return mergeAlgorithm.merge(RawTextComparator.DEFAULT, baseText, ourText, theirsText); @@ -1342,7 +1358,7 @@ protected boolean mergeTrees(AbstractTreeIterator baseTree, tw = new NameConflictTreeWalk(db, reader); tw.addTree(baseTree); - tw.addTree(headTree); + tw.setHead(tw.addTree(headTree)); tw.addTree(mergeTree); int dciPos = tw.addTree(buildIt); if (workingTreeIterator != null) { @@ -1403,6 +1419,13 @@ protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts) boolean hasAttributeNodeProvider = treeWalk .getAttributesNodeProvider() != null; while (treeWalk.next()) { + Attributes[] attributes = { NO_ATTRIBUTES, NO_ATTRIBUTES, + NO_ATTRIBUTES }; + if (hasAttributeNodeProvider) { + attributes[T_BASE] = treeWalk.getAttributes(T_BASE); + attributes[T_OURS] = treeWalk.getAttributes(T_OURS); + attributes[T_THEIRS] = treeWalk.getAttributes(T_THEIRS); + } if (!processEntry( treeWalk.getTree(T_BASE, CanonicalTreeParser.class), treeWalk.getTree(T_OURS, CanonicalTreeParser.class), @@ -1410,9 +1433,7 @@ protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts) treeWalk.getTree(T_INDEX, DirCacheBuildIterator.class), hasWorkingTreeIterator ? treeWalk.getTree(T_FILE, WorkingTreeIterator.class) : null, - ignoreConflicts, hasAttributeNodeProvider - ? treeWalk.getAttributes() - : NO_ATTRIBUTES)) { + ignoreConflicts, attributes)) { cleanUp(); return false; } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java index 1f614e31f..8269666d2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java @@ -1,6 +1,6 @@ /* - * Copyright (C) 2008-2009, Google Inc. - * Copyright (C) 2008, Shawn O. Pearce and others + * Copyright (C) 2008, 2009 Google Inc. + * Copyright (C) 2008, 2022 Shawn O. Pearce 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 @@ -14,6 +14,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -73,6 +74,7 @@ * threads. */ public class TreeWalk implements AutoCloseable, AttributesProvider { + private static final AbstractTreeIterator[] NO_TREES = {}; /** @@ -92,7 +94,7 @@ public enum OperationType { } /** - * Type of operation you want to retrieve the git attributes for. + * Type of operation you want to retrieve the git attributes for. */ private OperationType operationType = OperationType.CHECKOUT_OP; @@ -284,11 +286,20 @@ public static TreeWalk forPath(final Repository db, final String path, AbstractTreeIterator currentHead; - /** Cached attribute for the current entry */ - private Attributes attrs = null; + /** + * Cached attributes for the current entry; per tree. Index i+1 is for tree + * i; index 0 is for the deprecated legacy behavior. + */ + private Attributes[] attrs; - /** Cached attributes handler */ - private AttributesHandler attributesHandler; + /** + * Cached attributes handler; per tree. Index i+1 is for tree i; index 0 is + * for the deprecated legacy behavior. + */ + private AttributesHandler[] attributesHandlers; + + /** Can be set to identify the tree to use for {@link #getAttributes()}. */ + private int headIndex = -1; private Config config; @@ -514,6 +525,24 @@ public AttributesNodeProvider getAttributesNodeProvider() { return attributesNodeProvider; } + /** + * Identifies the tree at the given index as the head tree. This is the tree + * use by default to determine attributes and EOL modes. + * + * @param index + * of the tree to use as head + * @throws IllegalArgumentException + * if the index is out of range + * @since 6.1 + */ + public void setHead(int index) { + if (index < 0 || index >= trees.length) { + throw new IllegalArgumentException("Head index " + index //$NON-NLS-1$ + + " out of range [0," + trees.length + ')'); //$NON-NLS-1$ + } + headIndex = index; + } + /** * {@inheritDoc} *

@@ -556,25 +585,51 @@ public AttributesNodeProvider getAttributesNodeProvider() { */ @Override public Attributes getAttributes() { - if (attrs != null) - return attrs; + return getAttributes(headIndex); + } + /** + * Retrieves the git attributes based on the given tree. + * + * @param index + * of the tree to use as base for the attributes + * @return the attributes + * @since 6.1 + */ + public Attributes getAttributes(int index) { + int attrIndex = index + 1; + Attributes result = attrs[attrIndex]; + if (result != null) { + return result; + } if (attributesNodeProvider == null) { - // The work tree should have a AttributesNodeProvider to be able to - // retrieve the info and global attributes node throw new IllegalStateException( "The tree walk should have one AttributesNodeProvider set in order to compute the git attributes."); //$NON-NLS-1$ } try { - // Lazy create the attributesHandler on the first access of - // attributes. This requires the info, global and root - // attributes nodes - if (attributesHandler == null) { - attributesHandler = new AttributesHandler(this); + AttributesHandler handler = attributesHandlers[attrIndex]; + if (handler == null) { + if (index < 0) { + // Legacy behavior (headIndex not set, getAttributes() above + // called) + handler = new AttributesHandler(this, () -> { + return getTree(CanonicalTreeParser.class); + }); + } else { + handler = new AttributesHandler(this, () -> { + AbstractTreeIterator tree = trees[index]; + if (tree instanceof CanonicalTreeParser) { + return (CanonicalTreeParser) tree; + } + return null; + }); + } + attributesHandlers[attrIndex] = handler; } - attrs = attributesHandler.getAttributes(); - return attrs; + result = handler.getAttributes(); + attrs[attrIndex] = result; + return result; } catch (IOException e) { throw new JGitInternalException("Error while parsing attributes", //$NON-NLS-1$ e); @@ -595,11 +650,34 @@ public Attributes getAttributes() { */ @Nullable public EolStreamType getEolStreamType(OperationType opType) { - if (attributesNodeProvider == null || config == null) + if (attributesNodeProvider == null || config == null) { return null; - return EolStreamTypeUtil.detectStreamType( - opType != null ? opType : operationType, - config.get(WorkingTreeOptions.KEY), getAttributes()); + } + OperationType op = opType != null ? opType : operationType; + return EolStreamTypeUtil.detectStreamType(op, + config.get(WorkingTreeOptions.KEY), getAttributes()); + } + + /** + * Get the EOL stream type of the current entry for checking out using the + * config and {@link #getAttributes()}. + * + * @param tree + * index of the tree the check-out is to be from + * @return the EOL stream type of the current entry using the config and + * {@link #getAttributes()}. Note that this method may return null + * if the {@link org.eclipse.jgit.treewalk.TreeWalk} is not based on + * a working tree + * @since 6.1 + */ + @Nullable + public EolStreamType getCheckoutEolStreamType(int tree) { + if (attributesNodeProvider == null || config == null) { + return null; + } + Attributes attr = getAttributes(tree); + return EolStreamTypeUtil.detectStreamType(OperationType.CHECKOUT_OP, + config.get(WorkingTreeOptions.KEY), attr); } /** @@ -607,7 +685,8 @@ public EolStreamType getEolStreamType(OperationType opType) { */ public void reset() { attrs = null; - attributesHandler = null; + attributesHandlers = null; + headIndex = -1; trees = NO_TREES; advance = false; depth = 0; @@ -651,7 +730,9 @@ public void reset(AnyObjectId id) throws MissingObjectException, advance = false; depth = 0; - attrs = null; + attrs = new Attributes[2]; + attributesHandlers = new AttributesHandler[2]; + headIndex = -1; } /** @@ -701,7 +782,14 @@ public void reset(AnyObjectId... ids) throws MissingObjectException, trees = r; advance = false; depth = 0; - attrs = null; + if (oldLen == newLen) { + Arrays.fill(attrs, null); + Arrays.fill(attributesHandlers, null); + } else { + attrs = new Attributes[newLen + 1]; + attributesHandlers = new AttributesHandler[newLen + 1]; + } + headIndex = -1; } /** @@ -758,6 +846,16 @@ public int addTree(AbstractTreeIterator p) { p.matchShift = 0; trees = newTrees; + if (attrs == null) { + attrs = new Attributes[n + 2]; + } else { + attrs = Arrays.copyOf(attrs, n + 2); + } + if (attributesHandlers == null) { + attributesHandlers = new AttributesHandler[n + 2]; + } else { + attributesHandlers = Arrays.copyOf(attributesHandlers, n + 2); + } return n; } @@ -800,7 +898,7 @@ public boolean next() throws MissingObjectException, } for (;;) { - attrs = null; + Arrays.fill(attrs, null); final AbstractTreeIterator t = min(); if (t.eof()) { if (depth > 0) { @@ -1255,7 +1353,7 @@ public boolean isPostChildren() { */ public void enterSubtree() throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { - attrs = null; + Arrays.fill(attrs, null); final AbstractTreeIterator ch = currentHead; final AbstractTreeIterator[] tmp = new AbstractTreeIterator[trees.length]; for (int i = 0; i < trees.length; i++) { @@ -1374,11 +1472,12 @@ public T getTree(Class type) { /** * Inspect config and attributes to return a filtercommand applicable for - * the current path, but without expanding %f occurences + * the current path. * * @param filterCommandType * which type of filterCommand should be executed. E.g. "clean", - * "smudge" + * "smudge". For "smudge" consider using + * {{@link #getSmudgeCommand(int)} instead. * @return a filter command * @throws java.io.IOException * @since 4.2 @@ -1406,6 +1505,54 @@ public String getFilterCommand(String filterCommandType) QuotedString.BOURNE.quote((getPathString())))); } + /** + * Inspect config and attributes to return a filtercommand applicable for + * the current path. + * + * @param index + * of the tree the item to be smudged is in + * @return a filter command + * @throws java.io.IOException + * @since 6.1 + */ + public String getSmudgeCommand(int index) + throws IOException { + return getSmudgeCommand(getAttributes(index)); + } + + /** + * Inspect config and attributes to return a filtercommand applicable for + * the current path. + * + * @param attributes + * to use + * @return a filter command + * @throws java.io.IOException + * @since 6.1 + */ + public String getSmudgeCommand(Attributes attributes) throws IOException { + if (attributes == null) { + return null; + } + Attribute f = attributes.get(Constants.ATTR_FILTER); + if (f == null) { + return null; + } + String filterValue = f.getValue(); + if (filterValue == null) { + return null; + } + + String filterCommand = getFilterCommandDefinition(filterValue, + Constants.ATTR_FILTER_TYPE_SMUDGE); + if (filterCommand == null) { + return null; + } + return filterCommand.replaceAll("%f", //$NON-NLS-1$ + Matcher.quoteReplacement( + QuotedString.BOURNE.quote((getPathString())))); + } + /** * Get the filter command how it is defined in gitconfig. The returned * string may contain "%f" which needs to be replaced by the current path