Merge changes from topic 'sshd'

* changes:
  sshd: add missing javadoc in SshTestGitServer
  sshd: shared reference in JGitClientSession must be volatile
  sshd: correct the protocol version exchange
diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitFilter.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitFilter.java
index 51de8ab..1c5e7ec 100644
--- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitFilter.java
+++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/GitFilter.java
@@ -248,13 +248,13 @@
 					.through(enabled)//
 					.with(new TextFileServlet(Constants.HEAD));
 
-			final String info_alternates = "objects/info/alternates";
+			final String info_alternates = Constants.OBJECTS + "/" + Constants.INFO_ALTERNATES;
 			serve("*/" + info_alternates)//
 					.through(mustBeLocal)//
 					.through(enabled)//
 					.with(new TextFileServlet(info_alternates));
 
-			final String http_alternates = "objects/info/http-alternates";
+			final String http_alternates = Constants.OBJECTS + "/" + Constants.INFO_HTTP_ALTERNATES;
 			serve("*/" + http_alternates)//
 					.through(mustBeLocal)//
 					.through(enabled)//
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/Lfs.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/Lfs.java
index 4c46bbd..8dba09c 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/Lfs.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/Lfs.java
@@ -42,6 +42,8 @@
  */
 package org.eclipse.jgit.lfs;
 
+import static org.eclipse.jgit.lib.Constants.OBJECTS;
+
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -104,7 +106,7 @@
 	 */
 	public Path getLfsObjDir() {
 		if (objDir == null) {
-			objDir = root.resolve("objects"); //$NON-NLS-1$
+			objDir = root.resolve(OBJECTS);
 		}
 		return objDir;
 	}
diff --git a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java
index a830ff2..befb7f6 100644
--- a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java
+++ b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java
@@ -211,6 +211,14 @@
 				.replaceAll("\t", "\\\\t");
 	}
 
+	protected String shellQuote(String s) {
+		return "'" + s.replace("'", "'\\''") + "'";
+	}
+
+	protected String shellQuote(File f) {
+		return "'" + f.getPath().replace("'", "'\\''") + "'";
+	}
+
 	protected void assertStringArrayEquals(String expected, String[] actual) {
 		// if there is more than one line, ignore last one if empty
 		assertEquals(1,
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java
index e07fdd5..03aa469 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java
@@ -139,10 +139,6 @@
 				listTarEntries(result));
 	}
 
-	private static String shellQuote(String s) {
-		return "'" + s.replace("'", "'\\''") + "'";
-	}
-
 	@Test
 	public void testFormatOverridesFilename() throws Exception {
 		File archive = new File(db.getWorkTree(), "format-overrides-name.tar");
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CloneTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CloneTest.java
index 2c0abd7..c31f28c 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CloneTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CloneTest.java
@@ -78,9 +78,9 @@
 		File gitDir = db.getDirectory();
 		String sourceURI = gitDir.toURI().toString();
 		File target = createTempDirectory("target");
-		StringBuilder cmd = new StringBuilder("git clone ").append(sourceURI
-				+ " " + target.getPath());
-		String[] result = execute(cmd.toString());
+		String cmd = "git clone " + sourceURI + " "
+				+ shellQuote(target.getPath());
+		String[] result = execute(cmd);
 		assertArrayEquals(new String[] {
 				"Cloning into '" + target.getPath() + "'...",
 						"", "" }, result);
@@ -101,9 +101,9 @@
 		File gitDir = db.getDirectory();
 		String sourceURI = gitDir.toURI().toString();
 		File target = createTempDirectory("target");
-		StringBuilder cmd = new StringBuilder("git clone ").append(sourceURI
-				+ " " + target.getPath());
-		String[] result = execute(cmd.toString());
+		String cmd = "git clone " + sourceURI + " "
+				+ shellQuote(target.getPath());
+		String[] result = execute(cmd);
 		assertArrayEquals(new String[] {
 				"Cloning into '" + target.getPath() + "'...",
 				"warning: You appear to have cloned an empty repository.", "",
@@ -125,8 +125,8 @@
 		File gitDir = db.getDirectory();
 		String sourceURI = gitDir.toURI().toString();
 		String name = new URIish(sourceURI).getHumanishName();
-		StringBuilder cmd = new StringBuilder("git clone ").append(sourceURI);
-		String[] result = execute(cmd.toString());
+		String cmd = "git clone " + sourceURI;
+		String[] result = execute(cmd);
 		assertArrayEquals(new String[] {
 				"Cloning into '" + new File(target, name).getName() + "'...",
 				"", "" }, result);
@@ -143,10 +143,10 @@
 		String sourcePath = gitDir.getAbsolutePath();
 		String targetPath = (new File(sourcePath)).getParentFile()
 				.getParentFile().getAbsolutePath()
-				+ "/target.git";
-		StringBuilder cmd = new StringBuilder("git clone --bare ")
-				.append(sourcePath + " " + targetPath);
-		String[] result = execute(cmd.toString());
+				+ File.separator + "target.git";
+		String cmd = "git clone --bare " + shellQuote(sourcePath) + " "
+				+ shellQuote(targetPath);
+		String[] result = execute(cmd);
 		assertArrayEquals(new String[] {
 				"Cloning into '" + targetPath + "'...", "", "" }, result);
 		Git git2 = Git.open(new File(targetPath));
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/LsRemoteTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/LsRemoteTest.java
index ba6b771..fd3c383 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/LsRemoteTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/LsRemoteTest.java
@@ -80,7 +80,7 @@
 	@Test
 	public void testLsRemote() throws Exception {
 		final List<String> result = CLIGitCommand.execute(
-				"git ls-remote " + db.getDirectory(), db);
+				"git ls-remote " + shellQuote(db.getDirectory()), db);
 		assertArrayEquals(new String[] {
 				"d0b1ef2b3dea02bb2ca824445c04e6def012c32c	HEAD",
 				"d0b1ef2b3dea02bb2ca824445c04e6def012c32c	refs/heads/master",
@@ -98,7 +98,8 @@
 	public void testLsRemoteHeads() throws Exception {
 		final List<String> result = CLIGitCommand.execute(
 				"git ls-remote --heads "
-				+ db.getDirectory(), db);
+						+ shellQuote(db.getDirectory()),
+				db);
 		assertArrayEquals(new String[] {
 				"d0b1ef2b3dea02bb2ca824445c04e6def012c32c	refs/heads/master",
 				"d0b1ef2b3dea02bb2ca824445c04e6def012c32c	refs/heads/test",
@@ -108,7 +109,7 @@
 	@Test
 	public void testLsRemoteTags() throws Exception {
 		final List<String> result = CLIGitCommand.execute(
-				"git ls-remote --tags " + db.getDirectory(), db);
+				"git ls-remote --tags " + shellQuote(db.getDirectory()), db);
 		assertArrayEquals(new String[] {
 				"efc02078d83a5226986ae917323acec7e1e8b7cb	refs/tags/tag1",
 				"d0b1ef2b3dea02bb2ca824445c04e6def012c32c	refs/tags/tag1^{}",
@@ -122,7 +123,8 @@
 	@Test
 	public void testLsRemoteHeadsTags() throws Exception {
 		final List<String> result = CLIGitCommand.execute(
-				"git ls-remote --heads --tags " + db.getDirectory(), db);
+				"git ls-remote --heads --tags " + shellQuote(db.getDirectory()),
+				db);
 		assertArrayEquals(new String[] {
 				"d0b1ef2b3dea02bb2ca824445c04e6def012c32c	refs/heads/master",
 				"d0b1ef2b3dea02bb2ca824445c04e6def012c32c	refs/heads/test",
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AlternatesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AlternatesTest.java
index b09db03..2306e0b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AlternatesTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AlternatesTest.java
@@ -42,6 +42,7 @@
  */
 package org.eclipse.jgit.internal.storage.file;
 
+import static org.eclipse.jgit.lib.Constants.INFO_ALTERNATES;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
@@ -76,7 +77,7 @@
 	private void setAlternate(FileRepository from, FileRepository to)
 			throws IOException {
 		File alt = new File(from.getObjectDatabase().getDirectory(),
-				"info/alternates");
+				INFO_ALTERNATES);
 		alt.getParentFile().mkdirs();
 		File fromDir = from.getObjectDatabase().getDirectory();
 		File toDir = to.getObjectDatabase().getDirectory();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
index 5d0a7e2..1e2341b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
@@ -44,6 +44,7 @@
 package org.eclipse.jgit.internal.storage.file;
 
 import static org.eclipse.jgit.internal.storage.pack.PackWriter.NONE;
+import static org.eclipse.jgit.lib.Constants.INFO_ALTERNATES;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -138,7 +139,7 @@
 		config = new PackConfig(db);
 
 		dst = createBareRepository();
-		File alt = new File(dst.getObjectDatabase().getDirectory(), "info/alternates");
+		File alt = new File(dst.getObjectDatabase().getDirectory(), INFO_ALTERNATES);
 		alt.getParentFile().mkdirs();
 		write(alt, db.getObjectDatabase().getDirectory().getAbsolutePath() + "\n");
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
index c613849..825d15b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
@@ -96,7 +96,7 @@
 	public void test001_Initalize() {
 		final File gitdir = new File(trash, Constants.DOT_GIT);
 		final File hooks = new File(gitdir, "hooks");
-		final File objects = new File(gitdir, "objects");
+		final File objects = new File(gitdir, Constants.OBJECTS);
 		final File objects_pack = new File(objects, "pack");
 		final File objects_info = new File(objects, "info");
 		final File refs = new File(gitdir, "refs");
@@ -148,7 +148,7 @@
 		assertEqualsPath(theDir, r.getDirectory());
 		assertEqualsPath(repo1Parent, r.getWorkTree());
 		assertEqualsPath(new File(theDir, "index"), r.getIndexFile());
-		assertEqualsPath(new File(theDir, "objects"), r.getObjectDatabase()
+		assertEqualsPath(new File(theDir, Constants.OBJECTS), r.getObjectDatabase()
 				.getDirectory());
 	}
 
@@ -174,7 +174,7 @@
 		assertEqualsPath(theDir, r.getDirectory());
 		assertEqualsPath(repo1Parent.getParentFile(), r.getWorkTree());
 		assertEqualsPath(new File(theDir, "index"), r.getIndexFile());
-		assertEqualsPath(new File(theDir, "objects"), r.getObjectDatabase()
+		assertEqualsPath(new File(theDir, Constants.OBJECTS), r.getObjectDatabase()
 				.getDirectory());
 	}
 
@@ -198,7 +198,7 @@
 		assertEqualsPath(theDir, r.getDirectory());
 		assertEqualsPath(repo1Parent, r.getWorkTree());
 		assertEqualsPath(new File(theDir, "index"), r.getIndexFile());
-		assertEqualsPath(new File(theDir, "objects"), r.getObjectDatabase()
+		assertEqualsPath(new File(theDir, Constants.OBJECTS), r.getObjectDatabase()
 				.getDirectory());
 	}
 
@@ -227,7 +227,7 @@
 		assertEqualsPath(theDir, r.getDirectory());
 		assertEqualsPath(workdir, r.getWorkTree());
 		assertEqualsPath(new File(theDir, "index"), r.getIndexFile());
-		assertEqualsPath(new File(theDir, "objects"), r.getObjectDatabase()
+		assertEqualsPath(new File(theDir, Constants.OBJECTS), r.getObjectDatabase()
 				.getDirectory());
 	}
 
@@ -256,7 +256,7 @@
 		assertEqualsPath(theDir, r.getDirectory());
 		assertEqualsPath(workdir, r.getWorkTree());
 		assertEqualsPath(new File(theDir, "index"), r.getIndexFile());
-		assertEqualsPath(new File(theDir, "objects"), r.getObjectDatabase()
+		assertEqualsPath(new File(theDir, Constants.OBJECTS), r.getObjectDatabase()
 				.getDirectory());
 	}
 
@@ -312,7 +312,7 @@
 		}
 
 		final File o = new File(new File(new File(newdb.getDirectory(),
-				"objects"), "4b"), "825dc642cb6eb9a060e54bf8d69288fbee4904");
+				Constants.OBJECTS), "4b"), "825dc642cb6eb9a060e54bf8d69288fbee4904");
 		assertTrue("Exists " + o, o.isFile());
 		assertTrue("Read-only " + o, !o.canWrite());
 	}
@@ -324,7 +324,7 @@
 		final ObjectId treeId = insertTree(new TreeFormatter());
 		assertEquals("4b825dc642cb6eb9a060e54bf8d69288fbee4904", treeId.name());
 		final File o = new File(new File(
-				new File(db.getDirectory(), "objects"), "4b"),
+				new File(db.getDirectory(), Constants.OBJECTS), "4b"),
 				"825dc642cb6eb9a060e54bf8d69288fbee4904");
 		assertFalse("Exists " + o, o.isFile());
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PostUploadHookChainTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PostUploadHookChainTest.java
new file mode 100644
index 0000000..ea7e8ed
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PostUploadHookChainTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2019, Google LLC.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.transport;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+
+import org.eclipse.jgit.storage.pack.PackStatistics;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class PostUploadHookChainTest {
+
+	@Test
+	public void testDefaultIfEmpty() {
+		PostUploadHook[] noHooks = {};
+		PostUploadHook newChain = PostUploadHookChain
+				.newChain(Arrays.asList(noHooks));
+		assertEquals(newChain, PostUploadHook.NULL);
+	}
+
+	@Test
+	public void testFlattenChainIfOnlyOne() {
+		FakePostUploadHook hook1 = new FakePostUploadHook();
+		PostUploadHook newChain = PostUploadHookChain
+				.newChain(Arrays.asList(PostUploadHook.NULL, hook1));
+		assertEquals(newChain, hook1);
+	}
+
+	@Test
+	public void testMultipleHooks() {
+		FakePostUploadHook hook1 = new FakePostUploadHook();
+		FakePostUploadHook hook2 = new FakePostUploadHook();
+
+		PostUploadHook chained = PostUploadHookChain
+				.newChain(Arrays.asList(hook1, hook2));
+		chained.onPostUpload(null);
+
+		assertTrue(hook1.wasInvoked());
+		assertTrue(hook2.wasInvoked());
+	}
+
+	private static final class FakePostUploadHook implements PostUploadHook {
+		boolean invoked;
+
+		public boolean wasInvoked() {
+			return invoked;
+		}
+
+		@Override
+		public void onPostUpload(PackStatistics stats) {
+			invoked = true;
+		}
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PreUploadHookChainTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PreUploadHookChainTest.java
new file mode 100644
index 0000000..2a36d8a
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PreUploadHookChainTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2019, Google LLC.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.transport;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class PreUploadHookChainTest {
+
+	@Test
+	public void testDefaultIfEmpty() {
+		PreUploadHook[] noHooks = {};
+		PreUploadHook newChain = PreUploadHookChain
+				.newChain(Arrays.asList(noHooks));
+		assertEquals(newChain, PreUploadHook.NULL);
+	}
+
+	@Test
+	public void testFlattenChainIfOnlyOne() {
+		FakePreUploadHook hook1 = new FakePreUploadHook();
+		PreUploadHook newChain = PreUploadHookChain
+				.newChain(Arrays.asList(PreUploadHook.NULL, hook1));
+		assertEquals(newChain, hook1);
+	}
+
+	@Test
+	public void testMultipleHooks() throws ServiceMayNotContinueException {
+		FakePreUploadHook hook1 = new FakePreUploadHook();
+		FakePreUploadHook hook2 = new FakePreUploadHook();
+
+		PreUploadHook chained = PreUploadHookChain
+				.newChain(Arrays.asList(hook1, hook2));
+		chained.onBeginNegotiateRound(null, null, 0);
+
+		assertTrue(hook1.wasInvoked());
+		assertTrue(hook2.wasInvoked());
+	}
+
+	private static final class FakePreUploadHook implements PreUploadHook {
+		boolean invoked;
+
+		@Override
+		public void onBeginNegotiateRound(UploadPack up,
+				Collection<? extends ObjectId> wants, int cntOffered)
+				throws ServiceMayNotContinueException {
+			invoked = true;
+		}
+
+		@Override
+		public void onEndNegotiateRound(UploadPack up,
+				Collection<? extends ObjectId> wants, int cntCommon,
+				int cntNotFound, boolean ready)
+				throws ServiceMayNotContinueException {
+			throw new UnsupportedOperationException();
+		}
+
+		@Override
+		public void onSendPack(UploadPack up,
+				Collection<? extends ObjectId> wants,
+				Collection<? extends ObjectId> haves)
+				throws ServiceMayNotContinueException {
+			throw new UnsupportedOperationException();
+		}
+
+		public boolean wasInvoked() {
+			return invoked;
+		}
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ProtocolV2HookChainTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ProtocolV2HookChainTest.java
new file mode 100644
index 0000000..8fb1ca8
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ProtocolV2HookChainTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2019, Google LLC.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.transport;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ProtocolV2HookChainTest {
+
+	@Test
+	public void testDefaultIfEmpty() {
+		ProtocolV2Hook[] noHooks = {};
+		ProtocolV2Hook newChain = ProtocolV2HookChain
+				.newChain(Arrays.asList(noHooks));
+		assertEquals(newChain, ProtocolV2Hook.DEFAULT);
+	}
+
+	@Test
+	public void testFlattenChainIfOnlyOne() {
+		FakeProtocolV2Hook hook1 = new FakeProtocolV2Hook();
+		ProtocolV2Hook newChain = ProtocolV2HookChain
+				.newChain(Arrays.asList(ProtocolV2Hook.DEFAULT, hook1));
+		assertEquals(newChain, hook1);
+	}
+
+	@Test
+	public void testMultipleHooks() throws ServiceMayNotContinueException {
+		FakeProtocolV2Hook hook1 = new FakeProtocolV2Hook();
+		FakeProtocolV2Hook hook2 = new FakeProtocolV2Hook();
+
+		ProtocolV2Hook chained = ProtocolV2HookChain
+				.newChain(Arrays.asList(hook1, hook2));
+		chained.onLsRefs(LsRefsV2Request.builder().build());
+
+		assertTrue(hook1.wasInvoked());
+		assertTrue(hook2.wasInvoked());
+	}
+
+	private static final class FakeProtocolV2Hook implements ProtocolV2Hook {
+		boolean invoked;
+
+		@Override
+		public void onLsRefs(LsRefsV2Request req)
+				throws ServiceMayNotContinueException {
+			invoked = true;
+		}
+
+		@Override
+		public void onCapabilities(CapabilitiesV2Request req)
+				throws ServiceMayNotContinueException {
+			throw new UnsupportedOperationException();
+		}
+
+		@Override
+		public void onFetch(FetchV2Request req)
+				throws ServiceMayNotContinueException {
+			throw new UnsupportedOperationException();
+		}
+
+		public boolean wasInvoked() {
+			return invoked;
+		}
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java
index 0ef29d4..22c67c1 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java
@@ -1,6 +1,7 @@
 package org.eclipse.jgit.transport;
 
 import static org.eclipse.jgit.lib.MoreAsserts.assertThrows;
+import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.hasItems;
 import static org.hamcrest.Matchers.is;
@@ -23,6 +24,8 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
 
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
@@ -423,22 +426,18 @@
 	 * Invokes UploadPack with protocol v2 and sends it the given lines,
 	 * and returns UploadPack's output stream.
 	 */
-	private ByteArrayInputStream uploadPackV2Setup(RequestPolicy requestPolicy,
-			RefFilter refFilter, ProtocolV2Hook hook, String... inputLines)
+	private ByteArrayInputStream uploadPackV2Setup(
+			Consumer<UploadPack> postConstructionSetup, String... inputLines)
 			throws Exception {
 
 		ByteArrayInputStream send = linesAsInputStream(inputLines);
 
 		server.getConfig().setString("protocol", null, "version", "2");
 		UploadPack up = new UploadPack(server);
-		if (requestPolicy != null)
-			up.setRequestPolicy(requestPolicy);
-		if (refFilter != null)
-			up.setRefFilter(refFilter);
-		up.setExtraParameters(Sets.of("version=2"));
-		if (hook != null) {
-			up.setProtocolV2Hook(hook);
+		if (postConstructionSetup != null) {
+			postConstructionSetup.accept(up);
 		}
+		up.setExtraParameters(Sets.of("version=2"));
 
 		ByteArrayOutputStream recv = new ByteArrayOutputStream();
 		up.upload(send, recv, null);
@@ -452,6 +451,7 @@
 		try (ByteArrayOutputStream send = new ByteArrayOutputStream()) {
 			PacketLineOut pckOut = new PacketLineOut(send);
 			for (String line : inputLines) {
+				Objects.requireNonNull(line);
 				if (PacketLineIn.isEnd(line)) {
 					pckOut.end();
 				} else if (PacketLineIn.isDelimiter(line)) {
@@ -469,11 +469,12 @@
 	 * Returns UploadPack's output stream, not including the capability
 	 * advertisement by the server.
 	 */
-	private ByteArrayInputStream uploadPackV2(RequestPolicy requestPolicy,
-			RefFilter refFilter, ProtocolV2Hook hook, String... inputLines)
+	private ByteArrayInputStream uploadPackV2(
+			Consumer<UploadPack> postConstructionSetup,
+			String... inputLines)
 			throws Exception {
 		ByteArrayInputStream recvStream =
-				uploadPackV2Setup(requestPolicy, refFilter, hook, inputLines);
+				uploadPackV2Setup(postConstructionSetup, inputLines);
 		PacketLineIn pckIn = new PacketLineIn(recvStream);
 
 		// drain capabilities
@@ -484,7 +485,7 @@
 	}
 
 	private ByteArrayInputStream uploadPackV2(String... inputLines) throws Exception {
-		return uploadPackV2(null, null, null, inputLines);
+		return uploadPackV2(null, inputLines);
 	}
 
 	private static class TestV2Hook implements ProtocolV2Hook {
@@ -513,8 +514,9 @@
 	@Test
 	public void testV2Capabilities() throws Exception {
 		TestV2Hook hook = new TestV2Hook();
-		ByteArrayInputStream recvStream =
-				uploadPackV2Setup(null, null, hook, PacketLineIn.end());
+		ByteArrayInputStream recvStream = uploadPackV2Setup(
+				(UploadPack up) -> {up.setProtocolV2Hook(hook);},
+				PacketLineIn.end());
 		PacketLineIn pckIn = new PacketLineIn(recvStream);
 		assertThat(hook.capabilitiesRequest, notNullValue());
 		assertThat(pckIn.readString(), is("version 2"));
@@ -531,54 +533,72 @@
 		assertTrue(PacketLineIn.isEnd(pckIn.readString()));
 	}
 
-	@Test
-	public void testV2CapabilitiesAllowFilter() throws Exception {
-		server.getConfig().setBoolean("uploadpack", null, "allowfilter", true);
+	private void checkAdvertisedIfAllowed(String configSection, String configName,
+			String fetchCapability) throws Exception {
+		server.getConfig().setBoolean(configSection, null, configName, true);
 		ByteArrayInputStream recvStream =
-				uploadPackV2Setup(null, null, null, PacketLineIn.end());
+				uploadPackV2Setup(null, PacketLineIn.end());
 		PacketLineIn pckIn = new PacketLineIn(recvStream);
 
 		assertThat(pckIn.readString(), is("version 2"));
-		assertThat(
-				Arrays.asList(pckIn.readString(), pckIn.readString(),
-						pckIn.readString()),
-				// TODO(jonathantanmy) This check overspecifies the
-				// order of the capabilities of "fetch".
-				hasItems("ls-refs", "fetch=filter shallow", "server-option"));
-		assertTrue(PacketLineIn.isEnd(pckIn.readString()));
+
+		ArrayList<String> lines = new ArrayList<>();
+		String line;
+		while (!PacketLineIn.isEnd((line = pckIn.readString()))) {
+			if (line.startsWith("fetch=")) {
+				assertThat(
+					Arrays.asList(line.substring(6).split(" ")),
+					containsInAnyOrder(fetchCapability, "shallow"));
+				lines.add("fetch");
+			} else {
+				lines.add(line);
+			}
+		}
+		assertThat(lines, containsInAnyOrder("ls-refs", "fetch", "server-option"));
+	}
+
+	private void checkUnadvertisedIfUnallowed(String fetchCapability) throws Exception {
+		ByteArrayInputStream recvStream =
+				uploadPackV2Setup(null, PacketLineIn.end());
+		PacketLineIn pckIn = new PacketLineIn(recvStream);
+
+		assertThat(pckIn.readString(), is("version 2"));
+
+		ArrayList<String> lines = new ArrayList<>();
+		String line;
+		while (!PacketLineIn.isEnd((line = pckIn.readString()))) {
+			if (line.startsWith("fetch=")) {
+				assertThat(
+					Arrays.asList(line.substring(6).split(" ")),
+					hasItems("shallow"));
+				lines.add("fetch");
+			} else {
+				lines.add(line);
+			}
+		}
+		assertThat(lines, hasItems("ls-refs", "fetch", "server-option"));
+	}
+
+	@Test
+	public void testV2CapabilitiesAllowFilter() throws Exception {
+		checkAdvertisedIfAllowed("uploadpack", "allowfilter", "filter");
+		checkUnadvertisedIfUnallowed("filter");
 	}
 
 	@Test
 	public void testV2CapabilitiesRefInWant() throws Exception {
-		server.getConfig().setBoolean("uploadpack", null, "allowrefinwant", true);
-		ByteArrayInputStream recvStream =
-				uploadPackV2Setup(null, null, null, PacketLineIn.end());
-		PacketLineIn pckIn = new PacketLineIn(recvStream);
-
-		assertThat(pckIn.readString(), is("version 2"));
-		assertThat(
-				Arrays.asList(pckIn.readString(), pckIn.readString(),
-						pckIn.readString()),
-				// TODO(jonathantanmy) This check overspecifies the
-				// order of the capabilities of "fetch".
-				hasItems("ls-refs", "fetch=ref-in-want shallow",
-						"server-option"));
-		assertTrue(PacketLineIn.isEnd(pckIn.readString()));
+		checkAdvertisedIfAllowed("uploadpack", "allowrefinwant", "ref-in-want");
 	}
 
 	@Test
 	public void testV2CapabilitiesRefInWantNotAdvertisedIfUnallowed() throws Exception {
-		server.getConfig().setBoolean("uploadpack", null, "allowrefinwant", false);
-		ByteArrayInputStream recvStream =
-				uploadPackV2Setup(null, null, null, PacketLineIn.end());
-		PacketLineIn pckIn = new PacketLineIn(recvStream);
+		checkUnadvertisedIfUnallowed("ref-in-want");
+	}
 
-		assertThat(pckIn.readString(), is("version 2"));
-		assertThat(
-				Arrays.asList(pckIn.readString(), pckIn.readString(),
-						pckIn.readString()),
-				hasItems("ls-refs", "fetch=shallow", "server-option"));
-		assertTrue(PacketLineIn.isEnd(pckIn.readString()));
+	@Test
+	public void testV2CapabilitiesAllowSidebandAll() throws Exception {
+		checkAdvertisedIfAllowed("uploadpack", "allowsidebandall", "sideband-all");
+		checkUnadvertisedIfUnallowed("sideband-all");
 	}
 
 	@Test
@@ -586,7 +606,7 @@
 		server.getConfig().setBoolean("uploadpack", null, "allowrefinwant", true);
 		server.getConfig().setBoolean("uploadpack", null, "advertiserefinwant", false);
 		ByteArrayInputStream recvStream =
-				uploadPackV2Setup(null, null, null, PacketLineIn.end());
+				uploadPackV2Setup(null, PacketLineIn.end());
 		PacketLineIn pckIn = new PacketLineIn(recvStream);
 
 		assertThat(pckIn.readString(), is("version 2"));
@@ -614,7 +634,8 @@
 		remote.update("refs/tags/tag", tag);
 
 		TestV2Hook hook = new TestV2Hook();
-		ByteArrayInputStream recvStream = uploadPackV2(null, null, hook,
+		ByteArrayInputStream recvStream = uploadPackV2(
+				(UploadPack up) -> {up.setProtocolV2Hook(hook);},
 				"command=ls-refs\n", PacketLineIn.end());
 		PacketLineIn pckIn = new PacketLineIn(recvStream);
 
@@ -749,7 +770,7 @@
 				PacketLineIn.end() };
 
 		TestV2Hook testHook = new TestV2Hook();
-		uploadPackV2Setup(null, null, testHook, lines);
+		uploadPackV2Setup((UploadPack up) -> {up.setProtocolV2Hook(testHook);}, lines);
 
 		LsRefsV2Request req = testHook.lsRefsRequest;
 		assertEquals(2, req.getServerOptions().size());
@@ -785,14 +806,18 @@
 		remote.update("branch1", advertized);
 
 		// This works
-		uploadPackV2(RequestPolicy.ADVERTISED, null, null, "command=fetch\n",
-				PacketLineIn.delimiter(), "want " + advertized.name() + "\n",
-				PacketLineIn.end());
+		uploadPackV2(
+			(UploadPack up) -> {up.setRequestPolicy(RequestPolicy.ADVERTISED);},
+			"command=fetch\n",
+			PacketLineIn.delimiter(),
+			"want " + advertized.name() + "\n",
+			PacketLineIn.end());
 
 		// This doesn't
 		UploadPackInternalServerErrorException e = assertThrows(
 				UploadPackInternalServerErrorException.class,
-				() -> uploadPackV2(RequestPolicy.ADVERTISED, null, null,
+				() -> uploadPackV2(
+						(UploadPack up) -> {up.setRequestPolicy(RequestPolicy.ADVERTISED);},
 						"command=fetch\n", PacketLineIn.delimiter(),
 						"want " + unadvertized.name() + "\n",
 						PacketLineIn.end()));
@@ -809,14 +834,18 @@
 		remote.update("branch1", advertized);
 
 		// This works
-		uploadPackV2(RequestPolicy.REACHABLE_COMMIT, null, null,
-				"command=fetch\n", PacketLineIn.delimiter(),
-				"want " + reachable.name() + "\n", PacketLineIn.end());
+		uploadPackV2(
+			(UploadPack up) -> {up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT);},
+			"command=fetch\n",
+			PacketLineIn.delimiter(),
+			"want " + reachable.name() + "\n",
+				PacketLineIn.end());
 
 		// This doesn't
 		UploadPackInternalServerErrorException e = assertThrows(
 				UploadPackInternalServerErrorException.class,
-				() -> uploadPackV2(RequestPolicy.REACHABLE_COMMIT, null, null,
+				() -> uploadPackV2(
+						(UploadPack up) -> {up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT);},
 						"command=fetch\n", PacketLineIn.delimiter(),
 						"want " + unreachable.name() + "\n",
 						PacketLineIn.end()));
@@ -832,15 +861,25 @@
 		remote.update("secret", tip);
 
 		// This works
-		uploadPackV2(RequestPolicy.TIP, new RejectAllRefFilter(), null,
-				"command=fetch\n", PacketLineIn.delimiter(),
-				"want " + tip.name() + "\n", PacketLineIn.end());
+		uploadPackV2(
+			(UploadPack up) -> {
+				up.setRequestPolicy(RequestPolicy.TIP);
+				up.setRefFilter(new RejectAllRefFilter());
+			},
+			"command=fetch\n",
+			PacketLineIn.delimiter(),
+			"want " + tip.name() + "\n",
+				PacketLineIn.end());
 
 		// This doesn't
 		UploadPackInternalServerErrorException e = assertThrows(
 				UploadPackInternalServerErrorException.class,
-				() -> uploadPackV2(RequestPolicy.TIP, new RejectAllRefFilter(),
-						null, "command=fetch\n", PacketLineIn.delimiter(),
+				() -> uploadPackV2(
+						(UploadPack up) -> {
+							up.setRequestPolicy(RequestPolicy.TIP);
+							up.setRefFilter(new RejectAllRefFilter());
+						},
+						"command=fetch\n", PacketLineIn.delimiter(),
 						"want " + parentOfTip.name() + "\n",
 						PacketLineIn.end()));
 		assertThat(e.getCause().getMessage(),
@@ -856,16 +895,24 @@
 		remote.update("secret", tip);
 
 		// This works
-		uploadPackV2(RequestPolicy.REACHABLE_COMMIT_TIP,
-				new RejectAllRefFilter(), null, "command=fetch\n",
+		uploadPackV2(
+				(UploadPack up) -> {
+					up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT_TIP);
+					up.setRefFilter(new RejectAllRefFilter());
+				},
+				"command=fetch\n",
 				PacketLineIn.delimiter(), "want " + parentOfTip.name() + "\n",
 				PacketLineIn.end());
 
 		// This doesn't
 		UploadPackInternalServerErrorException e = assertThrows(
 				UploadPackInternalServerErrorException.class,
-				() -> uploadPackV2(RequestPolicy.REACHABLE_COMMIT_TIP,
-						new RejectAllRefFilter(), null, "command=fetch\n",
+				() -> uploadPackV2(
+						(UploadPack up) -> {
+							up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT_TIP);
+							up.setRefFilter(new RejectAllRefFilter());
+						},
+						"command=fetch\n",
 						PacketLineIn.delimiter(),
 						"want " + unreachable.name() + "\n",
 						PacketLineIn.end()));
@@ -879,9 +926,7 @@
 
 		// Exercise to make sure that even unreachable commits can be fetched
 		uploadPackV2(
-			RequestPolicy.ANY,
-			null,
-			null,
+			(UploadPack up) -> {up.setRequestPolicy(RequestPolicy.ANY);},
 			"command=fetch\n",
 			PacketLineIn.delimiter(),
 			"want " + unreachable.name() + "\n",
@@ -1481,7 +1526,7 @@
 				PacketLineIn.end() };
 
 		TestV2Hook testHook = new TestV2Hook();
-		uploadPackV2Setup(null, null, testHook, lines);
+		uploadPackV2Setup((UploadPack up) -> {up.setProtocolV2Hook(testHook);}, lines);
 
 		FetchV2Request req = testHook.fetchRequest;
 		assertNotNull(req);
@@ -1565,8 +1610,9 @@
 		input.add("done\n");
 		input.add(PacketLineIn.end());
 		ByteArrayInputStream recvStream =
-				uploadPackV2(RequestPolicy.ANY, /*refFilter=*/null,
-							 /*hook=*/null, input.toArray(new String[0]));
+				uploadPackV2(
+						(UploadPack up) -> {up.setRequestPolicy(RequestPolicy.ANY);},
+						input.toArray(new String[0]));
 		PacketLineIn pckIn = new PacketLineIn(recvStream);
 		assertThat(pckIn.readString(), is("packfile"));
 		parsePack(recvStream);
@@ -1833,34 +1879,39 @@
 				.has(preparator.subtree3.toObjectId()));
 	}
 
-	@Test
-	public void testV2FetchFilterWhenNotAllowed() throws Exception {
+	private void checkV2FetchWhenNotAllowed(String fetchLine, String expectedMessage)
+			throws Exception {
 		RevCommit commit = remote.commit().message("0").create();
 		remote.update("master", commit);
 
-		server.getConfig().setBoolean("uploadpack", null, "allowfilter", false);
-
 		UploadPackInternalServerErrorException e = assertThrows(
 				UploadPackInternalServerErrorException.class,
 				() -> uploadPackV2("command=fetch\n", PacketLineIn.delimiter(),
 						"want " + commit.toObjectId().getName() + "\n",
-						"filter blob:limit=5\n", "done\n", PacketLineIn.end()));
+						fetchLine, "done\n", PacketLineIn.end()));
 		assertThat(e.getCause().getMessage(),
-				containsString("unexpected filter blob:limit=5"));
+				containsString(expectedMessage));
+	}
+
+	@Test
+	public void testV2FetchFilterWhenNotAllowed() throws Exception {
+		checkV2FetchWhenNotAllowed(
+			"filter blob:limit=5\n",
+			"unexpected filter blob:limit=5");
 	}
 
 	@Test
 	public void testV2FetchWantRefIfNotAllowed() throws Exception {
-		RevCommit one = remote.commit().message("1").create();
-		remote.update("one", one);
+		checkV2FetchWhenNotAllowed(
+			"want-ref refs/heads/one\n",
+			"unexpected want-ref refs/heads/one");
+	}
 
-		UploadPackInternalServerErrorException e = assertThrows(
-				UploadPackInternalServerErrorException.class,
-				() -> uploadPackV2("command=fetch\n", PacketLineIn.delimiter(),
-						"want-ref refs/heads/one\n", "done\n",
-						PacketLineIn.end()));
-		assertThat(e.getCause().getMessage(),
-				containsString("unexpected want-ref refs/heads/one"));
+	@Test
+	public void testV2FetchSidebandAllIfNotAllowed() throws Exception {
+		checkV2FetchWhenNotAllowed(
+			"sideband-all\n",
+			"unexpected sideband-all");
 	}
 
 	@Test
@@ -2047,6 +2098,58 @@
 	}
 
 	@Test
+	public void testV2FetchSidebandAllNoPackfile() throws Exception {
+		RevCommit fooParent = remote.commit().message("x").create();
+		RevCommit fooChild = remote.commit().message("x").parent(fooParent).create();
+		RevCommit barParent = remote.commit().message("y").create();
+		RevCommit barChild = remote.commit().message("y").parent(barParent).create();
+		remote.update("branch1", fooChild);
+		remote.update("branch2", barChild);
+
+		server.getConfig().setBoolean("uploadpack", null, "allowsidebandall", true);
+
+		ByteArrayInputStream recvStream = uploadPackV2(
+			"command=fetch\n",
+			PacketLineIn.DELIM,
+			"sideband-all\n",
+			"want " + fooChild.toObjectId().getName() + "\n",
+			"want " + barChild.toObjectId().getName() + "\n",
+			"have " + fooParent.toObjectId().getName() + "\n",
+			PacketLineIn.END);
+		PacketLineIn pckIn = new PacketLineIn(recvStream);
+
+		assertThat(pckIn.readString(), is("\001acknowledgments"));
+		assertThat(pckIn.readString(), is("\001ACK " + fooParent.getName()));
+		assertTrue(PacketLineIn.isEnd(pckIn.readString()));
+	}
+
+	@Test
+	public void testV2FetchSidebandAllPackfile() throws Exception {
+		RevCommit commit = remote.commit().message("x").create();
+		remote.update("master", commit);
+
+		server.getConfig().setBoolean("uploadpack", null, "allowsidebandall", true);
+
+		ByteArrayInputStream recvStream = uploadPackV2("command=fetch\n",
+				PacketLineIn.DELIM,
+				"want " + commit.getName() + "\n",
+				"sideband-all\n",
+				"done\n",
+				PacketLineIn.END);
+		PacketLineIn pckIn = new PacketLineIn(recvStream);
+
+		String s;
+		// When sideband-all is used, object counting happens before
+		// "packfile" is written, and object counting outputs progress
+		// in sideband 2. Skip all these lines.
+		for (s = pckIn.readString(); s.startsWith("\002"); s = pckIn.readString()) {
+			// do nothing
+		}
+		assertThat(s, is("\001packfile"));
+		parsePack(recvStream);
+	}
+
+	@Test
 	public void testGetPeerAgentProtocolV0() throws Exception {
 		RevCommit one = remote.commit().message("1").create();
 		remote.update("one", one);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
index 258ccee..968ade6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
@@ -171,7 +171,7 @@
 		infoDirectory = new File(objects, "info"); //$NON-NLS-1$
 		packDirectory = new File(objects, "pack"); //$NON-NLS-1$
 		preservedDirectory = new File(packDirectory, "preserved"); //$NON-NLS-1$
-		alternatesFile = new File(infoDirectory, "alternates"); //$NON-NLS-1$
+		alternatesFile = new File(objects, Constants.INFO_ALTERNATES);
 		packList = new AtomicReference<>(NO_PACKS);
 		unpackedObjectCache = new UnpackedObjectCache();
 		this.fs = fs;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
index d1cf1cd..96e5066 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
@@ -681,7 +681,7 @@
 	 */
 	protected void setupInternals() throws IOException {
 		if (getObjectDirectory() == null && getGitDir() != null)
-			setObjectDirectory(safeFS().resolve(getGitDir(), "objects")); //$NON-NLS-1$
+			setObjectDirectory(safeFS().resolve(getGitDir(), Constants.OBJECTS));
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
index 8f4468e..a084c82 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
@@ -275,9 +275,27 @@
 	/** Logs folder name */
 	public static final String LOGS = "logs";
 
+	/**
+	 * Objects folder name
+	 * @since 5.5
+	 */
+	public static final String OBJECTS = "objects";
+
 	/** Info refs folder */
 	public static final String INFO_REFS = "info/refs";
 
+	/**
+	 * Info alternates file (goes under OBJECTS)
+	 * @since 5.5
+	 */
+	public static final String INFO_ALTERNATES = "info/alternates";
+
+	/**
+	 * HTTP alternates file (goes under OBJECTS)
+	 * @since 5.5
+	 */
+	public static final String INFO_HTTP_ALTERNATES = "info/http-alternates";
+
 	/** Packed refs file */
 	public static final String PACKED_REFS = "packed-refs";
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
index 27befba..fa113bf 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
@@ -474,7 +474,7 @@
 		 *         Git directory.
 		 */
 		public static boolean isGitRepository(File dir, FS fs) {
-			return fs.resolve(dir, "objects").exists() //$NON-NLS-1$
+			return fs.resolve(dir, Constants.OBJECTS).exists()
 					&& fs.resolve(dir, "refs").exists() //$NON-NLS-1$
 					&& isValidHead(new File(dir, Constants.HEAD));
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java
index 6c24269..86574c1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java
@@ -72,6 +72,8 @@
 	@NonNull
 	private final List<String> serverOptions;
 
+	private final boolean sidebandAll;
+
 	FetchV2Request(@NonNull List<ObjectId> peerHas,
 			@NonNull List<String> wantedRefs,
 			@NonNull Set<ObjectId> wantIds,
@@ -79,7 +81,8 @@
 			@NonNull List<String> deepenNotRefs, int depth,
 			@NonNull FilterSpec filterSpec,
 			boolean doneReceived, @NonNull Set<String> clientCapabilities,
-			@Nullable String agent, @NonNull List<String> serverOptions) {
+			@Nullable String agent, @NonNull List<String> serverOptions,
+			boolean sidebandAll) {
 		super(wantIds, depth, clientShallowCommits, filterSpec,
 				clientCapabilities, deepenSince,
 				deepenNotRefs, agent);
@@ -87,6 +90,7 @@
 		this.wantedRefs = requireNonNull(wantedRefs);
 		this.doneReceived = doneReceived;
 		this.serverOptions = requireNonNull(serverOptions);
+		this.sidebandAll = sidebandAll;
 	}
 
 	/**
@@ -127,6 +131,13 @@
 		return serverOptions;
 	}
 
+	/**
+	 * @return true if "sideband-all" was received
+	 */
+	boolean getSidebandAll() {
+		return sidebandAll;
+	}
+
 	/** @return A builder of {@link FetchV2Request}. */
 	static Builder builder() {
 		return new Builder();
@@ -159,6 +170,8 @@
 
 		final List<String> serverOptions = new ArrayList<>();
 
+		boolean sidebandAll;
+
 		private Builder() {
 		}
 
@@ -318,13 +331,23 @@
 		}
 
 		/**
+		 * @param value true if client sent "sideband-all"
+		 * @return this builder
+		 */
+		Builder setSidebandAll(boolean value) {
+			sidebandAll = value;
+			return this;
+		}
+
+		/**
 		 * @return Initialized fetch request
 		 */
 		FetchV2Request build() {
 			return new FetchV2Request(peerHas, wantedRefs, wantIds,
 					clientShallowCommits, deepenSince, deepenNotRefs,
 					depth, filterSpec, doneReceived, clientCapabilities,
-					agent, Collections.unmodifiableList(serverOptions));
+					agent, Collections.unmodifiableList(serverOptions),
+					sidebandAll);
 		}
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
index 096bb67..e3c0bc6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
@@ -174,6 +174,14 @@
 	public static final String OPTION_WANT_REF = "want-ref"; //$NON-NLS-1$
 
 	/**
+	 * The client requested that the whole response be multiplexed, with
+	 * each non-flush and non-delim pkt prefixed by a sideband designator.
+	 *
+	 * @since 5.5
+	 */
+	public static final String OPTION_SIDEBAND_ALL = "sideband-all"; //$NON-NLS-1$
+
+	/**
 	 * The client supports atomic pushes. If this option is used, the server
 	 * will update all refs within one atomic transaction.
 	 *
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschConfigSessionFactory.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschConfigSessionFactory.java
index 0bdd6ba..79af1eb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschConfigSessionFactory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschConfigSessionFactory.java
@@ -551,7 +551,7 @@
 	 * @param config
 	 *            to use
 	 */
-	void setConfig(OpenSshConfig config) {
+	synchronized void setConfig(OpenSshConfig config) {
 		this.config = config;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java
index e940091..7f837bb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java
@@ -74,6 +74,8 @@
 
 	private boolean flushOnEnd;
 
+	private boolean usingSideband;
+
 	/**
 	 * Create a new packet line writer.
 	 *
@@ -98,6 +100,28 @@
 	}
 
 	/**
+	 * @return whether to add a sideband designator to each non-flush and
+	 *     non-delim packet
+	 * @see #setUsingSideband
+	 * @since 5.5
+	 */
+	public boolean isUsingSideband() {
+		return usingSideband;
+	}
+
+	/**
+	 * @param value If true, when writing packet lines, add, as the first
+	 *     byte, a sideband designator to each non-flush and non-delim
+	 *     packet. See pack-protocol.txt and protocol-v2.txt from the Git
+	 *     project for more information, specifically the "side-band" and
+	 *     "sideband-all" sections.
+	 * @since 5.5
+	 */
+	public void setUsingSideband(boolean value) {
+		this.usingSideband = value;
+	}
+
+	/**
 	 * Write a UTF-8 encoded string as a single length-delimited packet.
 	 *
 	 * @param s
@@ -139,8 +163,14 @@
 	 * @since 4.5
 	 */
 	public void writePacket(byte[] buf, int pos, int len) throws IOException {
-		formatLength(len + 4);
-		out.write(lenbuffer, 0, 4);
+		if (usingSideband) {
+			formatLength(len + 5);
+			out.write(lenbuffer, 0, 4);
+			out.write(1);
+		} else {
+			formatLength(len + 4);
+			out.write(lenbuffer, 0, 4);
+		}
 		out.write(buf, pos, len);
 		if (log.isDebugEnabled()) {
 			String s = RawParseUtils.decode(UTF_8, buf, pos, len);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHookChain.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHookChain.java
index 542abe7..4334888 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHookChain.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHookChain.java
@@ -42,7 +42,9 @@
 
 package org.eclipse.jgit.transport;
 
+import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.eclipse.jgit.storage.pack.PackStatistics;
 
@@ -55,8 +57,7 @@
  * @since 4.1
  */
 public class PostUploadHookChain implements PostUploadHook {
-	private final PostUploadHook[] hooks;
-	private final int count;
+	private final List<PostUploadHook> hooks;
 
 	/**
 	 * Create a new hook chaining the given hooks together.
@@ -65,29 +66,29 @@
 	 *            hooks to execute, in order.
 	 * @return a new chain of the given hooks.
 	 */
-	public static PostUploadHook newChain(List<? extends PostUploadHook> hooks) {
-		PostUploadHook[] newHooks = new PostUploadHook[hooks.size()];
-		int i = 0;
-		for (PostUploadHook hook : hooks)
-			if (hook != PostUploadHook.NULL)
-				newHooks[i++] = hook;
-		if (i == 0)
+	public static PostUploadHook newChain(List<PostUploadHook> hooks) {
+		List<PostUploadHook> newHooks = hooks.stream()
+				.filter(hook -> !hook.equals(PostUploadHook.NULL))
+				.collect(Collectors.toList());
+
+		if (newHooks.isEmpty()) {
 			return PostUploadHook.NULL;
-		else if (i == 1)
-			return newHooks[0];
-		else
-			return new PostUploadHookChain(newHooks, i);
+		} else if (newHooks.size() == 1) {
+			return newHooks.get(0);
+		} else {
+			return new PostUploadHookChain(newHooks);
+		}
 	}
 
 	/** {@inheritDoc} */
 	@Override
 	public void onPostUpload(PackStatistics stats) {
-		for (int i = 0; i < count; i++)
-			hooks[i].onPostUpload(stats);
+		for (PostUploadHook hook : hooks) {
+			hook.onPostUpload(stats);
+		}
 	}
 
-	private PostUploadHookChain(PostUploadHook[] hooks, int count) {
-		this.hooks = hooks;
-		this.count = count;
+	private PostUploadHookChain(List<PostUploadHook> hooks) {
+		this.hooks = Collections.unmodifiableList(hooks);
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreUploadHookChain.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreUploadHookChain.java
index bfd52af..2192654 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreUploadHookChain.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PreUploadHookChain.java
@@ -44,7 +44,9 @@
 package org.eclipse.jgit.transport;
 
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -56,8 +58,7 @@
  * one hook throws an exception, execution of remaining hook methods is aborted.
  */
 public class PreUploadHookChain implements PreUploadHook {
-	private final PreUploadHook[] hooks;
-	private final int count;
+	private final List<PreUploadHook> hooks;
 
 	/**
 	 * Create a new hook chaining the given hooks together.
@@ -66,18 +67,18 @@
 	 *            hooks to execute, in order.
 	 * @return a new hook chain of the given hooks.
 	 */
-	public static PreUploadHook newChain(List<? extends PreUploadHook> hooks) {
-		PreUploadHook[] newHooks = new PreUploadHook[hooks.size()];
-		int i = 0;
-		for (PreUploadHook hook : hooks)
-			if (hook != PreUploadHook.NULL)
-				newHooks[i++] = hook;
-		if (i == 0)
+	public static PreUploadHook newChain(List<PreUploadHook> hooks) {
+		List<PreUploadHook> newHooks = hooks.stream()
+				.filter(hook -> !hook.equals(PreUploadHook.NULL))
+				.collect(Collectors.toList());
+
+		if (newHooks.isEmpty()) {
 			return PreUploadHook.NULL;
-		else if (i == 1)
-			return newHooks[0];
-		else
-			return new PreUploadHookChain(newHooks, i);
+		} else if (newHooks.size() == 1) {
+			return newHooks.get(0);
+		} else {
+			return new PreUploadHookChain(newHooks);
+		}
 	}
 
 	/** {@inheritDoc} */
@@ -85,8 +86,9 @@
 	public void onBeginNegotiateRound(UploadPack up,
 			Collection<? extends ObjectId> wants, int cntOffered)
 			throws ServiceMayNotContinueException {
-		for (int i = 0; i < count; i++)
-			hooks[i].onBeginNegotiateRound(up, wants, cntOffered);
+		for (PreUploadHook hook : hooks) {
+			hook.onBeginNegotiateRound(up, wants, cntOffered);
+		}
 	}
 
 	/** {@inheritDoc} */
@@ -95,8 +97,9 @@
 			Collection<? extends ObjectId> wants, int cntCommon,
 			int cntNotFound, boolean ready)
 			throws ServiceMayNotContinueException {
-		for (int i = 0; i < count; i++)
-			hooks[i].onEndNegotiateRound(up, wants, cntCommon, cntNotFound, ready);
+		for (PreUploadHook hook : hooks) {
+			hook.onEndNegotiateRound(up, wants, cntCommon, cntNotFound, ready);
+		}
 	}
 
 	/** {@inheritDoc} */
@@ -105,12 +108,12 @@
 			Collection<? extends ObjectId> wants,
 			Collection<? extends ObjectId> haves)
 			throws ServiceMayNotContinueException {
-		for (int i = 0; i < count; i++)
-			hooks[i].onSendPack(up, wants, haves);
+		for (PreUploadHook hook : hooks) {
+			hook.onSendPack(up, wants, haves);
+		}
 	}
 
-	private PreUploadHookChain(PreUploadHook[] hooks, int count) {
-		this.hooks = hooks;
-		this.count = count;
+	private PreUploadHookChain(List<PreUploadHook> hooks) {
+		this.hooks = Collections.unmodifiableList(hooks);
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2HookChain.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2HookChain.java
new file mode 100644
index 0000000..cc36473
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2HookChain.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2019, Google LLC.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.transport;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * {@link org.eclipse.jgit.transport.ProtocolV2Hook} that delegates to a list of
+ * other hooks.
+ * <p>
+ * Hooks are run in the order passed to the constructor. If running a method on
+ * one hook throws an exception, execution of remaining hook methods is aborted.
+ *
+ * @since 5.5
+ */
+public class ProtocolV2HookChain implements ProtocolV2Hook {
+	private final List<? extends ProtocolV2Hook> hooks;
+
+	/**
+	 * Create a new hook chaining the given hooks together.
+	 *
+	 * @param hooks
+	 *            hooks to execute, in order.
+	 * @return a new hook chain of the given hooks.
+	 */
+	public static ProtocolV2Hook newChain(
+			List<? extends ProtocolV2Hook> hooks) {
+		List<? extends ProtocolV2Hook> newHooks = hooks.stream()
+				.filter(hook -> !hook.equals(ProtocolV2Hook.DEFAULT))
+				.collect(Collectors.toList());
+
+		if (newHooks.isEmpty()) {
+			return ProtocolV2Hook.DEFAULT;
+		} else if (newHooks.size() == 1) {
+			return newHooks.get(0);
+		} else {
+			return new ProtocolV2HookChain(newHooks);
+		}
+	}
+
+	@Override
+	public void onCapabilities(CapabilitiesV2Request req)
+			throws ServiceMayNotContinueException {
+		for (ProtocolV2Hook hook : hooks) {
+			hook.onCapabilities(req);
+		}
+	}
+
+	@Override
+	public void onLsRefs(LsRefsV2Request req)
+			throws ServiceMayNotContinueException {
+		for (ProtocolV2Hook hook : hooks) {
+			hook.onLsRefs(req);
+		}
+	}
+
+	@Override
+	public void onFetch(FetchV2Request req)
+			throws ServiceMayNotContinueException {
+		for (ProtocolV2Hook hook : hooks) {
+			hook.onFetch(req);
+		}
+	}
+
+	private ProtocolV2HookChain(List<? extends ProtocolV2Hook> hooks) {
+		this.hooks = Collections.unmodifiableList(hooks);
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java
index caba15f..453be7f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java
@@ -49,6 +49,7 @@
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_NO_PROGRESS;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_OFS_DELTA;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SERVER_OPTION;
+import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDEBAND_ALL;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND_64K;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_THIN_PACK;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_WANT_REF;
@@ -210,6 +211,9 @@
 				filterReceived = true;
 				reqBuilder.setFilterSpec(FilterSpec.fromFilterLine(
 						line2.substring(OPTION_FILTER.length() + 1)));
+			} else if (transferConfig.isAllowSidebandAll()
+					&& line2.equals(OPTION_SIDEBAND_ALL)) {
+				reqBuilder.setSidebandAll(true);
 			} else {
 				throw new PackProtocolException(MessageFormat
 						.format(JGitText.get().unexpectedPacketLine, line2));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
index a3e655c..758d74c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
@@ -131,6 +131,7 @@
 	private final boolean allowTipSha1InWant;
 	private final boolean allowReachableSha1InWant;
 	private final boolean allowFilter;
+	private final boolean allowSidebandAll;
 	final @Nullable ProtocolVersion protocolVersion;
 	final String[] hideRefs;
 
@@ -210,6 +211,8 @@
 				"uploadpack", "allowfilter", false);
 		protocolVersion = ProtocolVersion.parse(rc.getString("protocol", null, "version"));
 		hideRefs = rc.getStringList("uploadpack", null, "hiderefs");
+		allowSidebandAll = rc.getBoolean(
+				"uploadpack", "allowsidebandall", false);
 	}
 
 	/**
@@ -292,6 +295,14 @@
 	}
 
 	/**
+	 * @return true if clients are allowed to specify a "sideband-all" line
+	 * @since 5.5
+	 */
+	public boolean isAllowSidebandAll() {
+		return allowSidebandAll;
+	}
+
+	/**
 	 * Get {@link org.eclipse.jgit.transport.RefFilter} respecting configured
 	 * hidden refs.
 	 *
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java
index 6118cb8..c0a70bc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java
@@ -259,7 +259,7 @@
 		@Override
 		Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException {
 			try {
-				return readAlternates(INFO_ALTERNATES);
+				return readAlternates(Constants.INFO_ALTERNATES);
 			} catch (FileNotFoundException err) {
 				// Fall through.
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
index 27ab879..d5b06c1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
@@ -48,6 +48,8 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.Constants.INFO_ALTERNATES;
+import static org.eclipse.jgit.lib.Constants.INFO_HTTP_ALTERNATES;
 import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP;
 import static org.eclipse.jgit.util.HttpSupport.ENCODING_X_GZIP;
 import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java
index 5c68308..8d91c60 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportSftp.java
@@ -43,7 +43,9 @@
 
 package org.eclipse.jgit.transport;
 
+import static org.eclipse.jgit.lib.Constants.INFO_ALTERNATES;
 import static org.eclipse.jgit.lib.Constants.LOCK_SUFFIX;
+import static org.eclipse.jgit.lib.Constants.OBJECTS;
 
 import java.io.BufferedReader;
 import java.io.FileNotFoundException;
@@ -172,7 +174,7 @@
 			try {
 				ftp = newSftp();
 				ftp.cd(path);
-				ftp.cd("objects"); //$NON-NLS-1$
+				ftp.cd(OBJECTS);
 				objectsPath = ftp.pwd();
 			} catch (FtpChannel.FtpException f) {
 				throw new TransportException(MessageFormat.format(
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
index 37ecead..2194f2f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
@@ -61,6 +61,7 @@
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_NO_PROGRESS;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_OFS_DELTA;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SHALLOW;
+import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDEBAND_ALL;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDE_BAND_64K;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_THIN_PACK;
@@ -278,8 +279,6 @@
 
 	private PacketLineIn pckIn;
 
-	private PacketLineOut pckOut;
-
 	private OutputStream msgOut = NullOutputStream.INSTANCE;
 
 	/**
@@ -603,6 +602,18 @@
 	}
 
 	/**
+	 * Get the currently installed protocol v2 hook.
+	 *
+	 * @return the hook or a default implementation if none installed.
+	 *
+	 * @since 5.5
+	 */
+	public ProtocolV2Hook getProtocolV2Hook() {
+		return this.protocolV2Hook != null ? this.protocolV2Hook
+				: ProtocolV2Hook.DEFAULT;
+	}
+
+	/**
 	 * Set the filter used while advertising the refs to the client.
 	 * <p>
 	 * Only refs allowed by this filter will be sent to the client. The filter
@@ -753,6 +764,7 @@
 	 */
 	public void upload(InputStream input, OutputStream output,
 			@Nullable OutputStream messages) throws IOException {
+		PacketLineOut pckOut = null;
 		try {
 			rawIn = input;
 			if (messages != null)
@@ -778,9 +790,9 @@
 			pckIn = new PacketLineIn(rawIn);
 			pckOut = new PacketLineOut(rawOut);
 			if (useProtocolV2()) {
-				serviceV2();
+				serviceV2(pckOut);
 			} else {
-				service();
+				service(pckOut);
 			}
 		} catch (UploadPackInternalServerErrorException err) {
 			// UploadPackInternalServerErrorException is a special exception
@@ -972,7 +984,7 @@
 		return RefDatabase.findRef(getAdvertisedOrDefaultRefs(), name);
 	}
 
-	private void service() throws IOException {
+	private void service(PacketLineOut pckOut) throws IOException {
 		boolean sendPack = false;
 		// If it's a non-bidi request, we need to read the entire request before
 		// writing a response. Buffer the response until then.
@@ -1028,7 +1040,7 @@
 
 			if (!req.getClientShallowCommits().isEmpty())
 				walk.assumeShallow(req.getClientShallowCommits());
-			sendPack = negotiate(req, accumulator);
+			sendPack = negotiate(req, accumulator, pckOut);
 			accumulator.timeNegotiating += System.currentTimeMillis()
 					- negotiateStart;
 
@@ -1054,11 +1066,11 @@
 
 		if (sendPack) {
 			sendPack(accumulator, req, refs == null ? null : refs.values(),
-					unshallowCommits, Collections.emptyList());
+					unshallowCommits, Collections.emptyList(), pckOut);
 		}
 	}
 
-	private void lsRefsV2() throws IOException {
+	private void lsRefsV2(PacketLineOut pckOut) throws IOException {
 		ProtocolV2Parser parser = new ProtocolV2Parser(transferConfig);
 		LsRefsV2Request req = parser.parseLsRefsRequest(pckIn);
 		protocolV2Hook.onLsRefs(req);
@@ -1103,7 +1115,7 @@
 		return result;
 	}
 
-	private void fetchV2() throws IOException {
+	private void fetchV2(PacketLineOut pckOut) throws IOException {
 		// Depending on the requestValidator, #processHaveLines may
 		// require that advertised be set. Set it only in the required
 		// circumstances (to avoid a full ref lookup in the case that
@@ -1123,6 +1135,10 @@
 
 		protocolV2Hook.onFetch(req);
 
+		if (req.getSidebandAll()) {
+			pckOut.setUsingSideband(true);
+		}
+
 		// TODO(ifrade): Refactor to pass around the Request object, instead of
 		// copying data back to class fields
 		List<ObjectId> deepenNots = new ArrayList<>();
@@ -1208,13 +1224,17 @@
 
 			if (sectionSent)
 				pckOut.writeDelim();
-			pckOut.writeString("packfile\n"); //$NON-NLS-1$
+			if (!pckOut.isUsingSideband()) {
+				// sendPack will write "packfile\n" for us if sideband-all is used.
+				// But sideband-all is not used, so we have to write it ourselves.
+				pckOut.writeString("packfile\n"); //$NON-NLS-1$
+			}
 			sendPack(new PackStatistics.Accumulator(),
 					req,
 					req.getClientCapabilities().contains(OPTION_INCLUDE_TAG)
 						? db.getRefDatabase().getRefsByPrefix(R_TAGS)
 						: null,
-					unshallowCommits, deepenNots);
+					unshallowCommits, deepenNots, pckOut);
 			// sendPack invokes pckOut.end() for us, so we do not
 			// need to invoke it here.
 		} else {
@@ -1227,7 +1247,7 @@
 	 * Returns true if this is the last command and we should tear down the
 	 * connection.
 	 */
-	private boolean serveOneCommandV2() throws IOException {
+	private boolean serveOneCommandV2(PacketLineOut pckOut) throws IOException {
 		String command;
 		try {
 			command = pckIn.readString();
@@ -1242,11 +1262,11 @@
 			return true;
 		}
 		if (command.equals("command=" + COMMAND_LS_REFS)) { //$NON-NLS-1$
-			lsRefsV2();
+			lsRefsV2(pckOut);
 			return false;
 		}
 		if (command.equals("command=" + COMMAND_FETCH)) { //$NON-NLS-1$
-			fetchV2();
+			fetchV2(pckOut);
 			return false;
 		}
 		throw new PackProtocolException(MessageFormat
@@ -1264,12 +1284,13 @@
 				COMMAND_FETCH + '=' +
 				(transferConfig.isAllowFilter() ? OPTION_FILTER + ' ' : "") + //$NON-NLS-1$
 				(advertiseRefInWant ? CAPABILITY_REF_IN_WANT + ' ' : "") + //$NON-NLS-1$
+				(transferConfig.isAllowSidebandAll() ? OPTION_SIDEBAND_ALL + ' ' : "") + //$NON-NLS-1$
 				OPTION_SHALLOW);
 		caps.add(CAPABILITY_SERVER_OPTION);
 		return caps;
 	}
 
-	private void serviceV2() throws IOException {
+	private void serviceV2(PacketLineOut pckOut) throws IOException {
 		if (biDirectionalPipe) {
 			// Just like in service(), the capability advertisement
 			// is sent only if this is a bidirectional pipe. (If
@@ -1282,14 +1303,14 @@
 			}
 			pckOut.end();
 
-			while (!serveOneCommandV2()) {
+			while (!serveOneCommandV2(pckOut)) {
 				// Repeat until an empty command or EOF.
 			}
 			return;
 		}
 
 		try {
-			serveOneCommandV2();
+			serveOneCommandV2(pckOut);
 		} finally {
 			while (0 < rawIn.skip(2048) || 0 <= rawIn.read()) {
 				// Discard until EOF.
@@ -1585,7 +1606,8 @@
 	}
 
 	private boolean negotiate(FetchRequest req,
-			PackStatistics.Accumulator accumulator)
+			PackStatistics.Accumulator accumulator,
+			PacketLineOut pckOut)
 			throws IOException {
 		okToGiveUp = Boolean.FALSE;
 
@@ -2063,6 +2085,8 @@
 	 *            shallow commits on the client that are now becoming unshallow
 	 * @param deepenNots
 	 *            objects that the client specified using --shallow-exclude
+	 * @param pckOut
+	 *            output writer
 	 * @throws IOException
 	 *             if an error occurred while generating or writing the pack.
 	 */
@@ -2070,14 +2094,15 @@
 			FetchRequest req,
 			@Nullable Collection<Ref> allTags,
 			List<ObjectId> unshallowCommits,
-			List<ObjectId> deepenNots) throws IOException {
+			List<ObjectId> deepenNots,
+			PacketLineOut pckOut) throws IOException {
 		Set<String> caps = req.getClientCapabilities();
 		boolean sideband = caps.contains(OPTION_SIDE_BAND)
 				|| caps.contains(OPTION_SIDE_BAND_64K);
 		if (sideband) {
 			try {
 				sendPack(true, req, accumulator, allTags, unshallowCommits,
-						deepenNots);
+						deepenNots, pckOut);
 			} catch (ServiceMayNotContinueException err) {
 				String message = err.getMessage();
 				if (message == null) {
@@ -2101,7 +2126,8 @@
 				throw new UploadPackInternalServerErrorException(err);
 			}
 		} else {
-			sendPack(false, req, accumulator, allTags, unshallowCommits, deepenNots);
+			sendPack(false, req, accumulator, allTags, unshallowCommits, deepenNots,
+					pckOut);
 		}
 	}
 
@@ -2132,6 +2158,8 @@
 	 *            shallow commits on the client that are now becoming unshallow
 	 * @param deepenNots
 	 *            objects that the client specified using --shallow-exclude
+	 * @param pckOut
+	 *            output writer
 	 * @throws IOException
 	 *             if an error occurred while generating or writing the pack.
 	 */
@@ -2140,7 +2168,8 @@
 			PackStatistics.Accumulator accumulator,
 			@Nullable Collection<Ref> allTags,
 			List<ObjectId> unshallowCommits,
-			List<ObjectId> deepenNots) throws IOException {
+			List<ObjectId> deepenNots,
+			PacketLineOut pckOut) throws IOException {
 		ProgressMonitor pm = NullProgressMonitor.INSTANCE;
 		OutputStream packOut = rawOut;
 
@@ -2268,6 +2297,9 @@
 				}
 			}
 
+			if (pckOut.isUsingSideband()) {
+				pckOut.writeString("packfile\n"); //$NON-NLS-1$
+			}
 			pw.writePack(pm, NullProgressMonitor.INSTANCE, packOut);
 
 			if (msgOut != NullOutputStream.INSTANCE) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkRemoteObjectDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkRemoteObjectDatabase.java
index 5c67253..4862d67 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkRemoteObjectDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkRemoteObjectDatabase.java
@@ -82,10 +82,6 @@
 
 	static final String INFO_PACKS = "info/packs"; //$NON-NLS-1$
 
-	static final String INFO_ALTERNATES = "info/alternates"; //$NON-NLS-1$
-
-	static final String INFO_HTTP_ALTERNATES = "info/http-alternates"; //$NON-NLS-1$
-
 	static final String INFO_REFS = ROOT_DIR + Constants.INFO_REFS;
 
 	abstract URIish getURI();
@@ -107,8 +103,10 @@
 	/**
 	 * Obtain alternate connections to alternate object databases (if any).
 	 * <p>
-	 * Alternates are typically read from the file {@link #INFO_ALTERNATES} or
-	 * {@link #INFO_HTTP_ALTERNATES}. The content of each line must be resolved
+         * Alternates are typically read from the file
+         * {@link org.eclipse.jgit.lib.Constants#INFO_ALTERNATES} or
+         * {@link org.eclipse.jgit.lib.Constants#INFO_HTTP_ALTERNATES}.
+         * The content of each line must be resolved
 	 * by the implementation and a new database reference should be returned to
 	 * represent the additional location.
 	 * <p>
diff --git a/pom.xml b/pom.xml
index fd5cb70..a07c3cc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -199,7 +199,7 @@
     <httpcore-version>4.4.10</httpcore-version>
     <slf4j-version>1.7.2</slf4j-version>
     <log4j-version>1.2.15</log4j-version>
-    <maven-javadoc-plugin-version>3.1.0</maven-javadoc-plugin-version>
+    <maven-javadoc-plugin-version>3.1.1</maven-javadoc-plugin-version>
     <tycho-extras-version>1.3.0</tycho-extras-version>
     <gson-version>2.8.2</gson-version>
     <bouncycastle-version>1.61</bouncycastle-version>
@@ -376,7 +376,7 @@
             <dependency><!-- add support for ssh/scp -->
               <groupId>org.apache.maven.wagon</groupId>
               <artifactId>wagon-ssh</artifactId>
-              <version>3.3.2</version>
+              <version>3.3.3</version>
             </dependency>
           </dependencies>
         </plugin>
@@ -906,7 +906,7 @@
               <dependency>
                 <groupId>org.eclipse.jdt</groupId>
                 <artifactId>ecj</artifactId>
-                <version>3.17.0</version>
+                <version>3.18.0</version>
               </dependency>
             </dependencies>
           </plugin>