Merge branch 'stable-6.1'

* stable-6.1:
  Prepare 6.1.1-SNAPSHOT builds
  JGit v6.1.0.202203080745-r
  [checkout] Use .gitattributes from the commit to be checked out
  Don't use final for method parameters
  [push] support the "matching" RefSpecs ":" and "+:"
  [push] Call the pre-push hook later in the push process
  IndexDiff: use tree filter also for SubmoduleWalk
  Run license check with option -Ddash.projectId=technology.jgit
  Exclude transitive dependencies of sshd-sftp
  Update DEPENDENCIES for 6.1.0 release
  Add dependency to dash-licenses
  Fix typos of some keys in LfsText
  Sort LfsText entries alphabetically
  Support for "lfs.url" from ".lfsconfig"

Change-Id: I1b9f0c0ed647837e00b9640d235dbfab2329c5a6
diff --git a/DEPENDENCIES b/DEPENDENCIES
index ebbe480..93fa850 100644
--- a/DEPENDENCIES
+++ b/DEPENDENCIES
@@ -1,69 +1,68 @@
 maven/mavencentral/args4j/args4j/2.33, MIT, approved, CQ11068
-maven/mavencentral/com.google.code.gson/gson/2.8.7, Apache-2.0, approved, CQ23496
-maven/mavencentral/com.googlecode.javaewah/JavaEWAH/1.1.12, Apache-2.0, approved, CQ11658
+maven/mavencentral/com.google.code.gson/gson/2.8.9, Apache-2.0, approved, CQ23496
+maven/mavencentral/com.googlecode.javaewah/JavaEWAH/1.1.13, Apache-2.0, approved, CQ11658
 maven/mavencentral/com.jcraft/jsch/0.1.55, BSD-3-Clause, approved, CQ19435
 maven/mavencentral/com.jcraft/jzlib/1.1.1, BSD-2-Clause, approved, CQ6218
 maven/mavencentral/commons-codec/commons-codec/1.11, Apache-2.0 AND BSD-3-Clause, approved, CQ15971
 maven/mavencentral/commons-logging/commons-logging/1.2, Apache-2.0, approved, CQ10162
-maven/mavencentral/javax.servlet/javax.servlet-api/3.1.0, Apache-2.0 AND (CDDL-1.1 OR GPL-2.0 WITH Classpath-exception-2.0), approved, CQ7248
-maven/mavencentral/junit/junit/4.13, , approved, CQ22796
-maven/mavencentral/log4j/log4j/1.2.15, Apache-2.0, approved, CQ7837
+maven/mavencentral/javax.servlet/javax.servlet-api/4.0.0, , approved, CQ16125
+maven/mavencentral/junit/junit/4.13.2, EPL-2.0, approved, CQ23636
 maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.9.0, Apache-2.0, approved, clearlydefined
 maven/mavencentral/net.bytebuddy/byte-buddy/1.9.0, Apache-2.0, approved, clearlydefined
 maven/mavencentral/net.i2p.crypto/eddsa/0.3.0, CC0-1.0, approved, CQ22537
+maven/mavencentral/net.java.dev.jna/jna-platform/5.8.0, Apache-2.0 OR LGPL-2.1-or-later, approved, CQ23218
+maven/mavencentral/net.java.dev.jna/jna/5.8.0, Apache-2.0 OR LGPL-2.1-or-later, approved, CQ23217
 maven/mavencentral/net.sf.jopt-simple/jopt-simple/4.6, MIT, approved, clearlydefined
-maven/mavencentral/org.apache.ant/ant-launcher/1.10.10, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560
-maven/mavencentral/org.apache.ant/ant/1.10.10, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560
-maven/mavencentral/org.apache.commons/commons-compress/1.20, Apache-2.0 AND BSD-3-Clause AND LicenseRef-Public-Domain, approved, CQ21771
+maven/mavencentral/org.apache.ant/ant-launcher/1.10.12, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560
+maven/mavencentral/org.apache.ant/ant/1.10.12, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560
+maven/mavencentral/org.apache.commons/commons-compress/1.21, Apache-2.0 AND BSD-3-Clause AND bzip2-1.0.6 AND LicenseRef-Public-Domain, approved, CQ23710
 maven/mavencentral/org.apache.commons/commons-math3/3.2, Apache-2.0, approved, clearlydefined
 maven/mavencentral/org.apache.httpcomponents/httpclient/4.5.13, Apache-2.0 AND LicenseRef-Public-Domain, approved, CQ23527
 maven/mavencentral/org.apache.httpcomponents/httpcore/4.4.14, Apache-2.0, approved, CQ23528
-maven/mavencentral/org.apache.sshd/sshd-common/2.7.0, Apache-2.0 and ISC, approved, CQ23469
-maven/mavencentral/org.apache.sshd/sshd-core/2.7.0, Apache-2.0, approved, CQ23469
-maven/mavencentral/org.apache.sshd/sshd-osgi/2.7.0, Apache-2.0 and ISC, approved, CQ23469
-maven/mavencentral/org.apache.sshd/sshd-sftp/2.7.0, Apache-2.0, approved, CQ23470
+maven/mavencentral/org.apache.sshd/sshd-osgi/2.8.0, Apache-2.0, approved, CQ23892
+maven/mavencentral/org.apache.sshd/sshd-sftp/2.8.0, Apache-2.0, approved, CQ23893
 maven/mavencentral/org.assertj/assertj-core/3.20.2, Apache-2.0, approved, clearlydefined
-maven/mavencentral/org.bouncycastle/bcpg-jdk15on/1.69, MIT and Apache-2.0, approved, CQ23472
-maven/mavencentral/org.bouncycastle/bcpkix-jdk15on/1.69, MIT, approved, CQ23473
-maven/mavencentral/org.bouncycastle/bcprov-jdk15on/1.69, MIT, approved, CQ23471
-maven/mavencentral/org.bouncycastle/bcutil-jdk15on/1.69, MIT, approved, CQ23474
-maven/mavencentral/org.eclipse.jetty/jetty-http/9.4.43.v20210629, , approved, eclipse
-maven/mavencentral/org.eclipse.jetty/jetty-io/9.4.43.v20210629, , approved, eclipse
-maven/mavencentral/org.eclipse.jetty/jetty-security/9.4.43.v20210629, , approved, eclipse
-maven/mavencentral/org.eclipse.jetty/jetty-server/9.4.43.v20210629, , approved, eclipse
-maven/mavencentral/org.eclipse.jetty/jetty-servlet/9.4.43.v20210629, , approved, eclipse
-maven/mavencentral/org.eclipse.jetty/jetty-util-ajax/9.4.43.v20210629, , approved, eclipse
-maven/mavencentral/org.eclipse.jetty/jetty-util/9.4.43.v20210629, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant.test/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.archive/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.gpg.bc/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.apache/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.server/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.test/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.http/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.ssh/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server.test/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.test/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm.test/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.test/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.jsch/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.test/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ui/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit/5.13.0-SNAPSHOT, , approved, eclipse
-maven/mavencentral/org.hamcrest/hamcrest-core/1.3, BSD-2-Clause, approved, CQ7063
-maven/mavencentral/org.hamcrest/hamcrest/2.2, BSD-2-Clause, approved, clearlydefined
-maven/mavencentral/org.mockito/mockito-core/2.23.0, MIT, approved, CQ17976
+maven/mavencentral/org.bouncycastle/bcpg-jdk15on/1.70, Apache-2.0, approved, #1713
+maven/mavencentral/org.bouncycastle/bcpkix-jdk15on/1.70, MIT, approved, clearlydefined
+maven/mavencentral/org.bouncycastle/bcprov-jdk15on/1.70, MIT, approved, #1712
+maven/mavencentral/org.bouncycastle/bcutil-jdk15on/1.70, MIT, approved, clearlydefined
+maven/mavencentral/org.eclipse.jetty.toolchain/jetty-servlet-api/4.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty
+maven/mavencentral/org.eclipse.jetty/jetty-http/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty
+maven/mavencentral/org.eclipse.jetty/jetty-io/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty
+maven/mavencentral/org.eclipse.jetty/jetty-security/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty
+maven/mavencentral/org.eclipse.jetty/jetty-server/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty
+maven/mavencentral/org.eclipse.jetty/jetty-servlet/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty
+maven/mavencentral/org.eclipse.jetty/jetty-util/10.0.6, EPL-2.0 OR Apache-2.0, approved, rt.jetty
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.archive/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.gpg.bc/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.apache/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.server/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.http/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.ssh/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.agent/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.jsch/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.test/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ui/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit/6.1.0-SNAPSHOT, BSD-3-Clause, approved, technology.jgit
+maven/mavencentral/org.hamcrest/hamcrest-core/1.3, BSD-2-Clause, approved, CQ11429
+maven/mavencentral/org.mockito/mockito-core/2.23.0, Apache-2.0 AND MIT, approved, #958
 maven/mavencentral/org.objenesis/objenesis/2.6, Apache-2.0, approved, CQ15478
-maven/mavencentral/org.openjdk.jmh/jmh-core/1.32, GPL-2.0, approved, CQ23499
-maven/mavencentral/org.openjdk.jmh/jmh-generator-annprocess/1.32, GPL-2.0, approved, CQ23500
-maven/mavencentral/org.osgi/org.osgi.core/4.3.1, Apache-2.0, approved, CQ10111
-maven/mavencentral/org.slf4j/jcl-over-slf4j/1.7.30, Apache-2.0, approved, CQ12843
+maven/mavencentral/org.openjdk.jmh/jmh-core/1.32, GPL-2.0-only with Classpath-exception-2.0, approved, #959
+maven/mavencentral/org.openjdk.jmh/jmh-generator-annprocess/1.32, GPL-2.0-only with Classpath-exception-2.0, approved, #962
+maven/mavencentral/org.osgi/org.osgi.core/6.0.0, Apache-2.0, approved, #1794
+maven/mavencentral/org.slf4j/jcl-over-slf4j/1.7.32, Apache-2.0, approved, CQ12843
 maven/mavencentral/org.slf4j/slf4j-api/1.7.30, MIT, approved, CQ13368
-maven/mavencentral/org.slf4j/slf4j-log4j12/1.7.30, MIT, approved, CQ7665
+maven/mavencentral/org.slf4j/slf4j-simple/1.7.30, MIT, approved, CQ7952
 maven/mavencentral/org.tukaani/xz/1.9, LicenseRef-Public-Domain, approved, CQ23498
diff --git a/org.eclipse.jgit.junit.ssh/pom.xml b/org.eclipse.jgit.junit.ssh/pom.xml
index 504c7dc..395b14b 100644
--- a/org.eclipse.jgit.junit.ssh/pom.xml
+++ b/org.eclipse.jgit.junit.ssh/pom.xml
@@ -55,6 +55,16 @@
       <groupId>org.apache.sshd</groupId>
       <artifactId>sshd-sftp</artifactId>
       <version>${apache-sshd-version}</version>
+      <exclusions>
+          <exclusion>
+              <groupId>org.apache.sshd</groupId>
+              <artifactId>sshd-core</artifactId>
+          </exclusion>
+          <exclusion>
+              <groupId>org.apache.sshd</groupId>
+              <artifactId>sshd-common</artifactId>
+          </exclusion>
+      </exclusions>
     </dependency>
 
     <dependency>
diff --git a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsConfigGitTest.java b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsConfigGitTest.java
new file mode 100644
index 0000000..98a0712
--- /dev/null
+++ b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/LfsConfigGitTest.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2022, Matthias Fromme <mfromme@dspace.de>
+ *
+ * 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.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.lfs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.attributes.FilterCommand;
+import org.eclipse.jgit.attributes.FilterCommandRegistry;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lfs.internal.LfsConnectionFactory;
+import org.eclipse.jgit.lfs.lib.Constants;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.transport.http.HttpConnection;
+import org.eclipse.jgit.util.HttpSupport;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Test if the lfs config is used in the correct way during checkout.
+ *
+ * Two lfs-files are created, one that comes before .gitattributes and
+ * .lfsconfig in git order (".aaa.txt") and one that comes after ("zzz.txt").
+ *
+ * During checkout/reset it is tested if the correct version of the lfs config
+ * is used.
+ *
+ * TODO: The current behavior seems a little bit strange/unintuitive. Some files
+ * are checked out before and some after the config files. This leads to the
+ * behavior, that during a single command the config changes. Since this seems
+ * to be the same way in native git, the behavior is accepted for now.
+ *
+ */
+public class LfsConfigGitTest extends RepositoryTestCase {
+
+	private static final String SMUDGE_NAME = org.eclipse.jgit.lib.Constants.BUILTIN_FILTER_PREFIX
+			+ Constants.ATTR_FILTER_DRIVER_PREFIX
+			+ org.eclipse.jgit.lib.Constants.ATTR_FILTER_TYPE_SMUDGE;
+
+	private static final String LFS_SERVER_URI1 = "https://lfs.server1/test/uri";
+
+	private static final String EXPECTED_SERVER_URL1 = LFS_SERVER_URI1
+			+ Protocol.OBJECTS_LFS_ENDPOINT;
+
+	private static final String LFS_SERVER_URI2 = "https://lfs.server2/test/uri";
+
+	private static final String EXPECTED_SERVER_URL2 = LFS_SERVER_URI2
+			+ Protocol.OBJECTS_LFS_ENDPOINT;
+
+	private static final String LFS_SERVER_URI3 = "https://lfs.server3/test/uri";
+
+	private static final String EXPECTED_SERVER_URL3 = LFS_SERVER_URI3
+			+ Protocol.OBJECTS_LFS_ENDPOINT;
+
+	private static final String FAKE_LFS_POINTER1 = "version https://git-lfs.github.com/spec/v1\n"
+			+ "oid sha256:6ce9fab52ee9a6c4c097def4e049c6acdeba44c99d26e83ba80adec1473c9b2d\n"
+			+ "size 253952\n";
+
+	private static final String FAKE_LFS_POINTER2 = "version https://git-lfs.github.com/spec/v1\n"
+			+ "oid sha256:a4b711cd989863ae2038758a62672138347abbbae4076a7ad3a545fda7d08f82\n"
+			+ "size 67072\n";
+
+	private static List<String> checkoutURLs = new ArrayList<>();
+
+	static class SmudgeFilterMock extends FilterCommand {
+		public SmudgeFilterMock(Repository db, InputStream in,
+				OutputStream out) throws IOException {
+			super(in, out);
+			HttpConnection lfsServerConn = LfsConnectionFactory.getLfsConnection(db,
+					HttpSupport.METHOD_POST, Protocol.OPERATION_DOWNLOAD);
+			checkoutURLs.add(lfsServerConn.getURL().toString());
+		}
+
+		@Override
+		public int run() throws IOException {
+			// Stupid no impl
+			in.transferTo(out);
+			return -1;
+		}
+	}
+
+	@BeforeClass
+	public static void installLfs() {
+		FilterCommandRegistry.register(SMUDGE_NAME, SmudgeFilterMock::new);
+	}
+
+	@AfterClass
+	public static void removeLfs() {
+		FilterCommandRegistry.unregister(SMUDGE_NAME);
+	}
+
+	private Git git;
+
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+		git = new Git(db);
+		// commit something
+		writeTrashFile("Test.txt", "Hello world");
+		git.add().addFilepattern("Test.txt").call();
+		git.commit().setMessage("Initial commit").call();
+		// prepare the config for LFS
+		StoredConfig config = git.getRepository().getConfig();
+		config.setString("filter", "lfs", "smudge", SMUDGE_NAME);
+		config.setString(ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_AUTOCRLF, "false");
+		config.save();
+
+		fileBefore = null;
+		fileAfter = null;
+		configFile = null;
+		gitAttributesFile = null;
+	}
+
+	File fileBefore;
+
+	File fileAfter;
+
+	File configFile;
+
+	File gitAttributesFile;
+
+	private void createLfsFiles(String lfsPointer) throws Exception {
+		/*
+		 * FileNames ".aaa.txt" and "zzz.txt" seem to be sufficient to get the
+		 * desired checkout order before and after ".lfsconfig", at least in a
+		 * number of manual tries. Since the files to checkout are contained in
+		 * a set (see DirCacheCheckout::doCheckout) the order cannot be
+		 * guaranteed.
+		 */
+
+		//File to be checked out before lfs config
+		String fileNameBefore = ".aaa.txt";
+		fileBefore = writeTrashFile(fileNameBefore, lfsPointer);
+		git.add().addFilepattern(fileNameBefore).call();
+
+		// File to be checked out after lfs config
+		String fileNameAfter = "zzz.txt";
+		fileAfter = writeTrashFile(fileNameAfter, lfsPointer);
+		git.add().addFilepattern(fileNameAfter).call();
+
+		git.commit().setMessage("Commit LFS Pointer files").call();
+	}
+
+
+	private String addLfsConfigFiles(String lfsServerUrl) throws Exception {
+		// Add config files to the repo
+		String lfsConfig1 = createLfsConfig(lfsServerUrl);
+		git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call();
+		// Modify gitattributes on second call, to force checkout too.
+		if (gitAttributesFile == null) {
+			gitAttributesFile = writeTrashFile(".gitattributes",
+				"*.txt filter=lfs");
+		} else {
+			gitAttributesFile = writeTrashFile(".gitattributes",
+					"*.txt filter=lfs\n");
+		}
+
+		git.add().addFilepattern(".gitattributes").call();
+		git.commit().setMessage("Commit config files").call();
+		return lfsConfig1;
+	}
+
+	private String createLfsConfig(String lfsServerUrl) throws IOException {
+		String lfsConfig1 = "[lfs]\n    url = " + lfsServerUrl;
+		configFile = writeTrashFile(Constants.DOT_LFS_CONFIG, lfsConfig1);
+		return lfsConfig1;
+	}
+
+	@Test
+	public void checkoutLfsObjects_reset() throws Exception {
+		createLfsFiles(FAKE_LFS_POINTER1);
+		String lfsConfig1 = addLfsConfigFiles(LFS_SERVER_URI1);
+
+		// Delete files to force action on reset
+		assertTrue(configFile.delete());
+		assertTrue(fileBefore.delete());
+		assertTrue(fileAfter.delete());
+
+		assertTrue(gitAttributesFile.delete());
+
+		// create config file with different url
+		createLfsConfig(LFS_SERVER_URI3);
+
+		checkoutURLs.clear();
+		git.reset().setMode(ResetType.HARD).call();
+
+		checkFile(configFile, lfsConfig1);
+		checkFile(fileBefore, FAKE_LFS_POINTER1);
+		checkFile(fileAfter, FAKE_LFS_POINTER1);
+
+		assertEquals(2, checkoutURLs.size());
+		// TODO: Should may be EXPECTED_SERVR_URL1
+		assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(0));
+		assertEquals(EXPECTED_SERVER_URL1, checkoutURLs.get(1));
+	}
+
+	@Test
+	public void checkoutLfsObjects_BranchSwitch() throws Exception {
+		// Create a new branch "URL1" and add config files
+		git.checkout().setCreateBranch(true).setName("URL1").call();
+
+		createLfsFiles(FAKE_LFS_POINTER1);
+		String lfsConfig1 = addLfsConfigFiles(LFS_SERVER_URI1);
+
+		// Create a second new branch "URL2" and add config files
+		git.checkout().setCreateBranch(true).setName("URL2").call();
+
+		createLfsFiles(FAKE_LFS_POINTER2);
+		String lfsConfig2 = addLfsConfigFiles(LFS_SERVER_URI2);
+
+		checkFile(configFile, lfsConfig2);
+		checkFile(fileBefore, FAKE_LFS_POINTER2);
+		checkFile(fileAfter, FAKE_LFS_POINTER2);
+
+		checkoutURLs.clear();
+		git.checkout().setName("URL1").call();
+
+		checkFile(configFile, lfsConfig1);
+		checkFile(fileBefore, FAKE_LFS_POINTER1);
+		checkFile(fileAfter, FAKE_LFS_POINTER1);
+
+		assertEquals(2, checkoutURLs.size());
+		// TODO: Should may be EXPECTED_SERVR_URL1
+		assertEquals(EXPECTED_SERVER_URL2, checkoutURLs.get(0));
+		assertEquals(EXPECTED_SERVER_URL1, checkoutURLs.get(1));
+
+		checkoutURLs.clear();
+		git.checkout().setName("URL2").call();
+
+		checkFile(configFile, lfsConfig2);
+		checkFile(fileBefore, FAKE_LFS_POINTER2);
+		checkFile(fileAfter, FAKE_LFS_POINTER2);
+
+		assertEquals(2, checkoutURLs.size());
+		// TODO: Should may be EXPECTED_SERVR_URL2
+		assertEquals(EXPECTED_SERVER_URL1, checkoutURLs.get(0));
+		assertEquals(EXPECTED_SERVER_URL2, checkoutURLs.get(1));
+	}
+
+	@Test
+	public void checkoutLfsObjects_BranchSwitch_ModifiedLocal()
+			throws Exception {
+
+		// Create a new branch "URL1" and add config files
+		git.checkout().setCreateBranch(true).setName("URL1").call();
+
+		createLfsFiles(FAKE_LFS_POINTER1);
+		addLfsConfigFiles(LFS_SERVER_URI1);
+
+		// Create a second new branch "URL2" and add config files
+		git.checkout().setCreateBranch(true).setName("URL2").call();
+
+		createLfsFiles(FAKE_LFS_POINTER2);
+		addLfsConfigFiles(LFS_SERVER_URI1);
+
+		// create config file with different url
+		assertTrue(configFile.delete());
+		String lfsConfig3 = createLfsConfig(LFS_SERVER_URI3);
+
+		checkFile(configFile, lfsConfig3);
+		checkFile(fileBefore, FAKE_LFS_POINTER2);
+		checkFile(fileAfter, FAKE_LFS_POINTER2);
+
+		checkoutURLs.clear();
+		git.checkout().setName("URL1").call();
+
+		checkFile(fileBefore, FAKE_LFS_POINTER1);
+		checkFile(fileAfter, FAKE_LFS_POINTER1);
+		checkFile(configFile, lfsConfig3);
+
+		assertEquals(2, checkoutURLs.size());
+
+		assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(0));
+		assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(1));
+
+		checkoutURLs.clear();
+		git.checkout().setName("URL2").call();
+
+		checkFile(fileBefore, FAKE_LFS_POINTER2);
+		checkFile(fileAfter, FAKE_LFS_POINTER2);
+		checkFile(configFile, lfsConfig3);
+
+		assertEquals(2, checkoutURLs.size());
+		assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(0));
+		assertEquals(EXPECTED_SERVER_URL3, checkoutURLs.get(1));
+	}
+}
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 @@ public void setUp() throws Exception {
 	}
 
 	@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.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java
index c7bd48e..badcb7d 100644
--- a/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java
+++ b/org.eclipse.jgit.lfs.test/tst/org/eclipse/jgit/lfs/internal/LfsConnectionFactoryTest.java
@@ -9,10 +9,15 @@
  */
 package org.eclipse.jgit.lfs.internal;
 
+import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 
-import java.util.TreeMap;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.RemoteAddCommand;
@@ -27,6 +32,8 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.transport.http.HttpConnection;
+import org.eclipse.jgit.util.HttpSupport;
 import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
@@ -42,6 +49,12 @@ public class LfsConnectionFactoryTest extends RepositoryTestCase {
 			+ Constants.ATTR_FILTER_DRIVER_PREFIX
 			+ org.eclipse.jgit.lib.Constants.ATTR_FILTER_TYPE_CLEAN;
 
+	private final static String LFS_SERVER_URL1 = "https://lfs.server1/test/uri";
+
+	private final static String LFS_SERVER_URL2 = "https://lfs.server2/test/uri";
+
+	private final static String ORIGIN_URL = "https://git.server/test/uri";
+
 	private Git git;
 
 	@BeforeClass
@@ -61,26 +74,23 @@ public static void removeLfs() {
 	public void setUp() throws Exception {
 		super.setUp();
 		git = new Git(db);
+
+		// Just to have a non empty repo
+		writeTrashFile("Test.txt", "Hello world from the LFS Factory Test");
+		git.add().addFilepattern("Test.txt").call();
+		git.commit().setMessage("Initial commit").call();
 	}
 
 	@Test
 	public void lfsUrlFromRemoteUrlWithDotGit() throws Exception {
 		addRemoteUrl("https://localhost/repo.git");
-
-		String lfsUrl = LfsConnectionFactory.getLfsUrl(db,
-				Protocol.OPERATION_DOWNLOAD,
-				new TreeMap<>());
-		assertEquals("https://localhost/repo.git/info/lfs", lfsUrl);
+		checkLfsUrl("https://localhost/repo.git/info/lfs");
 	}
 
 	@Test
 	public void lfsUrlFromRemoteUrlWithoutDotGit() throws Exception {
 		addRemoteUrl("https://localhost/repo");
-
-		String lfsUrl = LfsConnectionFactory.getLfsUrl(db,
-				Protocol.OPERATION_DOWNLOAD,
-				new TreeMap<>());
-		assertEquals("https://localhost/repo.git/info/lfs", lfsUrl);
+		checkLfsUrl("https://localhost/repo.git/info/lfs");
 	}
 
 	@Test
@@ -94,10 +104,7 @@ public void lfsUrlFromLocalConfig() throws Exception {
 				"https://localhost/repo/lfs");
 		cfg.save();
 
-		String lfsUrl = LfsConnectionFactory.getLfsUrl(db,
-				Protocol.OPERATION_DOWNLOAD,
-				new TreeMap<>());
-		assertEquals("https://localhost/repo/lfs", lfsUrl);
+		checkLfsUrl("https://localhost/repo/lfs");
 	}
 
 	@Test
@@ -111,16 +118,136 @@ public void lfsUrlFromOriginConfig() throws Exception {
 				"https://localhost/repo/lfs");
 		cfg.save();
 
-		String lfsUrl = LfsConnectionFactory.getLfsUrl(db,
-				Protocol.OPERATION_DOWNLOAD,
-				new TreeMap<>());
-		assertEquals("https://localhost/repo/lfs", lfsUrl);
+		checkLfsUrl("https://localhost/repo/lfs");
 	}
 
 	@Test
 	public void lfsUrlNotConfigured() throws Exception {
-		assertThrows(LfsConfigInvalidException.class, () -> LfsConnectionFactory
-				.getLfsUrl(db, Protocol.OPERATION_DOWNLOAD, new TreeMap<>()));
+		assertThrows(LfsConfigInvalidException.class,
+				() -> LfsConnectionFactory.getLfsConnection(db,
+				HttpSupport.METHOD_POST, Protocol.OPERATION_DOWNLOAD));
+	}
+
+	@Test
+	public void checkGetLfsConnection_lfsurl_lfsconfigFromWorkingDir()
+			throws Exception {
+		writeLfsConfig();
+		checkLfsUrl(LFS_SERVER_URL1);
+	}
+
+	@Test
+	public void checkGetLfsConnection_lfsurl_lfsconfigFromIndex()
+			throws Exception {
+		writeLfsConfig();
+		git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call();
+		deleteTrashFile(Constants.DOT_LFS_CONFIG);
+		checkLfsUrl(LFS_SERVER_URL1);
+	}
+
+	@Test
+	public void checkGetLfsConnection_lfsurl_lfsconfigFromHEAD()
+			throws Exception {
+		writeLfsConfig();
+		git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call();
+		git.commit().setMessage("Commit LFS Config").call();
+
+		/*
+		 * reading .lfsconfig from HEAD seems only testable using a bare repo,
+		 * since otherwise working tree or index are used
+		 */
+		File directory = createTempDirectory("testBareRepo");
+		try (Repository bareRepoDb = Git.cloneRepository()
+				.setDirectory(directory)
+				.setURI(db.getDirectory().toURI().toString()).setBare(true)
+				.call().getRepository()) {
+
+			checkLfsUrl(LFS_SERVER_URL1);
+		}
+	}
+
+	@Test
+	public void checkGetLfsConnection_remote_lfsconfigFromWorkingDir()
+			throws Exception {
+		addRemoteUrl(ORIGIN_URL);
+		writeLfsConfig(LFS_SERVER_URL1, "lfs", DEFAULT_REMOTE_NAME, "url");
+		checkLfsUrl(LFS_SERVER_URL1);
+	}
+
+	/**
+	 * Test the config file precedence.
+	 *
+	 * Checking only with the local repository config is sufficient since from
+	 * that point the "normal" precedence is used.
+	 *
+	 * @throws Exception
+	 */
+	@Test
+	public void checkGetLfsConnection_ConfigFilePrecedence_lfsconfigFromWorkingDir()
+			throws Exception {
+		writeLfsConfig();
+		checkLfsUrl(LFS_SERVER_URL1);
+
+		StoredConfig config = git.getRepository().getConfig();
+		config.setString(ConfigConstants.CONFIG_SECTION_LFS, null,
+				ConfigConstants.CONFIG_KEY_URL, LFS_SERVER_URL2);
+		config.save();
+
+		checkLfsUrl(LFS_SERVER_URL2);
+	}
+
+	@Test
+	public void checkGetLfsConnection_InvalidLfsConfig_WorkingDir()
+			throws Exception {
+		writeInvalidLfsConfig();
+		LfsConfigInvalidException actualException = assertThrows(
+				LfsConfigInvalidException.class, () -> {
+			LfsConnectionFactory.getLfsConnection(db, HttpSupport.METHOD_POST,
+					Protocol.OPERATION_DOWNLOAD);
+		});
+		assertTrue(getStackTrace(actualException)
+				.contains("Invalid line in config file"));
+	}
+
+	@Test
+	public void checkGetLfsConnection_InvalidLfsConfig_Index()
+			throws Exception {
+		writeInvalidLfsConfig();
+		git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call();
+		deleteTrashFile(Constants.DOT_LFS_CONFIG);
+		LfsConfigInvalidException actualException = assertThrows(
+				LfsConfigInvalidException.class, () -> {
+			LfsConnectionFactory.getLfsConnection(db, HttpSupport.METHOD_POST,
+					Protocol.OPERATION_DOWNLOAD);
+		});
+		assertTrue(getStackTrace(actualException)
+				.contains("Invalid line in config file"));
+	}
+
+	@Test
+	public void checkGetLfsConnection_InvalidLfsConfig_HEAD() throws Exception {
+		writeInvalidLfsConfig();
+		git.add().addFilepattern(Constants.DOT_LFS_CONFIG).call();
+		git.commit().setMessage("Commit LFS Config").call();
+
+		/*
+		 * reading .lfsconfig from HEAD seems only testable using a bare repo,
+		 * since otherwise working tree or index are used
+		 */
+		File directory = createTempDirectory("testBareRepo");
+		try (Repository bareRepoDb = Git.cloneRepository()
+				.setDirectory(directory)
+				.setURI(db.getDirectory().toURI().toString()).setBare(true)
+				.call().getRepository()) {
+			LfsConfigInvalidException actualException = assertThrows(
+					LfsConfigInvalidException.class,
+					() -> {
+						LfsConnectionFactory.getLfsConnection(db,
+								HttpSupport.METHOD_POST,
+								Protocol.OPERATION_DOWNLOAD);
+					});
+			assertTrue(getStackTrace(actualException)
+					.contains("Invalid line in config file"));
+		}
 	}
 
 	private void addRemoteUrl(String remotUrl) throws Exception {
@@ -129,4 +256,62 @@ private void addRemoteUrl(String remotUrl) throws Exception {
 		add.setName(org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME);
 		add.call();
 	}
+
+	/**
+	 * Returns the stack trace of the provided exception as string
+	 *
+	 * @param actualException
+	 * @return The exception stack trace as string
+	 */
+	private String getStackTrace(Exception actualException) {
+		StringWriter sw = new StringWriter();
+		PrintWriter pw = new PrintWriter(sw);
+		actualException.printStackTrace(pw);
+		return sw.toString();
+	}
+
+	private void writeLfsConfig() throws IOException {
+		writeLfsConfig(LFS_SERVER_URL1, "lfs", "url");
+	}
+
+	private void writeLfsConfig(String lfsUrl, String section, String name)
+			throws IOException {
+		writeLfsConfig(lfsUrl, section, null, name);
+	}
+
+	/*
+	 * Write simple lfs config with single entry. Do not use FileBasedConfig to
+	 * avoid introducing new dependency (for now).
+	 */
+	private void writeLfsConfig(String lfsUrl, String section,
+			String subsection, String name) throws IOException {
+		StringBuilder config = new StringBuilder();
+		config.append("[");
+		config.append(section);
+		if (subsection != null) {
+			config.append(" \"");
+			config.append(subsection);
+			config.append("\"");
+		}
+		config.append("]\n");
+		config.append("    ");
+		config.append(name);
+		config.append(" = ");
+		config.append(lfsUrl);
+		writeTrashFile(Constants.DOT_LFS_CONFIG, config.toString());
+	}
+
+	private void writeInvalidLfsConfig() throws IOException {
+		writeTrashFile(Constants.DOT_LFS_CONFIG,
+				"{lfs]\n    url = " + LFS_SERVER_URL1);
+	}
+
+	private void checkLfsUrl(String lfsUrl) throws IOException {
+		HttpConnection lfsServerConn;
+		lfsServerConn = LfsConnectionFactory.getLfsConnection(db,
+				HttpSupport.METHOD_POST, Protocol.OPERATION_DOWNLOAD);
+
+		assertEquals(lfsUrl + Protocol.OBJECTS_LFS_ENDPOINT,
+				lfsServerConn.getURL().toString());
+	}
 }
diff --git a/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties b/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties
index 0e00f14..642b83d 100644
--- a/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties
+++ b/org.eclipse.jgit.lfs/resources/org/eclipse/jgit/lfs/internal/LfsText.properties
@@ -1,19 +1,19 @@
 corruptLongObject=The content hash ''{0}'' of the long object ''{1}'' doesn''t match its id, the corrupt object will be deleted.
-incorrectLONG_OBJECT_ID_LENGTH=Incorrect LONG_OBJECT_ID_LENGTH.
-inconsistentMediafileLength=Mediafile {0} has unexpected length; expected {1} but found {2}.
+dotLfsConfigReadFailed=Reading .lfsconfig failed
 inconsistentContentLength=Unexpected content length reported by LFS server ({0}), expected {1} but reported was {2}
+inconsistentMediafileLength=Mediafile {0} has unexpected length; expected {1} but found {2}.
+incorrectLONG_OBJECT_ID_LENGTH=Incorrect LONG_OBJECT_ID_LENGTH.
 invalidLongId=Invalid id: {0}
 invalidLongIdLength=Invalid id length {0}; should be {1}
-lfsUnavailable=LFS is not available for repository {0}
-protocolError=LFS Protocol Error {0}: {1}
-requiredHashFunctionNotAvailable=Required hash function {0} not available.
-repositoryNotFound=Repository {0} not found
-repositoryReadOnly=Repository {0} is read-only
-lfsUnavailable=LFS is not available for repository {0}
-lfsUnathorized=Not authorized to perform operation {0} on repository {1}
 lfsFailedToGetRepository=failed to get repository {0}
 lfsNoDownloadUrl="Need to download object from LFS server but couldn't determine LFS server URL"
+lfsUnauthorized=Not authorized to perform operation {0} on repository {1}
+lfsUnavailable=LFS is not available for repository {0}
+missingLocalObject="Local Object {0} is missing"
+protocolError=LFS Protocol Error {0}: {1}
+repositoryNotFound=Repository {0} not found
+repositoryReadOnly=Repository {0} is read-only
+requiredHashFunctionNotAvailable=Required hash function {0} not available.
 serverFailure=When trying to open a connection to {0} the server responded with an error code. rc={1}
-wrongAmoutOfDataReceived=While downloading data from the content server {0} {1} bytes have been received while {2} have been expected
 userConfigInvalid="User config file {0} invalid {1}"
-missingLocalObject="Local Object {0} is missing"
\ No newline at end of file
+wrongAmountOfDataReceived=While downloading data from the content server {0} {1} bytes have been received while {2} have been expected
\ No newline at end of file
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java
index 3411887..c26a1bf 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/SmudgeFilter.java
@@ -205,7 +205,7 @@ public static Collection<Path> downloadLfsResource(Lfs lfs, Repository db,
 					long bytesCopied = Files.copy(contentIn, path);
 					if (bytesCopied != o.size) {
 						throw new IOException(MessageFormat.format(
-								LfsText.get().wrongAmoutOfDataReceived,
+								LfsText.get().wrongAmountOfDataReceived,
 								contentServerConn.getURL(),
 								Long.valueOf(bytesCopied),
 								Long.valueOf(o.size)));
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java
index 36889db..0dc6aea 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/errors/LfsUnauthorized.java
@@ -31,7 +31,7 @@ public class LfsUnauthorized extends LfsException {
 	 *            the repository name.
 	 */
 	public LfsUnauthorized(String operation, String name) {
-		super(MessageFormat.format(LfsText.get().lfsUnathorized, operation,
+		super(MessageFormat.format(LfsText.get().lfsUnauthorized, operation,
 				name));
 	}
 }
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConfig.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConfig.java
new file mode 100644
index 0000000..71d395c
--- /dev/null
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConfig.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2022, Matthias Fromme <mfromme@dspace.de>
+ *
+ * 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.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.lfs.internal;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lfs.errors.LfsConfigInvalidException;
+import org.eclipse.jgit.lfs.lib.Constants;
+import org.eclipse.jgit.lib.BlobBasedConfig;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.treewalk.TreeWalk;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+
+/**
+ * Encapsulate access to the .lfsconfig.
+ *
+ * According to the document
+ * https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-config.5.ronn
+ * the order to find the .lfsconfig file is:
+ *
+ * <pre>
+ *   1. in the root of the working tree
+ *   2. in the index
+ *   3. in the HEAD, for bare repositories this is the only place
+ *      that is searched
+ * </pre>
+ *
+ * Values from the .lfsconfig are used only if not specified in another git
+ * config file to allow local override without modifiction of a committed file.
+ */
+public class LfsConfig {
+	private Repository db;
+	private Config delegate;
+
+	/**
+	 * Create a new instance of the LfsConfig.
+	 *
+	 * @param db
+	 *            the associated repo
+	 * @throws IOException
+	 */
+	public LfsConfig(Repository db) throws IOException {
+		this.db = db;
+		delegate = this.load();
+	}
+
+	/**
+	 * Read the .lfsconfig file from the repository
+	 *
+	 * @return The loaded lfs config or null if it does not exist
+	 *
+	 * @throws IOException
+	 */
+	private Config load() throws IOException {
+		Config result = null;
+
+		if (!db.isBare()) {
+			result = loadFromWorkingTree();
+			if (result == null) {
+				result = loadFromIndex();
+			}
+		}
+
+		if (result == null) {
+			result = loadFromHead();
+		}
+
+		if (result == null) {
+			result = emptyConfig();
+		}
+
+		return result;
+	}
+
+	/**
+	 * Try to read the lfs config from a file called .lfsconfig at the top level
+	 * of the working tree.
+	 *
+	 * @return the config, or <code>null</code>
+	 * @throws IOException
+	 */
+	@Nullable
+	private Config loadFromWorkingTree()
+			throws IOException {
+		File lfsConfig = db.getFS().resolve(db.getWorkTree(),
+				Constants.DOT_LFS_CONFIG);
+		if (lfsConfig.exists() && lfsConfig.isFile()) {
+			FileBasedConfig config = new FileBasedConfig(lfsConfig, db.getFS());
+			try {
+				config.load();
+				return config;
+			} catch (ConfigInvalidException e) {
+				throw new LfsConfigInvalidException(
+						LfsText.get().dotLfsConfigReadFailed, e);
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Try to read the lfs config from an entry called .lfsconfig contained in
+	 * the index.
+	 *
+	 * @return the config, or <code>null</code> if the entry does not exist
+	 * @throws IOException
+	 */
+	@Nullable
+	private Config loadFromIndex()
+			throws IOException {
+		try {
+			DirCacheEntry entry = db.readDirCache()
+					.getEntry(Constants.DOT_LFS_CONFIG);
+			if (entry != null) {
+				return new BlobBasedConfig(null, db, entry.getObjectId());
+			}
+		} catch (ConfigInvalidException e) {
+			throw new LfsConfigInvalidException(
+					LfsText.get().dotLfsConfigReadFailed, e);
+		}
+		return null;
+	}
+
+	/**
+	 * Try to read the lfs config from an entry called .lfsconfig contained in
+	 * the head revision.
+	 *
+	 * @return the config, or <code>null</code> if the file does not exist
+	 * @throws IOException
+	 */
+	@Nullable
+	private Config loadFromHead() throws IOException {
+		try (RevWalk revWalk = new RevWalk(db)) {
+			ObjectId headCommitId = db.resolve(HEAD);
+			if (headCommitId == null) {
+				return null;
+			}
+			RevCommit commit = revWalk.parseCommit(headCommitId);
+			RevTree tree = commit.getTree();
+			TreeWalk treewalk = TreeWalk.forPath(db, Constants.DOT_LFS_CONFIG,
+					tree);
+			if (treewalk != null) {
+				return new BlobBasedConfig(null, db, treewalk.getObjectId(0));
+			}
+		} catch (ConfigInvalidException e) {
+			throw new LfsConfigInvalidException(
+					LfsText.get().dotLfsConfigReadFailed, e);
+		}
+		return null;
+	}
+
+	/**
+	 * Create an empty config as fallback to avoid null pointer checks.
+	 *
+	 * @return an empty config
+	 */
+	private Config emptyConfig() {
+		return new Config();
+	}
+
+	/**
+	 * Get string value or null if not found.
+	 *
+	 * First tries to find the value in the git config files. If not found tries
+	 * to find data in .lfsconfig.
+	 *
+	 * @param section
+	 *            the section
+	 * @param subsection
+	 *            the subsection for the value
+	 * @param name
+	 *            the key name
+	 * @return a String value from the config, <code>null</code> if not found
+	 */
+	public String getString(final String section, final String subsection,
+			final String name) {
+		String result = db.getConfig().getString(section, subsection, name);
+		if (result == null) {
+			result = delegate.getString(section, subsection, name);
+		}
+		return result;
+	}
+}
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java
index 5a17d41..12b688d 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsConnectionFactory.java
@@ -45,7 +45,6 @@
  * Provides means to get a valid LFS connection for a given repository.
  */
 public class LfsConnectionFactory {
-
 	private static final int SSH_AUTH_TIMEOUT_SECONDS = 30;
 	private static final String SCHEME_HTTPS = "https"; //$NON-NLS-1$
 	private static final String SCHEME_SSH = "ssh"; //$NON-NLS-1$
@@ -104,19 +103,19 @@ public static HttpConnection getLfsConnection(Repository db, String method,
 	 *            additional headers that can be used to connect to LFS server
 	 * @return the URL for the LFS server. e.g.
 	 *         "https://github.com/github/git-lfs.git/info/lfs"
-	 * @throws LfsConfigInvalidException
-	 *             if the LFS config is invalid
+	 * @throws IOException
+	 *             if the LFS config is invalid or cannot be accessed
 	 * @see <a href=
 	 *      "https://github.com/git-lfs/git-lfs/blob/main/docs/api/server-discovery.md">
 	 *      Server Discovery documentation</a>
 	 */
-	static String getLfsUrl(Repository db, String purpose,
+	private static String getLfsUrl(Repository db, String purpose,
 			Map<String, String> additionalHeaders)
-			throws LfsConfigInvalidException {
-		StoredConfig config = db.getConfig();
+			throws IOException {
+		LfsConfig config = new LfsConfig(db);
 		String lfsUrl = config.getString(ConfigConstants.CONFIG_SECTION_LFS,
-				null,
-				ConfigConstants.CONFIG_KEY_URL);
+				null, ConfigConstants.CONFIG_KEY_URL);
+
 		Exception ex = null;
 		if (lfsUrl == null) {
 			String remoteUrl = null;
@@ -124,6 +123,7 @@ static String getLfsUrl(Repository db, String purpose,
 				lfsUrl = config.getString(ConfigConstants.CONFIG_SECTION_LFS,
 						remote,
 						ConfigConstants.CONFIG_KEY_URL);
+
 				// This could be done better (more precise logic), but according
 				// to https://github.com/git-lfs/git-lfs/issues/1759 git-lfs
 				// generally only supports 'origin' in an integrated workflow.
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java
index 1ca37a9..06234c1 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/LfsText.java
@@ -28,21 +28,22 @@ public static LfsText get() {
 
 	// @formatter:off
 	/***/ public String corruptLongObject;
-	/***/ public String inconsistentMediafileLength;
+	/***/ public String dotLfsConfigReadFailed;
 	/***/ public String inconsistentContentLength;
+	/***/ public String inconsistentMediafileLength;
 	/***/ public String incorrectLONG_OBJECT_ID_LENGTH;
 	/***/ public String invalidLongId;
 	/***/ public String invalidLongIdLength;
-	/***/ public String lfsUnavailable;
-	/***/ public String protocolError;
-	/***/ public String requiredHashFunctionNotAvailable;
-	/***/ public String repositoryNotFound;
-	/***/ public String repositoryReadOnly;
-	/***/ public String lfsUnathorized;
 	/***/ public String lfsFailedToGetRepository;
 	/***/ public String lfsNoDownloadUrl;
-	/***/ public String serverFailure;
-	/***/ public String wrongAmoutOfDataReceived;
-	/***/ public String userConfigInvalid;
+	/***/ public String lfsUnauthorized;
+	/***/ public String lfsUnavailable;
 	/***/ public String missingLocalObject;
+	/***/ public String protocolError;
+	/***/ public String repositoryNotFound;
+	/***/ public String repositoryReadOnly;
+	/***/ public String requiredHashFunctionNotAvailable;
+	/***/ public String serverFailure;
+	/***/ public String userConfigInvalid;
+	/***/ public String wrongAmountOfDataReceived;
 }
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/Constants.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/Constants.java
index 3212a63..9b41ec3 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/Constants.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/lib/Constants.java
@@ -82,6 +82,13 @@ public final class Constants {
 	public static final String ATTR_FILTER_DRIVER_PREFIX = "lfs/";
 
 	/**
+	 * Config file name for lfs specific configuration
+	 *
+	 * @since 6.1
+	 */
+	public static final String DOT_LFS_CONFIG = ".lfsconfig";
+
+	/**
 	 * Create a new digest function for objects.
 	 *
 	 * @return a new digest object.
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java
index 1a1f5b4..6f7aa63 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java
@@ -10,6 +10,7 @@
 package org.eclipse.jgit.api;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
@@ -392,28 +393,64 @@ public void testPushDefaultMatching() throws Exception {
 			git.add().addFilepattern("f").call();
 			RevCommit commit = git.commit().setMessage("adding f").call();
 
-			git.checkout().setName("also-pushed").setCreateBranch(true).call();
+			git.checkout().setName("not-pushed").setCreateBranch(true).call();
 			git.checkout().setName("branchtopush").setCreateBranch(true).call();
 
 			assertEquals(null,
 					git2.getRepository().resolve("refs/heads/branchtopush"));
 			assertEquals(null,
-					git2.getRepository().resolve("refs/heads/also-pushed"));
+					git2.getRepository().resolve("refs/heads/not-pushed"));
 			assertEquals(null,
 					git2.getRepository().resolve("refs/heads/master"));
-			git.push().setRemote("test").setPushDefault(PushDefault.MATCHING)
+			// push master and branchtopush
+			git.push().setRemote("test").setRefSpecs(
+					new RefSpec("refs/heads/master:refs/heads/master"),
+					new RefSpec(
+							"refs/heads/branchtopush:refs/heads/branchtopush"))
 					.call();
 			assertEquals(commit.getId(),
-					git2.getRepository().resolve("refs/heads/branchtopush"));
-			assertEquals(commit.getId(),
-					git2.getRepository().resolve("refs/heads/also-pushed"));
-			assertEquals(commit.getId(),
 					git2.getRepository().resolve("refs/heads/master"));
-			assertEquals(commit.getId(), git.getRepository()
-					.resolve("refs/remotes/origin/branchtopush"));
-			assertEquals(commit.getId(), git.getRepository()
-					.resolve("refs/remotes/origin/also-pushed"));
 			assertEquals(commit.getId(),
+					git2.getRepository().resolve("refs/heads/branchtopush"));
+			assertEquals(null,
+					git2.getRepository().resolve("refs/heads/not-pushed"));
+			// Create two different commits on these two branches
+			writeTrashFile("b", "on branchtopush");
+			git.add().addFilepattern("b").call();
+			RevCommit bCommit = git.commit().setMessage("on branchtopush")
+					.call();
+			git.checkout().setName("master").call();
+			writeTrashFile("m", "on master");
+			git.add().addFilepattern("m").call();
+			RevCommit mCommit = git.commit().setMessage("on master").call();
+			// Now push with mode "matching": should push both branches.
+			Iterable<PushResult> result = git.push().setRemote("test")
+					.setPushDefault(PushDefault.MATCHING)
+					.call();
+			int n = 0;
+			for (PushResult r : result) {
+				n++;
+				assertEquals(1, n);
+				assertEquals(2, r.getRemoteUpdates().size());
+				for (RemoteRefUpdate update : r.getRemoteUpdates()) {
+					assertFalse(update.isMatching());
+					assertTrue(update.getSrcRef()
+							.equals("refs/heads/branchtopush")
+							|| update.getSrcRef().equals("refs/heads/master"));
+					assertEquals(RemoteRefUpdate.Status.OK, update.getStatus());
+				}
+			}
+			assertEquals(bCommit.getId(),
+					git2.getRepository().resolve("refs/heads/branchtopush"));
+			assertEquals(null,
+					git2.getRepository().resolve("refs/heads/not-pushed"));
+			assertEquals(mCommit.getId(),
+					git2.getRepository().resolve("refs/heads/master"));
+			assertEquals(bCommit.getId(), git.getRepository()
+					.resolve("refs/remotes/origin/branchtopush"));
+			assertEquals(null, git.getRepository()
+					.resolve("refs/remotes/origin/not-pushed"));
+			assertEquals(mCommit.getId(),
 					git.getRepository().resolve("refs/remotes/origin/master"));
 		}
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StatusCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StatusCommandTest.java
index 5311edb..19281f6 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StatusCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StatusCommandTest.java
@@ -21,8 +21,10 @@
 import org.eclipse.jgit.api.errors.NoFilepatternException;
 import org.eclipse.jgit.errors.NoWorkTreeException;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.Sets;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
 import org.eclipse.jgit.util.FS;
 import org.junit.Test;
 
@@ -181,4 +183,31 @@ public void testFolderPrefix() throws Exception {
 		}
 	}
 
+	@Test
+	public void testNestedCommittedGitRepoAndPathFilter() throws Exception {
+		commitFile("file.txt", "file", "master");
+		try (Repository inner = new FileRepositoryBuilder()
+				.setWorkTree(new File(db.getWorkTree(), "subgit")).build()) {
+			inner.create();
+			writeTrashFile("subgit/sub.txt", "sub");
+			try (Git outerGit = new Git(db); Git innerGit = new Git(inner)) {
+				innerGit.add().addFilepattern("sub.txt").call();
+				innerGit.commit().setMessage("Inner commit").call();
+				outerGit.add().addFilepattern("subgit").call();
+				outerGit.commit().setMessage("Outer commit").call();
+				assertTrue(innerGit.status().call().isClean());
+				assertTrue(outerGit.status().call().isClean());
+				writeTrashFile("subgit/sub.txt", "sub2");
+				assertFalse(innerGit.status().call().isClean());
+				assertFalse(outerGit.status().call().isClean());
+				assertTrue(
+						outerGit.status().addPath("file.txt").call().isClean());
+				assertTrue(outerGit.status().addPath("doesntexist").call()
+						.isClean());
+				assertFalse(
+						outerGit.status().addPath("subgit").call().isClean());
+			}
+		}
+	}
+
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java
index 6928859..2e8b30f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushProcessTest.java
@@ -14,14 +14,19 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 
+import org.eclipse.jgit.api.errors.AbortedByHookException;
 import org.eclipse.jgit.errors.NotSupportedException;
 import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.hooks.PrePushHook;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
 import org.eclipse.jgit.lib.ProgressMonitor;
@@ -31,6 +36,7 @@
 import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
 import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
+import org.eclipse.jgit.util.io.NullOutputStream;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -220,7 +226,17 @@ public void testUpdateUnexpectedRemoteVsForce() throws IOException {
 						.fromString("0000000000000000000000000000000000000001"));
 		final Ref ref = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE, "refs/heads/master",
 				ObjectId.fromString("ac7e7e44c1885efb472ad54a78327d66bfc4ecef"));
-		testOneUpdateStatus(rru, ref, Status.REJECTED_REMOTE_CHANGED, null);
+		try (ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+				PrintStream out = new PrintStream(bytes, true,
+						StandardCharsets.UTF_8);
+				PrintStream err = new PrintStream(NullOutputStream.INSTANCE)) {
+			MockPrePushHook hook = new MockPrePushHook(db, out, err);
+			testOneUpdateStatus(rru, ref, Status.REJECTED_REMOTE_CHANGED, null,
+					hook);
+			out.flush();
+			String result = new String(bytes.toString(StandardCharsets.UTF_8));
+			assertEquals("", result);
+		}
 	}
 
 	/**
@@ -256,10 +272,22 @@ public void testUpdateMixedCases() throws IOException {
 		refUpdates.add(rruOk);
 		refUpdates.add(rruReject);
 		advertisedRefs.add(refToChange);
-		executePush();
-		assertEquals(Status.OK, rruOk.getStatus());
-		assertTrue(rruOk.isFastForward());
-		assertEquals(Status.NON_EXISTING, rruReject.getStatus());
+		try (ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+				PrintStream out = new PrintStream(bytes, true,
+						StandardCharsets.UTF_8);
+				PrintStream err = new PrintStream(NullOutputStream.INSTANCE)) {
+			MockPrePushHook hook = new MockPrePushHook(db, out, err);
+			executePush(hook);
+			assertEquals(Status.OK, rruOk.getStatus());
+			assertTrue(rruOk.isFastForward());
+			assertEquals(Status.NON_EXISTING, rruReject.getStatus());
+			out.flush();
+			String result = new String(bytes.toString(StandardCharsets.UTF_8));
+			assertEquals(
+					"null 0000000000000000000000000000000000000000 "
+							+ "refs/heads/master 2c349335b7f797072cf729c4f3bb0914ecb6dec9\n",
+					result);
+		}
 	}
 
 	/**
@@ -346,10 +374,18 @@ private PushResult testOneUpdateStatus(final RemoteRefUpdate rru,
 			final Ref advertisedRef, final Status expectedStatus,
 			Boolean fastForward) throws NotSupportedException,
 			TransportException {
+		return testOneUpdateStatus(rru, advertisedRef, expectedStatus,
+				fastForward, null);
+	}
+
+	private PushResult testOneUpdateStatus(final RemoteRefUpdate rru,
+			final Ref advertisedRef, final Status expectedStatus,
+			Boolean fastForward, PrePushHook hook)
+			throws NotSupportedException, TransportException {
 		refUpdates.add(rru);
 		if (advertisedRef != null)
 			advertisedRefs.add(advertisedRef);
-		final PushResult result = executePush();
+		final PushResult result = executePush(hook);
 		assertEquals(expectedStatus, rru.getStatus());
 		if (fastForward != null)
 			assertEquals(fastForward, Boolean.valueOf(rru.isFastForward()));
@@ -358,7 +394,12 @@ private PushResult testOneUpdateStatus(final RemoteRefUpdate rru,
 
 	private PushResult executePush() throws NotSupportedException,
 			TransportException {
-		process = new PushProcess(transport, refUpdates);
+		return executePush(null);
+	}
+
+	private PushResult executePush(PrePushHook hook)
+			throws NotSupportedException, TransportException {
+		process = new PushProcess(transport, refUpdates, hook);
 		return process.execute(new TextProgressMonitor());
 	}
 
@@ -416,4 +457,20 @@ public void push(ProgressMonitor monitor,
 			}
 		}
 	}
+
+	private static class MockPrePushHook extends PrePushHook {
+
+		private final PrintStream output;
+
+		public MockPrePushHook(Repository repo, PrintStream out,
+				PrintStream err) {
+			super(repo, out, err);
+			output = out;
+		}
+
+		@Override
+		protected void doRun() throws AbortedByHookException, IOException {
+			output.print(getStdinArgs());
+		}
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java
index 5569bca..b56308c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java
@@ -466,4 +466,18 @@ public void onlyWildCard() {
 		assertTrue(a.matchSource("refs/heads/master"));
 		assertNull(a.getDestination());
 	}
+
+	@Test
+	public void matching() {
+		RefSpec a = new RefSpec(":");
+		assertTrue(a.isMatching());
+		assertFalse(a.isForceUpdate());
+	}
+
+	@Test
+	public void matchingForced() {
+		RefSpec a = new RefSpec("+:");
+		assertTrue(a.isMatching());
+		assertTrue(a.isForceUpdate());
+	}
 }
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 @@ public void setUp() throws Exception {
 		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 void testBuiltinCleanAndSmudgeFilter() throws IOException, GitAPIExceptio
 		config.save();
 	}
 
+	@Test
+	public void testBranchSwitch() throws Exception {
+		String builtinCommandPrefix = "jgit://builtin/test/";
+		FilterCommandRegistry.register(builtinCommandPrefix + "smudge",
+				new TestCommandFactory('s'));
+		FilterCommandRegistry.register(builtinCommandPrefix + "clean",
+				new TestCommandFactory('c'));
+		StoredConfig config = git.getRepository().getConfig();
+		config.setString("filter", "test", "smudge",
+				builtinCommandPrefix + "smudge");
+		config.setString("filter", "test", "clean",
+				builtinCommandPrefix + "clean");
+		config.save();
+		// We're on the test branch
+		File aFile = writeTrashFile("a.txt", "a");
+		writeTrashFile(".gitattributes", "a.txt filter=test");
+		File cFile = writeTrashFile("cc/c.txt", "C");
+		writeTrashFile("cc/.gitattributes", "c.txt filter=test");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("On test").call();
+		git.checkout().setName("master").call();
+		git.branchCreate().setName("other").call();
+		git.checkout().setName("other").call();
+		writeTrashFile("b.txt", "b");
+		writeTrashFile(".gitattributes", "b.txt filter=test");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("On other").call();
+		git.checkout().setName("test").call();
+		checkFile(aFile, "scsa");
+		checkFile(cFile, "scsC");
+	}
+
+	@Test
+	public void testCheckoutSingleFile() throws Exception {
+		String builtinCommandPrefix = "jgit://builtin/test/";
+		FilterCommandRegistry.register(builtinCommandPrefix + "smudge",
+				new TestCommandFactory('s'));
+		FilterCommandRegistry.register(builtinCommandPrefix + "clean",
+				new TestCommandFactory('c'));
+		StoredConfig config = git.getRepository().getConfig();
+		config.setString("filter", "test", "smudge",
+				builtinCommandPrefix + "smudge");
+		config.setString("filter", "test", "clean",
+				builtinCommandPrefix + "clean");
+		config.save();
+		// We're on the test branch
+		File aFile = writeTrashFile("a.txt", "a");
+		File attributes = writeTrashFile(".gitattributes", "a.txt filter=test");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("On test").call();
+		git.checkout().setName("master").call();
+		git.branchCreate().setName("other").call();
+		git.checkout().setName("other").call();
+		writeTrashFile("b.txt", "b");
+		writeTrashFile(".gitattributes", "b.txt filter=test");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("On other").call();
+		git.checkout().setName("master").call();
+		assertFalse(aFile.exists());
+		assertFalse(attributes.exists());
+		git.checkout().setStartPoint("test").addPath("a.txt").call();
+		checkFile(aFile, "scsa");
+	}
+
+	@Test
+	public void testCheckoutSingleFile2() throws Exception {
+		String builtinCommandPrefix = "jgit://builtin/test/";
+		FilterCommandRegistry.register(builtinCommandPrefix + "smudge",
+				new TestCommandFactory('s'));
+		FilterCommandRegistry.register(builtinCommandPrefix + "clean",
+				new TestCommandFactory('c'));
+		StoredConfig config = git.getRepository().getConfig();
+		config.setString("filter", "test", "smudge",
+				builtinCommandPrefix + "smudge");
+		config.setString("filter", "test", "clean",
+				builtinCommandPrefix + "clean");
+		config.save();
+		// We're on the test branch
+		File aFile = writeTrashFile("a.txt", "a");
+		File attributes = writeTrashFile(".gitattributes", "a.txt filter=test");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("On test").call();
+		git.checkout().setName("master").call();
+		git.branchCreate().setName("other").call();
+		git.checkout().setName("other").call();
+		writeTrashFile("b.txt", "b");
+		writeTrashFile(".gitattributes", "b.txt filter=test");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("On other").call();
+		git.checkout().setName("master").call();
+		assertFalse(aFile.exists());
+		assertFalse(attributes.exists());
+		writeTrashFile(".gitattributes", "");
+		git.checkout().setStartPoint("test").addPath("a.txt").call();
+		checkFile(aFile, "scsa");
+	}
+
+	@Test
+	public void testMerge() throws Exception {
+		String builtinCommandPrefix = "jgit://builtin/test/";
+		FilterCommandRegistry.register(builtinCommandPrefix + "smudge",
+				new TestCommandFactory('s'));
+		FilterCommandRegistry.register(builtinCommandPrefix + "clean",
+				new TestCommandFactory('c'));
+		StoredConfig config = git.getRepository().getConfig();
+		config.setString("filter", "test", "smudge",
+				builtinCommandPrefix + "smudge");
+		config.setString("filter", "test", "clean",
+				builtinCommandPrefix + "clean");
+		config.save();
+		// We're on the test branch. Set up two branches that are expected to
+		// merge cleanly.
+		File aFile = writeTrashFile("a.txt", "a");
+		writeTrashFile(".gitattributes", "a.txt filter=test");
+		git.add().addFilepattern(".").call();
+		RevCommit aCommit = git.commit().setMessage("On test").call();
+		git.checkout().setName("master").call();
+		assertFalse(aFile.exists());
+		git.branchCreate().setName("other").call();
+		git.checkout().setName("other").call();
+		writeTrashFile("b/b.txt", "b");
+		writeTrashFile("b/.gitattributes", "b.txt filter=test");
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("On other").call();
+		MergeResult result = git.merge().include(aCommit).call();
+		assertEquals(MergeResult.MergeStatus.MERGED, result.getMergeStatus());
+		checkFile(aFile, "scsa");
+	}
+
 }
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index 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/api/PushCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java
index 4f57f35..08353df 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java
@@ -230,7 +230,7 @@ private void determineDefaultRefSpecs(Config config)
 			refSpecs.add(new RefSpec(getCurrentBranch()));
 			break;
 		case MATCHING:
-			setPushAll();
+			refSpecs.add(new RefSpec(":")); //$NON-NLS-1$
 			break;
 		case NOTHING:
 			throw new InvalidRefNameException(
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 @@ 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 Attributes getAttributes() throws IOException {
 				isDirectory,
 				treeWalk.getTree(WorkingTreeIterator.class),
 				treeWalk.getTree(DirCacheIterator.class),
-				treeWalk.getTree(CanonicalTreeParser.class),
+				attributesTree.get(),
 				attributes);
 
 		// Gets the attributes located in the global attribute file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
index 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 @@ public void preScanTwoTrees() throws CorruptObjectException, IOException {
 		walk = new NameConflictTreeWalk(repo);
 		builder = dc.builder();
 
-		addTree(walk, headCommitTree);
+		walk.setHead(addTree(walk, headCommitTree));
 		addTree(walk, mergeCommitTree);
 		int dciPos = walk.addTree(new DirCacheBuildIterator(builder));
 		walk.addTree(workingTree);
@@ -315,13 +316,6 @@ public void preScanTwoTrees() throws CorruptObjectException, IOException {
 		}
 	}
 
-	private void addTree(TreeWalk tw, ObjectId id) throws MissingObjectException, IncorrectObjectTypeException, IOException {
-		if (id == null)
-			tw.addTree(new EmptyTreeIterator());
-		else
-			tw.addTree(id);
-	}
-
 	/**
 	 * Scan index and merge tree (no HEAD). Used e.g. for initial checkout when
 	 * there is no head yet.
@@ -341,7 +335,7 @@ public void prescanOneTree()
 		builder = dc.builder();
 
 		walk = new NameConflictTreeWalk(repo);
-		addTree(walk, mergeCommitTree);
+		walk.setHead(addTree(walk, mergeCommitTree));
 		int dciPos = walk.addTree(new DirCacheBuildIterator(builder));
 		walk.addTree(workingTree);
 		workingTree.setDirCacheIterator(walk, dciPos);
@@ -356,6 +350,14 @@ public void prescanOneTree()
 		conflicts.removeAll(removed);
 	}
 
+	private int addTree(TreeWalk tw, ObjectId id) throws MissingObjectException,
+			IncorrectObjectTypeException, IOException {
+		if (id == null) {
+			return tw.addTree(new EmptyTreeIterator());
+		}
+		return tw.addTree(id);
+	}
+
 	/**
 	 * Processing an entry in the context of {@link #prescanOneTree()} when only
 	 * one tree is given
@@ -382,17 +384,14 @@ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i,
 						// failOnConflict is false. Putting something to conflicts
 						// would mean we delete it. Instead we want the mergeCommit
 						// content to be checked out.
-						update(m.getEntryPathString(), m.getEntryObjectId(),
-								m.getEntryFileMode());
+						update(m);
 					}
 				} else
-					update(m.getEntryPathString(), m.getEntryObjectId(),
-						m.getEntryFileMode());
+					update(m);
 			} else if (f == null || !m.idEqual(i)) {
 				// The working tree file is missing or the merge content differs
 				// from index content
-				update(m.getEntryPathString(), m.getEntryObjectId(),
-						m.getEntryFileMode());
+				update(m);
 			} else if (i.getDirCacheEntry() != null) {
 				// The index contains a file (and not a folder)
 				if (f.isModified(i.getDirCacheEntry(), true,
@@ -400,8 +399,7 @@ void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i,
 						|| i.getDirCacheEntry().getStage() != 0)
 					// The working tree file is dirty or the index contains a
 					// conflict
-					update(m.getEntryPathString(), m.getEntryObjectId(),
-							m.getEntryFileMode());
+					update(m);
 				else {
 					// update the timestamp of the index with the one from the
 					// file if not set, as we are sure to be in sync here.
@@ -802,7 +800,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
 				if (f != null && isModifiedSubtree_IndexWorkingtree(name)) {
 					conflict(name, dce, h, m); // 1
 				} else {
-					update(name, mId, mMode); // 2
+					update(1, name, mId, mMode); // 2
 				}
 
 				break;
@@ -828,7 +826,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
 				// are found later
 				break;
 			case 0xD0F: // 19
-				update(name, mId, mMode);
+				update(1, name, mId, mMode);
 				break;
 			case 0xDF0: // conflict without a rule
 			case 0x0FD: // 15
@@ -839,7 +837,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
 					if (isModifiedSubtree_IndexWorkingtree(name))
 						conflict(name, dce, h, m); // 8
 					else
-						update(name, mId, mMode); // 7
+						update(1, name, mId, mMode); // 7
 				} else
 					conflict(name, dce, h, m); // 9
 				break;
@@ -859,7 +857,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
 				break;
 			case 0x0DF: // 16 17
 				if (!isModifiedSubtree_IndexWorkingtree(name))
-					update(name, mId, mMode);
+					update(1, name, mId, mMode);
 				else
 					conflict(name, dce, h, m);
 				break;
@@ -929,7 +927,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
 				// At least one of Head, Index, Merge is not empty
 				// -> only Merge contains something for this path. Use it!
 				// Potentially update the file
-				update(name, mId, mMode); // 1
+				update(1, name, mId, mMode); // 1
 			else if (m == null)
 				// Nothing in Merge
 				// Something in Head
@@ -947,7 +945,7 @@ else if (m == null)
 				// find in Merge. Potentially updates the file.
 				if (equalIdAndMode(hId, hMode, mId, mMode)) {
 					if (initialCheckout || force) {
-						update(name, mId, mMode);
+						update(1, name, mId, mMode);
 					} else {
 						keep(name, dce, f);
 					}
@@ -1131,7 +1129,7 @@ && isModified_IndexTree(name, iId, iMode, mId, mMode,
 
 						// TODO check that we don't overwrite some unsaved
 						// file content
-						update(name, mId, mMode);
+						update(1, name, mId, mMode);
 					} else if (dce != null
 							&& (f != null && f.isModified(dce, true,
 									this.walk.getObjectReader()))) {
@@ -1150,7 +1148,7 @@ && isModified_IndexTree(name, iId, iMode, mId, mMode,
 						// -> Standard case when switching between branches:
 						// Nothing new in index but something different in
 						// Merge. Update index and file
-						update(name, mId, mMode);
+						update(1, name, mId, mMode);
 					}
 				} else {
 					// Head differs from index or merge is same as index
@@ -1237,12 +1235,17 @@ private void remove(String path) {
 		removed.add(path);
 	}
 
-	private void update(String path, ObjectId mId, FileMode mode)
-			throws IOException {
+	private void update(CanonicalTreeParser tree) throws IOException {
+		update(0, tree.getEntryPathString(), tree.getEntryObjectId(),
+				tree.getEntryFileMode());
+	}
+
+	private void update(int index, String path, ObjectId mId,
+			FileMode mode) throws IOException {
 		if (!FileMode.TREE.equals(mode)) {
 			updated.put(path, new CheckoutMetadata(
-					walk.getEolStreamType(CHECKOUT_OP),
-					walk.getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE)));
+					walk.getCheckoutEolStreamType(index),
+					walk.getSmudgeCommand(index)));
 
 			DirCacheEntry entry = new DirCacheEntry(path, DirCacheEntry.STAGE_0);
 			entry.setObjectId(mId);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java
index 28ea927..df9fd47 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/IndexDiff.java
@@ -568,6 +568,9 @@ public boolean diff(ProgressMonitor monitor, int estWorkTreeSize,
 		if (ignoreSubmoduleMode != IgnoreSubmoduleMode.ALL) {
 			try (SubmoduleWalk smw = new SubmoduleWalk(repository)) {
 				smw.setTree(new DirCacheIterator(dirCache));
+				if (filter != null) {
+					smw.setFilter(filter);
+				}
 				smw.setBuilderFactory(factory);
 				while (smw.next()) {
 					IgnoreSubmoduleMode localIgnoreSubmoduleMode = ignoreSubmoduleMode;
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 @@ public enum MergeFailureReason {
 	private ContentMergeStrategy contentStrategy = ContentMergeStrategy.CONFLICT;
 
 	/**
-	 * Keeps {@link CheckoutMetadata} for {@link #checkout()} and
-	 * {@link #cleanUp()}.
+	 * Keeps {@link CheckoutMetadata} for {@link #checkout()}.
 	 */
 	private Map<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 @@ protected boolean mergeImpl() throws IOException {
 		}
 		if (!inCore) {
 			checkoutMetadata = new HashMap<>();
+			cleanupMetadata = new HashMap<>();
 		}
 		try {
 			return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1],
 					false);
 		} finally {
 			checkoutMetadata = null;
+			cleanupMetadata = null;
 			if (implicitDirCache) {
 				dircache.unlock();
 			}
@@ -447,7 +453,7 @@ protected void cleanUp() throws NoWorkTreeException,
 			DirCacheEntry entry = dc.getEntry(mpath);
 			if (entry != null) {
 				DirCacheCheckout.checkoutEntry(db, entry, reader, false,
-						checkoutMetadata.get(mpath));
+						cleanupMetadata.get(mpath));
 			}
 			mpathsIt.remove();
 		}
@@ -501,22 +507,26 @@ private DirCacheEntry keep(DirCacheEntry e) {
 	 * Remembers the {@link CheckoutMetadata} for the given path; it may be
 	 * needed in {@link #checkout()} or in {@link #cleanUp()}.
 	 *
+	 * @param map
+	 *            to add the metadata to
 	 * @param path
 	 *            of the current node
 	 * @param attributes
-	 *            for the current node
+	 *            to use for determining the metadata
 	 * @throws IOException
 	 *             if the smudge filter cannot be determined
-	 * @since 5.1
+	 * @since 6.1
 	 */
-	protected void addCheckoutMetadata(String path, Attributes attributes)
+	protected void addCheckoutMetadata(Map<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 @@ protected void addCheckoutMetadata(String path, Attributes attributes)
 	 * @param entry
 	 *            to add
 	 * @param attributes
-	 *            for the current entry
+	 *            the {@link Attributes} of the trees
 	 * @throws IOException
 	 *             if the {@link CheckoutMetadata} cannot be determined
-	 * @since 5.1
+	 * @since 6.1
 	 */
 	protected void addToCheckout(String path, DirCacheEntry entry,
-			Attributes attributes) throws IOException {
+			Attributes[] attributes)
+			throws IOException {
 		toBeCheckedOut.put(path, entry);
-		addCheckoutMetadata(path, attributes);
+		addCheckoutMetadata(cleanupMetadata, path, attributes[T_OURS]);
+		addCheckoutMetadata(checkoutMetadata, path, attributes[T_THEIRS]);
 	}
 
 	/**
@@ -549,7 +561,7 @@ protected void addToCheckout(String path, DirCacheEntry entry,
 	 * @param isFile
 	 *            whether it is a file
 	 * @param attributes
-	 *            for the entry
+	 *            to use for determining the {@link CheckoutMetadata}
 	 * @throws IOException
 	 *             if the {@link CheckoutMetadata} cannot be determined
 	 * @since 5.1
@@ -558,7 +570,7 @@ protected void addDeletion(String path, boolean isFile,
 			Attributes attributes) throws IOException {
 		toBeDeleted.add(path);
 		if (isFile) {
-			addCheckoutMetadata(path, attributes);
+			addCheckoutMetadata(cleanupMetadata, path, attributes);
 		}
 	}
 
@@ -599,7 +611,7 @@ protected void addDeletion(String path, boolean isFile,
 	 *            see
 	 *            {@link org.eclipse.jgit.merge.ResolveMerger#mergeTrees(AbstractTreeIterator, RevTree, RevTree, boolean)}
 	 * @param attributes
-	 *            the attributes defined for this entry
+	 *            the {@link Attributes} for the three trees
 	 * @return <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 @@ protected void addDeletion(String path, boolean isFile,
 	 * @throws org.eclipse.jgit.errors.IncorrectObjectTypeException
 	 * @throws org.eclipse.jgit.errors.CorruptObjectException
 	 * @throws java.io.IOException
-	 * @since 4.9
+	 * @since 6.1
 	 */
 	protected boolean processEntry(CanonicalTreeParser base,
 			CanonicalTreeParser ours, CanonicalTreeParser theirs,
 			DirCacheBuildIterator index, WorkingTreeIterator work,
-			boolean ignoreConflicts, Attributes attributes)
+			boolean ignoreConflicts, Attributes[] attributes)
 			throws MissingObjectException, IncorrectObjectTypeException,
 			CorruptObjectException, IOException {
 		enterSubtree = true;
@@ -729,7 +741,7 @@ protected boolean processEntry(CanonicalTreeParser base,
 				// Base, ours, and theirs all contain a folder: don't delete
 				return true;
 			}
-			addDeletion(tw.getPathString(), nonTree(modeO), attributes);
+			addDeletion(tw.getPathString(), nonTree(modeO), attributes[T_OURS]);
 			return true;
 		}
 
@@ -772,7 +784,7 @@ protected boolean processEntry(CanonicalTreeParser base,
 		if (nonTree(modeO) && nonTree(modeT)) {
 			// Check worktree before modifying files
 			boolean worktreeDirty = isWorktreeDirty(work, ourDce);
-			if (!attributes.canBeContentMerged() && worktreeDirty) {
+			if (!attributes[T_OURS].canBeContentMerged() && worktreeDirty) {
 				return false;
 			}
 
@@ -791,7 +803,7 @@ protected boolean processEntry(CanonicalTreeParser base,
 				mergeResults.put(tw.getPathString(), result);
 				unmergedPaths.add(tw.getPathString());
 				return true;
-			} else if (!attributes.canBeContentMerged()) {
+			} else if (!attributes[T_OURS].canBeContentMerged()) {
 				// File marked as binary
 				switch (getContentMergeStrategy()) {
 				case OURS:
@@ -842,13 +854,16 @@ protected boolean processEntry(CanonicalTreeParser base,
 			if (ignoreConflicts) {
 				result.setContainsConflicts(false);
 			}
-			updateIndex(base, ours, theirs, result, attributes);
+			updateIndex(base, ours, theirs, result, attributes[T_OURS]);
 			String currentPath = tw.getPathString();
 			if (result.containsConflicts() && !ignoreConflicts) {
 				unmergedPaths.add(currentPath);
 			}
 			modifiedFiles.add(currentPath);
-			addCheckoutMetadata(currentPath, attributes);
+			addCheckoutMetadata(cleanupMetadata, currentPath,
+					attributes[T_OURS]);
+			addCheckoutMetadata(checkoutMetadata, currentPath,
+					attributes[T_THEIRS]);
 		} else if (modeO != modeT) {
 			// OURS or THEIRS has been deleted
 			if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw
@@ -881,7 +896,8 @@ protected boolean processEntry(CanonicalTreeParser base,
 						// markers). But also stage 0 of the index is filled
 						// with that content.
 						result.setContainsConflicts(false);
-						updateIndex(base, ours, theirs, result, attributes);
+						updateIndex(base, ours, theirs, result,
+								attributes[T_OURS]);
 					} else {
 						add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH,
 								0);
@@ -896,11 +912,9 @@ protected boolean processEntry(CanonicalTreeParser base,
 							if (isWorktreeDirty(work, ourDce)) {
 								return false;
 							}
-							if (nonTree(modeT)) {
-								if (e != null) {
-									addToCheckout(tw.getPathString(), e,
-											attributes);
-								}
+							if (nonTree(modeT) && e != null) {
+								addToCheckout(tw.getPathString(), e,
+										attributes);
 							}
 						}
 
@@ -945,14 +959,16 @@ private static MergeResult<SubmoduleConflict> createGitLinksMergeResult(
 	 */
 	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 @@ protected boolean mergeTrees(AbstractTreeIterator baseTree,
 
 		tw = new NameConflictTreeWalk(db, reader);
 		tw.addTree(baseTree);
-		tw.addTree(headTree);
+		tw.setHead(tw.addTree(headTree));
 		tw.addTree(mergeTree);
 		int dciPos = tw.addTree(buildIt);
 		if (workingTreeIterator != null) {
@@ -1403,6 +1419,13 @@ protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts)
 		boolean hasAttributeNodeProvider = treeWalk
 				.getAttributesNodeProvider() != null;
 		while (treeWalk.next()) {
+			Attributes[] attributes = { NO_ATTRIBUTES, NO_ATTRIBUTES,
+					NO_ATTRIBUTES };
+			if (hasAttributeNodeProvider) {
+				attributes[T_BASE] = treeWalk.getAttributes(T_BASE);
+				attributes[T_OURS] = treeWalk.getAttributes(T_OURS);
+				attributes[T_THEIRS] = treeWalk.getAttributes(T_THEIRS);
+			}
 			if (!processEntry(
 					treeWalk.getTree(T_BASE, CanonicalTreeParser.class),
 					treeWalk.getTree(T_OURS, CanonicalTreeParser.class),
@@ -1410,9 +1433,7 @@ protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts)
 					treeWalk.getTree(T_INDEX, DirCacheBuildIterator.class),
 					hasWorkingTreeIterator ? treeWalk.getTree(T_FILE,
 							WorkingTreeIterator.class) : null,
-					ignoreConflicts, hasAttributeNodeProvider
-							? treeWalk.getAttributes()
-							: NO_ATTRIBUTES)) {
+					ignoreConflicts, attributes)) {
 				cleanUp();
 				return false;
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
index a244c55..942dad4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
@@ -18,11 +18,15 @@
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
+import org.eclipse.jgit.api.errors.AbortedByHookException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.NotSupportedException;
 import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.hooks.PrePushHook;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
@@ -58,6 +62,8 @@ class PushProcess {
 	/** A list of option strings associated with this push */
 	private List<String> pushOptions;
 
+	private final PrePushHook prePush;
+
 	/**
 	 * Create process for specified transport and refs updates specification.
 	 *
@@ -66,12 +72,14 @@ class PushProcess {
 	 *            connection.
 	 * @param toPush
 	 *            specification of refs updates (and local tracking branches).
-	 *
+	 * @param prePush
+	 *            {@link PrePushHook} to run after the remote advertisement has
+	 *            been gotten
 	 * @throws TransportException
 	 */
-	PushProcess(final Transport transport,
-			final Collection<RemoteRefUpdate> toPush) throws TransportException {
-		this(transport, toPush, null);
+	PushProcess(Transport transport, Collection<RemoteRefUpdate> toPush,
+			PrePushHook prePush) throws TransportException {
+		this(transport, toPush, prePush, null);
 	}
 
 	/**
@@ -82,16 +90,19 @@ class PushProcess {
 	 *            connection.
 	 * @param toPush
 	 *            specification of refs updates (and local tracking branches).
+	 * @param prePush
+	 *            {@link PrePushHook} to run after the remote advertisement has
+	 *            been gotten
 	 * @param out
 	 *            OutputStream to write messages to
 	 * @throws TransportException
 	 */
-	PushProcess(final Transport transport,
-			final Collection<RemoteRefUpdate> toPush, OutputStream out)
-			throws TransportException {
+	PushProcess(Transport transport, Collection<RemoteRefUpdate> toPush,
+			PrePushHook prePush, OutputStream out) throws TransportException {
 		this.walker = new RevWalk(transport.local);
 		this.transport = transport;
 		this.toPush = new LinkedHashMap<>();
+		this.prePush = prePush;
 		this.out = out;
 		this.pushOptions = transport.getPushOptions();
 		for (RemoteRefUpdate rru : toPush) {
@@ -129,10 +140,38 @@ PushResult execute(ProgressMonitor monitor)
 				res.setAdvertisedRefs(transport.getURI(), connection
 						.getRefsMap());
 				res.peerUserAgent = connection.getPeerUserAgent();
-				res.setRemoteUpdates(toPush);
 				monitor.endTask();
 
+				Map<String, RemoteRefUpdate> expanded = expandMatching();
+				toPush.clear();
+				toPush.putAll(expanded);
+
+				res.setRemoteUpdates(toPush);
 				final Map<String, RemoteRefUpdate> preprocessed = prepareRemoteUpdates();
+				List<RemoteRefUpdate> willBeAttempted = preprocessed.values()
+						.stream().filter(u -> {
+							switch (u.getStatus()) {
+							case NON_EXISTING:
+							case REJECTED_NODELETE:
+							case REJECTED_NONFASTFORWARD:
+							case REJECTED_OTHER_REASON:
+							case REJECTED_REMOTE_CHANGED:
+							case UP_TO_DATE:
+								return false;
+							default:
+								return true;
+							}
+						}).collect(Collectors.toList());
+				if (!willBeAttempted.isEmpty()) {
+					if (prePush != null) {
+						try {
+							prePush.setRefs(willBeAttempted);
+							prePush.call();
+						} catch (AbortedByHookException | IOException e) {
+							throw new TransportException(e.getMessage(), e);
+						}
+					}
+				}
 				if (transport.isDryRun())
 					modifyUpdatesForDryRun();
 				else if (!preprocessed.isEmpty())
@@ -201,25 +240,8 @@ private Map<String, RemoteRefUpdate> prepareRemoteUpdates()
 				continue;
 			}
 
-			// check for fast-forward:
-			// - both old and new ref must point to commits, AND
-			// - both of them must be known for us, exist in repository, AND
-			// - old commit must be ancestor of new commit
-			boolean fastForward = true;
-			try {
-				RevObject oldRev = walker.parseAny(advertisedOld);
-				final RevObject newRev = walker.parseAny(rru.getNewObjectId());
-				if (!(oldRev instanceof RevCommit)
-						|| !(newRev instanceof RevCommit)
-						|| !walker.isMergedInto((RevCommit) oldRev,
-								(RevCommit) newRev))
-					fastForward = false;
-			} catch (MissingObjectException x) {
-				fastForward = false;
-			} catch (Exception x) {
-				throw new TransportException(transport.getURI(), MessageFormat.format(
-						JGitText.get().readingObjectsFromLocalRepositoryFailed, x.getMessage()), x);
-			}
+			boolean fastForward = isFastForward(advertisedOld,
+					rru.getNewObjectId());
 			rru.setFastForward(fastForward);
 			if (!fastForward && !rru.isForceUpdate()) {
 				rru.setStatus(Status.REJECTED_NONFASTFORWARD);
@@ -233,6 +255,134 @@ private Map<String, RemoteRefUpdate> prepareRemoteUpdates()
 		return result;
 	}
 
+	/**
+	 * Determines whether an update from {@code oldOid} to {@code newOid} is a
+	 * fast-forward update:
+	 * <ul>
+	 * <li>both old and new must be commits, AND</li>
+	 * <li>both of them must be known to us and exist in the repository,
+	 * AND</li>
+	 * <li>the old commit must be an ancestor of the new commit.</li>
+	 * </ul>
+	 *
+	 * @param oldOid
+	 *            {@link ObjectId} of the old commit
+	 * @param newOid
+	 *            {@link ObjectId} of the new commit
+	 * @return {@code true} if the update fast-forwards, {@code false} otherwise
+	 * @throws TransportException
+	 */
+	private boolean isFastForward(ObjectId oldOid, ObjectId newOid)
+			throws TransportException {
+		try {
+			RevObject oldRev = walker.parseAny(oldOid);
+			RevObject newRev = walker.parseAny(newOid);
+			if (!(oldRev instanceof RevCommit) || !(newRev instanceof RevCommit)
+					|| !walker.isMergedInto((RevCommit) oldRev,
+							(RevCommit) newRev)) {
+				return false;
+			}
+		} catch (MissingObjectException x) {
+			return false;
+		} catch (Exception x) {
+			throw new TransportException(transport.getURI(),
+					MessageFormat.format(JGitText
+							.get().readingObjectsFromLocalRepositoryFailed,
+							x.getMessage()),
+					x);
+		}
+		return true;
+	}
+
+	/**
+	 * Expands all placeholder {@link RemoteRefUpdate}s for "matching"
+	 * {@link RefSpec}s ":" in {@link #toPush} and returns the resulting map in
+	 * which the placeholders have been replaced by their expansion.
+	 *
+	 * @return a new map of {@link RemoteRefUpdate}s keyed by remote name
+	 * @throws TransportException
+	 *             if the expansion results in duplicate updates
+	 */
+	private Map<String, RemoteRefUpdate> expandMatching()
+			throws TransportException {
+		Map<String, RemoteRefUpdate> result = new LinkedHashMap<>();
+		boolean hadMatch = false;
+		for (RemoteRefUpdate update : toPush.values()) {
+			if (update.isMatching()) {
+				if (hadMatch) {
+					throw new TransportException(MessageFormat.format(
+							JGitText.get().duplicateRemoteRefUpdateIsIllegal,
+							":")); //$NON-NLS-1$
+				}
+				expandMatching(result, update);
+				hadMatch = true;
+			} else if (result.put(update.getRemoteName(), update) != null) {
+				throw new TransportException(MessageFormat.format(
+						JGitText.get().duplicateRemoteRefUpdateIsIllegal,
+						update.getRemoteName()));
+			}
+		}
+		return result;
+	}
+
+	/**
+	 * Expands the placeholder {@link RemoteRefUpdate} {@code match} for a
+	 * "matching" {@link RefSpec} ":" or "+:" and puts the expansion into the
+	 * given map {@code updates}.
+	 *
+	 * @param updates
+	 *            map to put the expansion in
+	 * @param match
+	 *            the placeholder {@link RemoteRefUpdate} to expand
+	 *
+	 * @throws TransportException
+	 *             if the expansion results in duplicate updates, or the local
+	 *             branches cannot be determined
+	 */
+	private void expandMatching(Map<String, RemoteRefUpdate> updates,
+			RemoteRefUpdate match) throws TransportException {
+		try {
+			Map<String, Ref> advertisement = connection.getRefsMap();
+			Collection<RefSpec> fetchSpecs = match.getFetchSpecs();
+			boolean forceUpdate = match.isForceUpdate();
+			for (Ref local : transport.local.getRefDatabase()
+					.getRefsByPrefix(Constants.R_HEADS)) {
+				if (local.isSymbolic()) {
+					continue;
+				}
+				String name = local.getName();
+				Ref advertised = advertisement.get(name);
+				if (advertised == null || advertised.isSymbolic()) {
+					continue;
+				}
+				ObjectId oldOid = advertised.getObjectId();
+				if (oldOid == null || ObjectId.zeroId().equals(oldOid)) {
+					continue;
+				}
+				ObjectId newOid = local.getObjectId();
+				if (newOid == null || ObjectId.zeroId().equals(newOid)) {
+					continue;
+				}
+
+				RemoteRefUpdate rru = new RemoteRefUpdate(transport.local, name,
+						newOid, name, forceUpdate,
+						Transport.findTrackingRefName(name, fetchSpecs),
+						oldOid);
+				if (updates.put(rru.getRemoteName(), rru) != null) {
+					throw new TransportException(MessageFormat.format(
+							JGitText.get().duplicateRemoteRefUpdateIsIllegal,
+							rru.getRemoteName()));
+				}
+			}
+		} catch (IOException x) {
+			throw new TransportException(transport.getURI(),
+					MessageFormat.format(JGitText
+							.get().readingObjectsFromLocalRepositoryFailed,
+							x.getMessage()),
+					x);
+		}
+	}
+
 	private Map<String, RemoteRefUpdate> rejectAll() {
 		for (RemoteRefUpdate rru : toPush.values()) {
 			if (rru.getStatus() == Status.NOT_ATTEMPTED) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
index ac357af..56d0036 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
@@ -12,11 +12,11 @@
 
 import java.io.Serializable;
 import java.text.MessageFormat;
+import java.util.Objects;
 
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.util.References;
 
 /**
  * Describes how refs in one repository copy into another repository.
@@ -50,6 +50,9 @@ public static boolean isWildcard(String s) {
 	/** Is this specification actually a wildcard match? */
 	private boolean wildcard;
 
+	/** Is this the special ":" RefSpec? */
+	private boolean matching;
+
 	/**
 	 * How strict to be about wildcards.
 	 *
@@ -71,6 +74,7 @@ public enum WildcardMode {
 		 */
 		ALLOW_MISMATCH
 	}
+
 	/** Whether a wildcard is allowed on one side but not the other. */
 	private WildcardMode allowMismatchedWildcards;
 
@@ -87,6 +91,7 @@ public enum WildcardMode {
 	 * applications, as at least one field must be set to match a source name.
 	 */
 	public RefSpec() {
+		matching = false;
 		force = false;
 		wildcard = false;
 		srcName = Constants.HEAD;
@@ -133,17 +138,25 @@ public RefSpec(String spec, WildcardMode mode) {
 			s = s.substring(1);
 		}
 
+		boolean matchPushSpec = false;
 		final int c = s.lastIndexOf(':');
 		if (c == 0) {
 			s = s.substring(1);
-			if (isWildcard(s)) {
+			if (s.isEmpty()) {
+				matchPushSpec = true;
 				wildcard = true;
-				if (mode == WildcardMode.REQUIRE_MATCH) {
-					throw new IllegalArgumentException(MessageFormat
-							.format(JGitText.get().invalidWildcards, spec));
+				srcName = Constants.R_HEADS + '*';
+				dstName = srcName;
+			} else {
+				if (isWildcard(s)) {
+					wildcard = true;
+					if (mode == WildcardMode.REQUIRE_MATCH) {
+						throw new IllegalArgumentException(MessageFormat
+								.format(JGitText.get().invalidWildcards, spec));
+					}
 				}
+				dstName = checkValid(s);
 			}
-			dstName = checkValid(s);
 		} else if (c > 0) {
 			String src = s.substring(0, c);
 			String dst = s.substring(c + 1);
@@ -168,6 +181,7 @@ public RefSpec(String spec, WildcardMode mode) {
 			}
 			srcName = checkValid(s);
 		}
+		matching = matchPushSpec;
 	}
 
 	/**
@@ -195,6 +209,7 @@ public RefSpec(String spec) {
 	}
 
 	private RefSpec(RefSpec p) {
+		matching = false;
 		force = p.isForceUpdate();
 		wildcard = p.isWildcard();
 		srcName = p.getSource();
@@ -203,6 +218,17 @@ private RefSpec(RefSpec p) {
 	}
 
 	/**
+	 * Tells whether this {@link RefSpec} is the special "matching" RefSpec ":"
+	 * for pushing.
+	 *
+	 * @return whether this is a "matching" RefSpec
+	 * @since 6.1
+	 */
+	public boolean isMatching() {
+		return matching;
+	}
+
+	/**
 	 * Check if this specification wants to forcefully update the destination.
 	 *
 	 * @return true if this specification asks for updates without merge tests.
@@ -220,6 +246,7 @@ public boolean isForceUpdate() {
 	 */
 	public RefSpec setForceUpdate(boolean forceUpdate) {
 		final RefSpec r = new RefSpec(this);
+		r.matching = matching;
 		r.force = forceUpdate;
 		return r;
 	}
@@ -322,8 +349,7 @@ public RefSpec setDestination(String destination) {
 	 *             The wildcard status of the new source disagrees with the
 	 *             wildcard status of the new destination.
 	 */
-	public RefSpec setSourceDestination(final String source,
-			final String destination) {
+	public RefSpec setSourceDestination(String source, String destination) {
 		if (isWildcard(source) != isWildcard(destination))
 			throw new IllegalStateException(JGitText.get().sourceDestinationMustMatch);
 		final RefSpec r = new RefSpec(this);
@@ -541,37 +567,36 @@ public boolean equals(Object obj) {
 		if (!(obj instanceof RefSpec))
 			return false;
 		final RefSpec b = (RefSpec) obj;
-		if (isForceUpdate() != b.isForceUpdate())
+		if (isForceUpdate() != b.isForceUpdate()) {
 			return false;
-		if (isWildcard() != b.isWildcard())
-			return false;
-		if (!eq(getSource(), b.getSource()))
-			return false;
-		if (!eq(getDestination(), b.getDestination()))
-			return false;
-		return true;
-	}
-
-	private static boolean eq(String a, String b) {
-		if (References.isSameObject(a, b)) {
-			return true;
 		}
-		if (a == null || b == null)
+		if (isMatching()) {
+			return b.isMatching();
+		} else if (b.isMatching()) {
 			return false;
-		return a.equals(b);
+		}
+		return isWildcard() == b.isWildcard()
+				&& Objects.equals(getSource(), b.getSource())
+				&& Objects.equals(getDestination(), b.getDestination());
 	}
 
 	/** {@inheritDoc} */
 	@Override
 	public String toString() {
 		final StringBuilder r = new StringBuilder();
-		if (isForceUpdate())
+		if (isForceUpdate()) {
 			r.append('+');
-		if (getSource() != null)
-			r.append(getSource());
-		if (getDestination() != null) {
+		}
+		if (isMatching()) {
 			r.append(':');
-			r.append(getDestination());
+		} else {
+			if (getSource() != null) {
+				r.append(getSource());
+			}
+			if (getDestination() != null) {
+				r.append(':');
+				r.append(getDestination());
+			}
 		}
 		return r.toString();
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java
index 43eaac7..218e62c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteRefUpdate.java
@@ -12,7 +12,9 @@
 
 import java.io.IOException;
 import java.text.MessageFormat;
+import java.util.Collection;
 
+import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
@@ -116,6 +118,12 @@ public enum Status {
 	private RefUpdate localUpdate;
 
 	/**
+	 * If set, the RemoteRefUpdate is a placeholder for the "matching" RefSpec
+	 * to be expanded after the advertisements have been received in a push.
+	 */
+	private Collection<RefSpec> fetchSpecs;
+
+	/**
 	 * Construct remote ref update request by providing an update specification.
 	 * Object is created with default
 	 * {@link org.eclipse.jgit.transport.RemoteRefUpdate.Status#NOT_ATTEMPTED}
@@ -157,9 +165,8 @@ public enum Status {
 	 * @throws java.lang.IllegalArgumentException
 	 *             if some required parameter was null
 	 */
-	public RemoteRefUpdate(final Repository localDb, final String srcRef,
-			final String remoteName, final boolean forceUpdate,
-			final String localName, final ObjectId expectedOldObjectId)
+	public RemoteRefUpdate(Repository localDb, String srcRef, String remoteName,
+			boolean forceUpdate, String localName, ObjectId expectedOldObjectId)
 			throws IOException {
 		this(localDb, srcRef, srcRef != null ? localDb.resolve(srcRef)
 				: ObjectId.zeroId(), remoteName, forceUpdate, localName,
@@ -203,9 +210,8 @@ public RemoteRefUpdate(final Repository localDb, final String srcRef,
 	 * @throws java.lang.IllegalArgumentException
 	 *             if some required parameter was null
 	 */
-	public RemoteRefUpdate(final Repository localDb, final Ref srcRef,
-			final String remoteName, final boolean forceUpdate,
-			final String localName, final ObjectId expectedOldObjectId)
+	public RemoteRefUpdate(Repository localDb, Ref srcRef, String remoteName,
+			boolean forceUpdate, String localName, ObjectId expectedOldObjectId)
 			throws IOException {
 		this(localDb, srcRef != null ? srcRef.getName() : null,
 				srcRef != null ? srcRef.getObjectId() : null, remoteName,
@@ -255,28 +261,41 @@ public RemoteRefUpdate(final Repository localDb, final Ref srcRef,
 	 * @throws java.lang.IllegalArgumentException
 	 *             if some required parameter was null
 	 */
-	public RemoteRefUpdate(final Repository localDb, final String srcRef,
-			final ObjectId srcId, final String remoteName,
-			final boolean forceUpdate, final String localName,
-			final ObjectId expectedOldObjectId) throws IOException {
-		if (remoteName == null)
-			throw new IllegalArgumentException(JGitText.get().remoteNameCannotBeNull);
-		if (srcId == null && srcRef != null)
-			throw new IOException(MessageFormat.format(
-					JGitText.get().sourceRefDoesntResolveToAnyObject, srcRef));
+	public RemoteRefUpdate(Repository localDb, String srcRef, ObjectId srcId,
+			String remoteName, boolean forceUpdate, String localName,
+			ObjectId expectedOldObjectId) throws IOException {
+		this(localDb, srcRef, srcId, remoteName, forceUpdate, localName, null,
+				expectedOldObjectId);
+	}
 
-		if (srcRef != null)
+	private RemoteRefUpdate(Repository localDb, String srcRef, ObjectId srcId,
+			String remoteName, boolean forceUpdate, String localName,
+			Collection<RefSpec> fetchSpecs, ObjectId expectedOldObjectId)
+			throws IOException {
+		if (fetchSpecs == null) {
+			if (remoteName == null) {
+				throw new IllegalArgumentException(
+						JGitText.get().remoteNameCannotBeNull);
+			}
+			if (srcId == null && srcRef != null) {
+				throw new IOException(MessageFormat.format(
+						JGitText.get().sourceRefDoesntResolveToAnyObject,
+						srcRef));
+			}
+		}
+		if (srcRef != null) {
 			this.srcRef = srcRef;
-		else if (srcId != null && !srcId.equals(ObjectId.zeroId()))
+		} else if (srcId != null && !srcId.equals(ObjectId.zeroId())) {
 			this.srcRef = srcId.name();
-		else
+		} else {
 			this.srcRef = null;
-
-		if (srcId != null)
+		}
+		if (srcId != null) {
 			this.newObjectId = srcId;
-		else
+		} else {
 			this.newObjectId = ObjectId.zeroId();
-
+		}
+		this.fetchSpecs = fetchSpecs;
 		this.remoteName = remoteName;
 		this.forceUpdate = forceUpdate;
 		if (localName != null && localDb != null) {
@@ -292,8 +311,9 @@ else if (srcId != null && !srcId.equals(ObjectId.zeroId()))
 						? localUpdate.getOldObjectId()
 						: ObjectId.zeroId(),
 					newObjectId);
-		} else
+		} else {
 			trackingRefUpdate = null;
+		}
 		this.localDb = localDb;
 		this.expectedOldObjectId = expectedOldObjectId;
 		this.status = Status.NOT_ATTEMPTED;
@@ -316,11 +336,57 @@ else if (srcId != null && !srcId.equals(ObjectId.zeroId()))
 	 *             local tracking branch or srcRef of base object no longer can
 	 *             be resolved to any object.
 	 */
-	public RemoteRefUpdate(final RemoteRefUpdate base,
-			final ObjectId newExpectedOldObjectId) throws IOException {
-		this(base.localDb, base.srcRef, base.remoteName, base.forceUpdate,
+	public RemoteRefUpdate(RemoteRefUpdate base,
+			ObjectId newExpectedOldObjectId) throws IOException {
+		this(base.localDb, base.srcRef, base.newObjectId, base.remoteName,
+				base.forceUpdate,
 				(base.trackingRefUpdate == null ? null : base.trackingRefUpdate
-						.getLocalName()), newExpectedOldObjectId);
+						.getLocalName()),
+				base.fetchSpecs, newExpectedOldObjectId);
+	}
+
+	/**
+	 * Creates a "placeholder" update for the "matching" RefSpec ":".
+	 *
+	 * @param localDb
+	 *            local repository to push from
+	 * @param forceUpdate
+	 *            whether non-fast-forward updates shall be allowed
+	 * @param fetchSpecs
+	 *            The fetch {@link RefSpec}s to use when this placeholder is
+	 *            expanded to determine remote tracking branch updates
+	 */
+	RemoteRefUpdate(Repository localDb, boolean forceUpdate,
+			@NonNull Collection<RefSpec> fetchSpecs) {
+		this.localDb = localDb;
+		this.forceUpdate = forceUpdate;
+		this.fetchSpecs = fetchSpecs;
+		this.trackingRefUpdate = null;
+		this.srcRef = null;
+		this.remoteName = null;
+		this.newObjectId = null;
+		this.status = Status.NOT_ATTEMPTED;
+	}
+
+	/**
+	 * Tells whether this {@link RemoteRefUpdate} is a placeholder for a
+	 * "matching" {@link RefSpec}.
+	 *
+	 * @return {@code true} if this is a placeholder, {@code false} otherwise
+	 * @since 6.1
+	 */
+	public boolean isMatching() {
+		return fetchSpecs != null;
+	}
+
+	/**
+	 * Retrieves the fetch {@link RefSpec}s of this {@link RemoteRefUpdate}.
+	 *
+	 * @return the fetch {@link RefSpec}s, or {@code null} if
+	 *         {@code this.}{@link #isMatching()} {@code == false}
+	 */
+	Collection<RefSpec> getFetchSpecs() {
+		return fetchSpecs;
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
index bfe26d9..0eab443 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
@@ -40,7 +40,6 @@
 
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
-import org.eclipse.jgit.api.errors.AbortedByHookException;
 import org.eclipse.jgit.errors.NotSupportedException;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.hooks.Hooks;
@@ -590,6 +589,11 @@ public static Collection<RemoteRefUpdate> findRemoteRefUpdatesFor(
 		final Collection<RefSpec> procRefs = expandPushWildcardsFor(db, specs);
 
 		for (RefSpec spec : procRefs) {
+			if (spec.isMatching()) {
+				result.add(new RemoteRefUpdate(db, spec.isForceUpdate(),
+						fetchSpecs));
+				continue;
+			}
 			String srcSpec = spec.getSource();
 			final Ref srcRef = db.findRef(srcSpec);
 			if (srcRef != null)
@@ -660,7 +664,7 @@ private static Collection<RefSpec> expandPushWildcardsFor(
 
 		List<Ref> localRefs = null;
 		for (RefSpec spec : specs) {
-			if (spec.isWildcard()) {
+			if (!spec.isMatching() && spec.isWildcard()) {
 				if (localRefs == null) {
 					localRefs = db.getRefDatabase().getRefs();
 				}
@@ -676,7 +680,7 @@ private static Collection<RefSpec> expandPushWildcardsFor(
 		return procRefs;
 	}
 
-	private static String findTrackingRefName(final String remoteName,
+	static String findTrackingRefName(final String remoteName,
 			final Collection<RefSpec> fetchSpecs) {
 		// try to find matching tracking refs
 		for (RefSpec fetchSpec : fetchSpecs) {
@@ -1375,16 +1379,9 @@ public PushResult push(final ProgressMonitor monitor,
 			if (toPush.isEmpty())
 				throw new TransportException(JGitText.get().nothingToPush);
 		}
-		if (prePush != null) {
-			try {
-				prePush.setRefs(toPush);
-				prePush.call();
-			} catch (AbortedByHookException | IOException e) {
-				throw new TransportException(e.getMessage(), e);
-			}
-		}
 
-		final PushProcess pushProcess = new PushProcess(this, toPush, out);
+		final PushProcess pushProcess = new PushProcess(this, toPush, prePush,
+				out);
 		return pushProcess.execute(monitor);
 	}
 
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 @@ public enum OperationType {
 	}
 
 	/**
-	 *            Type of operation you want to retrieve the git attributes for.
+	 * Type of operation you want to retrieve the git attributes for.
 	 */
 	private OperationType operationType = OperationType.CHECKOUT_OP;
 
@@ -284,11 +286,20 @@ public static TreeWalk forPath(final Repository db, final String path,
 
 	AbstractTreeIterator currentHead;
 
-	/** Cached attribute for the current entry */
-	private Attributes attrs = null;
+	/**
+	 * Cached attributes for the current entry; per tree. Index i+1 is for tree
+	 * i; index 0 is for the deprecated legacy behavior.
+	 */
+	private Attributes[] attrs;
 
-	/** Cached attributes handler */
-	private AttributesHandler attributesHandler;
+	/**
+	 * Cached attributes handler; per tree. Index i+1 is for tree i; index 0 is
+	 * for the deprecated legacy behavior.
+	 */
+	private AttributesHandler[] attributesHandlers;
+
+	/** Can be set to identify the tree to use for {@link #getAttributes()}. */
+	private int headIndex = -1;
 
 	private Config config;
 
@@ -515,6 +526,24 @@ public AttributesNodeProvider getAttributesNodeProvider() {
 	}
 
 	/**
+	 * 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 @@ public AttributesNodeProvider getAttributesNodeProvider() {
 	 */
 	@Override
 	public Attributes getAttributes() {
-		if (attrs != null)
-			return attrs;
+		return getAttributes(headIndex);
+	}
 
+	/**
+	 * Retrieves the git attributes based on the given tree.
+	 *
+	 * @param index
+	 *            of the tree to use as base for the attributes
+	 * @return the attributes
+	 * @since 6.1
+	 */
+	public Attributes getAttributes(int index) {
+		int attrIndex = index + 1;
+		Attributes result = attrs[attrIndex];
+		if (result != null) {
+			return result;
+		}
 		if (attributesNodeProvider == null) {
-			// The work tree should have a AttributesNodeProvider to be able to
-			// retrieve the info and global attributes node
 			throw new IllegalStateException(
 					"The tree walk should have one AttributesNodeProvider set in order to compute the git attributes."); //$NON-NLS-1$
 		}
 
 		try {
-			// Lazy create the attributesHandler on the first access of
-			// attributes. This requires the info, global and root
-			// attributes nodes
-			if (attributesHandler == null) {
-				attributesHandler = new AttributesHandler(this);
+			AttributesHandler handler = attributesHandlers[attrIndex];
+			if (handler == null) {
+				if (index < 0) {
+					// Legacy behavior (headIndex not set, getAttributes() above
+					// called)
+					handler = new AttributesHandler(this, () -> {
+						return getTree(CanonicalTreeParser.class);
+					});
+				} else {
+					handler = new AttributesHandler(this, () -> {
+						AbstractTreeIterator tree = trees[index];
+						if (tree instanceof CanonicalTreeParser) {
+							return (CanonicalTreeParser) tree;
+						}
+						return null;
+					});
+				}
+				attributesHandlers[attrIndex] = handler;
 			}
-			attrs = attributesHandler.getAttributes();
-			return attrs;
+			result = handler.getAttributes();
+			attrs[attrIndex] = result;
+			return result;
 		} catch (IOException e) {
 			throw new JGitInternalException("Error while parsing attributes", //$NON-NLS-1$
 					e);
@@ -595,11 +650,34 @@ public Attributes getAttributes() {
 	 */
 	@Nullable
 	public EolStreamType getEolStreamType(OperationType opType) {
-		if (attributesNodeProvider == null || config == null)
+		if (attributesNodeProvider == null || config == null) {
 			return null;
-		return EolStreamTypeUtil.detectStreamType(
-				opType != null ? opType : operationType,
-					config.get(WorkingTreeOptions.KEY), getAttributes());
+		}
+		OperationType op = opType != null ? opType : operationType;
+		return EolStreamTypeUtil.detectStreamType(op,
+				config.get(WorkingTreeOptions.KEY), getAttributes());
+	}
+
+	/**
+	 * Get the EOL stream type of the current entry for checking out using the
+	 * config and {@link #getAttributes()}.
+	 *
+	 * @param tree
+	 *            index of the tree the check-out is to be from
+	 * @return the EOL stream type of the current entry using the config and
+	 *         {@link #getAttributes()}. Note that this method may return null
+	 *         if the {@link org.eclipse.jgit.treewalk.TreeWalk} is not based on
+	 *         a working tree
+	 * @since 6.1
+	 */
+	@Nullable
+	public EolStreamType getCheckoutEolStreamType(int tree) {
+		if (attributesNodeProvider == null || config == null) {
+			return null;
+		}
+		Attributes attr = getAttributes(tree);
+		return EolStreamTypeUtil.detectStreamType(OperationType.CHECKOUT_OP,
+				config.get(WorkingTreeOptions.KEY), attr);
 	}
 
 	/**
@@ -607,7 +685,8 @@ public EolStreamType getEolStreamType(OperationType opType) {
 	 */
 	public void reset() {
 		attrs = null;
-		attributesHandler = null;
+		attributesHandlers = null;
+		headIndex = -1;
 		trees = NO_TREES;
 		advance = false;
 		depth = 0;
@@ -651,7 +730,9 @@ public void reset(AnyObjectId id) throws MissingObjectException,
 
 		advance = false;
 		depth = 0;
-		attrs = null;
+		attrs = new Attributes[2];
+		attributesHandlers = new AttributesHandler[2];
+		headIndex = -1;
 	}
 
 	/**
@@ -701,7 +782,14 @@ public void reset(AnyObjectId... ids) throws MissingObjectException,
 		trees = r;
 		advance = false;
 		depth = 0;
-		attrs = null;
+		if (oldLen == newLen) {
+			Arrays.fill(attrs, null);
+			Arrays.fill(attributesHandlers, null);
+		} else {
+			attrs = new Attributes[newLen + 1];
+			attributesHandlers = new AttributesHandler[newLen + 1];
+		}
+		headIndex = -1;
 	}
 
 	/**
@@ -758,6 +846,16 @@ public int addTree(AbstractTreeIterator p) {
 		p.matchShift = 0;
 
 		trees = newTrees;
+		if (attrs == null) {
+			attrs = new Attributes[n + 2];
+		} else {
+			attrs = Arrays.copyOf(attrs, n + 2);
+		}
+		if (attributesHandlers == null) {
+			attributesHandlers = new AttributesHandler[n + 2];
+		} else {
+			attributesHandlers = Arrays.copyOf(attributesHandlers, n + 2);
+		}
 		return n;
 	}
 
@@ -800,7 +898,7 @@ public boolean next() throws MissingObjectException,
 			}
 
 			for (;;) {
-				attrs = null;
+				Arrays.fill(attrs, null);
 				final AbstractTreeIterator t = min();
 				if (t.eof()) {
 					if (depth > 0) {
@@ -1255,7 +1353,7 @@ public boolean isPostChildren() {
 	 */
 	public void enterSubtree() throws MissingObjectException,
 			IncorrectObjectTypeException, CorruptObjectException, IOException {
-		attrs = null;
+		Arrays.fill(attrs, null);
 		final AbstractTreeIterator ch = currentHead;
 		final AbstractTreeIterator[] tmp = new AbstractTreeIterator[trees.length];
 		for (int i = 0; i < trees.length; i++) {
@@ -1374,11 +1472,12 @@ public <T extends AbstractTreeIterator> T getTree(Class<T> type) {
 
 	/**
 	 * Inspect config and attributes to return a filtercommand applicable for
-	 * the current path, but without expanding %f occurences
+	 * the current path.
 	 *
 	 * @param filterCommandType
 	 *            which type of filterCommand should be executed. E.g. "clean",
-	 *            "smudge"
+	 *            "smudge". For "smudge" consider using
+	 *            {{@link #getSmudgeCommand(int)} instead.
 	 * @return a filter command
 	 * @throws java.io.IOException
 	 * @since 4.2
@@ -1407,6 +1506,54 @@ public String getFilterCommand(String filterCommandType)
 	}
 
 	/**
+	 * 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
diff --git a/pom.xml b/pom.xml
index f24b676..c6fa636 100644
--- a/pom.xml
+++ b/pom.xml
@@ -203,6 +203,14 @@
       <id>repo.eclipse.org.cbi-snapshots</id>
       <url>https://repo.eclipse.org/content/repositories/cbi-snapshots/</url>
     </pluginRepository>
+    <pluginRepository>
+      <id>repo.eclipse.org.dash-releases</id>
+      <url>https://repo.eclipse.org/content/repositories/dash-licenses-releases/</url>
+    </pluginRepository>
+    <pluginRepository>
+      <id>repo.eclipse.org.dash-snapshots</id>
+      <url>https://repo.eclipse.org/content/repositories/dash-licenses-snapshots/</url>
+    </pluginRepository>
   </pluginRepositories>
 
   <build>
@@ -382,6 +390,11 @@
           <artifactId>spring-boot-maven-plugin</artifactId>
           <version>2.5.4</version>
         </plugin>
+        <plugin>
+          <groupId>org.eclipse.dash</groupId>
+          <artifactId>license-tool-plugin</artifactId>
+          <version>0.0.1-SNAPSHOT</version>
+        </plugin>
       </plugins>
     </pluginManagement>
 
@@ -540,6 +553,10 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-report-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>org.eclipse.dash</groupId>
+        <artifactId>license-tool-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>