[checkout] Use .gitattributes from the commit to be checked out

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>
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 8964310..3e83c8e 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 <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
@@ -68,6 +68,27 @@
 	}
 
 	@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";
 		File smallFile = writeTrashFile("Test.txt", content);
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 36f94fb..89d31c3 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 <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
@@ -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 @@
 		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 @@
 		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 e026e31..00b89a4 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -39,6 +39,26 @@
             </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>
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 638dd82..7ec7859 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 <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.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 @@
 
 	private final TreeWalk treeWalk;
 
+	private final Supplier<CanonicalTreeParser> attributesTree;
+
 	private final AttributesNode globalNode;
 
 	private final AttributesNode infoNode;
@@ -98,22 +69,41 @@
 	 * @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 @@
 				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 c904a78..3d50a82 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 <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 @@
 		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 @@
 		}
 	}
 
-	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 @@
 		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 @@
 		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 @@
 						// 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 @@
 						|| 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 @@
 				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 @@
 				// 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 @@
 					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 @@
 				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 @@
 				// 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 @@
 				// 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 @@
 
 						// 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 @@
 						// -> 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 @@
 		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 7767662..b9ab1d1 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 <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 @@
 	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 @@
 		}
 		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 @@
 			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 @@
 	 * 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 @@
 	 * @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 @@
 	 * @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 @@
 			Attributes attributes) throws IOException {
 		toBeDeleted.add(path);
 		if (isFile) {
-			addCheckoutMetadata(path, attributes);
+			addCheckoutMetadata(cleanupMetadata, path, attributes);
 		}
 	}
 
@@ -599,7 +611,7 @@
 	 *            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 @@
 	 * @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 @@
 				// 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 @@
 		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 @@
 				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 @@
 			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 @@
 						// 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 @@
 							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 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 @@
 
 		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 @@
 		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 @@
 					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 1f614e3..8269666 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 <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 @@
 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 @@
 	}
 
 	/**
-	 *            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 @@
 
 	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;
 
@@ -515,6 +526,24 @@
 	}
 
 	/**
+	 * 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>
 	 * Retrieve the git attributes for the current entry.
@@ -556,25 +585,51 @@
 	 */
 	@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 @@
 	 */
 	@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 void reset() {
 		attrs = null;
-		attributesHandler = null;
+		attributesHandlers = null;
+		headIndex = -1;
 		trees = NO_TREES;
 		advance = false;
 		depth = 0;
@@ -651,7 +730,9 @@
 
 		advance = false;
 		depth = 0;
-		attrs = null;
+		attrs = new Attributes[2];
+		attributesHandlers = new AttributesHandler[2];
+		headIndex = -1;
 	}
 
 	/**
@@ -701,7 +782,14 @@
 		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 @@
 		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 @@
 			}
 
 			for (;;) {
-				attrs = null;
+				Arrays.fill(attrs, null);
 				final AbstractTreeIterator t = min();
 				if (t.eof()) {
 					if (depth > 0) {
@@ -1255,7 +1353,7 @@
 	 */
 	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 @@
 
 	/**
 	 * 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
@@ -1407,6 +1506,54 @@
 	}
 
 	/**
+	 * 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
 	 * before executing the filter command. These filter definitions are cached