]> source.dussan.org Git - jgit.git/commitdiff
[checkout] Use .gitattributes from the commit to be checked out 66/190766/4
authorThomas Wolf <thomas.wolf@paranor.ch>
Sun, 13 Feb 2022 22:30:36 +0000 (23:30 +0100)
committerMatthias Sohn <matthias.sohn@sap.com>
Mon, 7 Mar 2022 17:45:25 +0000 (18:45 +0100)
JGit used only one set of attributes constructed from the global and
info attributes, plus the attributes from working tree, index, and
HEAD.

These attributes must be used to determine whether the working tree is
dirty.

But for actually checking out a file, one must use the attributes from
global, info, and *the commit to be checked out*. Otherwise one may not
pick up definitions that are only in the .gitattributes of the commit
to be checked out or that are changed in that commit with respect to
the attributes currently in HEAD, the index, or the working tree.

Maintain in TreeWalk different Attributes per tree, and add operations
to determine EOL handling and smudge filters per tree.

Use the new methods in DirCacheCheckout and ResolveMerger. Note that
merging in JGit actually used the attributes from the base, not those
from ours, which looks dubious at least. It now uses those from ours,
and for checking out the ones from theirs.

The canBeContentMerged() determination was also done from the base
attributes, and is newly done from the ours attributes. Possibly this
should take into account all three attributes, and only if all three
agree the item can be content merged, a content merge should be
attempted? (What if the binary/text setting changes between base, ours,
or theirs?)

Also note that JGit attempts to perform content merges on non-binary
LFS files; there it used the filter attribute from base, too, even for
the ours and theirs versions. Newly it takes the filter attribute from
the correct tree. I'm not convinced doing content merges on potentially
huge files like LFS files is really a good idea.

Add tests in FilterCommandsTest and LfsGitTest to verify the behavior.

Open question: using index and working tree as fallback for the
attributes of ours (assuming it is HEAD) is OK. But does it also make
sense for base and theirs in merging?

Bug: 578707
Change-Id: I0bf433e9e3eb28479b6272e17c0666e175e67d08
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsGitTest.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FilterCommandsTest.java
org.eclipse.jgit/.settings/.api_filters
org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesHandler.java
org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java

index 8964310e417f9f7827f0009075095747f8952853..3e83c8ef49d9e39d7ea92b4874150343afddadb7 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2021, 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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 class LfsGitTest extends RepositoryTestCase {
                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";
index 36f94fbd204a8a2d9c70829a9fd5595dcab7f407..89d31c3e8f8bace1f938be87b663c4bb5c2d8ab2 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016, Christian Halstrick <christian.halstrick@sap.com> and others
+ * Copyright (C) 2016, 2022 Christian Halstrick <christian.halstrick@sap.com> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
 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 class FilterCommandsTest extends RepositoryTestCase {
                secondCommit = git.commit().setMessage("Second commit").call();
        }
 
+       @Override
+       public void tearDown() throws Exception {
+               Set<String> existingFilters = new HashSet<>(
+                               FilterCommandRegistry.getRegisteredFilterCommands());
+               existingFilters.forEach(FilterCommandRegistry::unregister);
+               super.tearDown();
+       }
+
        @Test
        public void testBuiltinCleanFilter()
                        throws IOException, GitAPIException {
@@ -217,4 +230,133 @@ public class FilterCommandsTest extends RepositoryTestCase {
                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");
+       }
+
 }
index e026e31dc5b20b805692bbf382d61894a09da862..00b89a4b3d5fb755b5f5310b9be2e551b4a0408e 100644 (file)
             </message_arguments>
         </filter>
     </resource>
+    <resource path="src/org/eclipse/jgit/merge/ResolveMerger.java" type="org.eclipse.jgit.merge.ResolveMerger">
+        <filter id="338792546">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.merge.ResolveMerger"/>
+                <message_argument value="addCheckoutMetadata(String, Attributes)"/>
+            </message_arguments>
+        </filter>
+        <filter id="338792546">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.merge.ResolveMerger"/>
+                <message_argument value="addToCheckout(String, DirCacheEntry, Attributes)"/>
+            </message_arguments>
+        </filter>
+        <filter id="338792546">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.merge.ResolveMerger"/>
+                <message_argument value="processEntry(CanonicalTreeParser, CanonicalTreeParser, CanonicalTreeParser, DirCacheBuildIterator, WorkingTreeIterator, boolean, Attributes)"/>
+            </message_arguments>
+        </filter>
+    </resource>
     <resource path="src/org/eclipse/jgit/transport/BasePackPushConnection.java" type="org.eclipse.jgit.transport.BasePackPushConnection">
         <filter id="338792546">
             <message_arguments>
index 638dd827ed029c0612b02e6a14d4c6f97d577250..7ec78597fa284cb2d482c7d3a223c99f87a83b7a 100644 (file)
@@ -1,43 +1,11 @@
 /*
- * Copyright (C) 2015, Ivan Motsch <ivan.motsch@bsiag.com>
+ * Copyright (C) 2015, 2022 Ivan Motsch <ivan.motsch@bsiag.com> 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.HashMap;
 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<CanonicalTreeParser> 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<CanonicalTreeParser> 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 class AttributesHandler {
                                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
index c904a782dbe60df092729fcec191c19e7a3ca8a4..3d50a8215571819015e49f87ee10ac89dfc7d618 100644 (file)
@@ -4,7 +4,8 @@
  * Copyright (C) 2008, Roger C. Soares <rogersoares@intelinet.com.br>
  * Copyright (C) 2006, Shawn O. Pearce <spearce@spearce.org>
  * Copyright (C) 2010, Chrisian Halstrick <christian.halstrick@sap.com>
- * Copyright (C) 2019-2020, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2019, 2020, Andre Bossert <andre.bossert@siemens.com>
+ * Copyright (C) 2017, 2022, Thomas Wolf <thomas.wolf@paranor.ch> 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 class DirCacheCheckout {
                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 class DirCacheCheckout {
                }
        }
 
-       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 class DirCacheCheckout {
                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 class DirCacheCheckout {
                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 @@ public class DirCacheCheckout {
                                                // 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 @@ public class DirCacheCheckout {
                                                || 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 @@ public class DirCacheCheckout {
                                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 @@ public class DirCacheCheckout {
                                // 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 @@ public class DirCacheCheckout {
                                        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 @@ public class DirCacheCheckout {
                                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 @@ public class DirCacheCheckout {
                                // 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 @@ public class DirCacheCheckout {
                                // 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 @@ public class DirCacheCheckout {
 
                                                // 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 @@ public class DirCacheCheckout {
                                                // -> 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 @@ public class DirCacheCheckout {
                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);
index 77676628679f0545693b2e9ff505d92054c987b2..b9ab1d1b7af537804faa8ce5431376271e700830 100644 (file)
@@ -3,7 +3,7 @@
  * Copyright (C) 2010-2012, Matthias Sohn <matthias.sohn@sap.com>
  * Copyright (C) 2012, Research In Motion Limited
  * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr)
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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 class ResolveMerger extends ThreeWayMerger {
        private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT;
 
        /**
-        * Keeps {@link CheckoutMetadata} for {@link #checkout()} and
-        * {@link #cleanUp()}.
+        * Keeps {@link CheckoutMetadata} for {@link #checkout()}.
         */
        private Map<String, CheckoutMetadata> checkoutMetadata;
 
+       /**
+        * Keeps {@link CheckoutMetadata} for {@link #cleanUp()}.
+        */
+       private Map<String, CheckoutMetadata> cleanupMetadata;
+
        private static MergeAlgorithm getMergeAlgorithm(Config config) {
                SupportedAlgorithm diffAlg = config.getEnum(
                                CONFIG_DIFF_SECTION, null, CONFIG_KEY_ALGORITHM,
@@ -383,12 +387,14 @@ public class ResolveMerger extends ThreeWayMerger {
                }
                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 @@ public class ResolveMerger extends ThreeWayMerger {
                        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 @@ public class ResolveMerger extends ThreeWayMerger {
         * 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<String, CheckoutMetadata> 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 @@ public class ResolveMerger extends ThreeWayMerger {
         * @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 @@ public class ResolveMerger extends ThreeWayMerger {
         * @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 @@ public class ResolveMerger extends ThreeWayMerger {
                        Attributes attributes) throws IOException {
                toBeDeleted.add(path);
                if (isFile) {
-                       addCheckoutMetadata(path, attributes);
+                       addCheckoutMetadata(cleanupMetadata, path, attributes);
                }
        }
 
@@ -599,7 +611,7 @@ public class ResolveMerger extends ThreeWayMerger {
         *            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 <code>false</code> 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 @@ public class ResolveMerger extends ThreeWayMerger {
         * @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 @@ public class ResolveMerger extends ThreeWayMerger {
                                // 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 @@ public class ResolveMerger extends ThreeWayMerger {
                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 @@ public class ResolveMerger extends ThreeWayMerger {
                                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 @@ public class ResolveMerger extends ThreeWayMerger {
                        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 @@ public class ResolveMerger extends ThreeWayMerger {
                                                // 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 @@ public class ResolveMerger extends ThreeWayMerger {
                                                        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 @@ public class ResolveMerger extends ThreeWayMerger {
         */
        private MergeResult<RawText> 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 @@ public class ResolveMerger extends ThreeWayMerger {
 
                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 @@ public class ResolveMerger extends ThreeWayMerger {
                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 @@ public class ResolveMerger extends ThreeWayMerger {
                                        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;
                        }
index 1f614e31f61c4cdf83d260bde9a28cdb31a3f5f3..8269666d26e7dae1cdd584371e5ef27eb0416602 100644 (file)
@@ -1,6 +1,6 @@
 /*
- * Copyright (C) 2008-2009, Google Inc.
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2009 Google Inc.
+ * Copyright (C) 2008, 2022 Shawn O. Pearce <spearce@spearce.org> 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 @@ package org.eclipse.jgit.treewalk;
 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 @@ import org.eclipse.jgit.util.io.EolStreamTypeUtil;
  * threads.
  */
 public class TreeWalk implements AutoCloseable, AttributesProvider {
+
        private static final AbstractTreeIterator[] NO_TREES = {};
 
        /**
@@ -92,7 +94,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider {
        }
 
        /**
-        *            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 class TreeWalk implements AutoCloseable, AttributesProvider {
 
        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; per tree. Index i+1 is for tree i; index 0 is
+        * for the deprecated legacy behavior.
+        */
+       private AttributesHandler[] attributesHandlers;
 
-       /** Cached attributes handler */
-       private AttributesHandler attributesHandler;
+       /** Can be set to identify the tree to use for {@link #getAttributes()}. */
+       private int headIndex = -1;
 
        private Config config;
 
@@ -514,6 +525,24 @@ public class TreeWalk implements AutoCloseable, AttributesProvider {
                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}
         * <p>
@@ -556,25 +585,51 @@ public class TreeWalk implements AutoCloseable, AttributesProvider {
         */
        @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 class TreeWalk implements AutoCloseable, AttributesProvider {
         */
        @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 class TreeWalk implements AutoCloseable, AttributesProvider {
         */
        public void reset() {
                attrs = null;
-               attributesHandler = null;
+               attributesHandlers = null;
+               headIndex = -1;
                trees = NO_TREES;
                advance = false;
                depth = 0;
@@ -651,7 +730,9 @@ public class TreeWalk implements AutoCloseable, AttributesProvider {
 
                advance = false;
                depth = 0;
-               attrs = null;
+               attrs = new Attributes[2];
+               attributesHandlers = new AttributesHandler[2];
+               headIndex = -1;
        }
 
        /**
@@ -701,7 +782,14 @@ public class TreeWalk implements AutoCloseable, AttributesProvider {
                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 class TreeWalk implements AutoCloseable, AttributesProvider {
                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 class TreeWalk implements AutoCloseable, AttributesProvider {
                        }
 
                        for (;;) {
-                               attrs = null;
+                               Arrays.fill(attrs, null);
                                final AbstractTreeIterator t = min();
                                if (t.eof()) {
                                        if (depth > 0) {
@@ -1255,7 +1353,7 @@ public class TreeWalk implements AutoCloseable, AttributesProvider {
         */
        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 class TreeWalk implements AutoCloseable, AttributesProvider {
 
        /**
         * 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 class TreeWalk implements AutoCloseable, AttributesProvider {
                                                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