Merge branch 'stable-5.12'

* stable-5.12:
  Prepare 5.12.1-SNAPSHOT builds
  JGit v5.12.0.202106070339-r
  [license-check] Update list of project dependencies
  [errorprone] Fix warning InputStreamSlowMultibyteRead
  [errorprone] Make operator precedence explicit in OpenSshConfigFile
  Update jetty to 9.4.41.v20210516
  Prepare 5.1.17-SNAPSHOT builds
  JGit v5.1.16.202106041830-r
  BatchRefUpdate: Skip saving conflicting ref names and prefixes in memory
  BatchRefUpdateTest: Accurately assert RefsChangedEvent(s) fired
  Optimize RefDirectory.isNameConflicting()
  Update bazlets and bazel version

Change-Id: I9bd4dfc3796a5034be7b3e86b35ef591bb516a2c
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 5045e94..b0b5f68 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
@@ -999,6 +999,108 @@
 	}
 
 	@Test
+	public void testV2FetchWithoutWaitForDoneReceivesPackfile()
+			throws Exception {
+		String commonInBlob = "abcdefghijklmnopqrstuvwxyz";
+
+		RevBlob parentBlob = remote.blob(commonInBlob + "a");
+		RevCommit parent = remote
+				.commit(remote.tree(remote.file("foo", parentBlob)));
+		remote.update("branch1", parent);
+
+		RevCommit localParent = null;
+		RevCommit localChild = null;
+		try (TestRepository<InMemoryRepository> local = new TestRepository<>(
+				client)) {
+			RevBlob localParentBlob = local.blob(commonInBlob + "a");
+			localParent = local
+					.commit(local.tree(local.file("foo", localParentBlob)));
+			RevBlob localChildBlob = local.blob(commonInBlob + "b");
+			localChild = local.commit(
+					local.tree(local.file("foo", localChildBlob)), localParent);
+			local.update("branch1", localChild);
+		}
+
+		ByteArrayInputStream recvStream = uploadPackV2("command=fetch\n",
+				PacketLineIn.delimiter(),
+				"have " + localParent.toObjectId().getName() + "\n",
+				"have " + localChild.toObjectId().getName() + "\n",
+				PacketLineIn.end());
+		PacketLineIn pckIn = new PacketLineIn(recvStream);
+		assertThat(pckIn.readString(), is("acknowledgments"));
+		assertThat(Arrays.asList(pckIn.readString()),
+				hasItems("ACK " + parent.toObjectId().getName()));
+		assertThat(pckIn.readString(), is("ready"));
+		assertTrue(PacketLineIn.isDelimiter(pckIn.readString()));
+		assertThat(pckIn.readString(), is("packfile"));
+		parsePack(recvStream);
+	}
+
+	@Test
+	public void testV2FetchWithWaitForDoneOnlyDoesNegotiation()
+			throws Exception {
+		String commonInBlob = "abcdefghijklmnopqrstuvwxyz";
+
+		RevBlob parentBlob = remote.blob(commonInBlob + "a");
+		RevCommit parent = remote
+				.commit(remote.tree(remote.file("foo", parentBlob)));
+		remote.update("branch1", parent);
+
+		RevCommit localParent = null;
+		RevCommit localChild = null;
+		try (TestRepository<InMemoryRepository> local = new TestRepository<>(
+				client)) {
+			RevBlob localParentBlob = local.blob(commonInBlob + "a");
+			localParent = local
+					.commit(local.tree(local.file("foo", localParentBlob)));
+			RevBlob localChildBlob = local.blob(commonInBlob + "b");
+			localChild = local.commit(
+					local.tree(local.file("foo", localChildBlob)), localParent);
+			local.update("branch1", localChild);
+		}
+
+		ByteArrayInputStream recvStream = uploadPackV2("command=fetch\n",
+				PacketLineIn.delimiter(), "wait-for-done\n",
+				"have " + localParent.toObjectId().getName() + "\n",
+				"have " + localChild.toObjectId().getName() + "\n",
+				PacketLineIn.end());
+		PacketLineIn pckIn = new PacketLineIn(recvStream);
+		assertThat(pckIn.readString(), is("acknowledgments"));
+		assertThat(Arrays.asList(pckIn.readString()),
+				hasItems("ACK " + parent.toObjectId().getName()));
+		assertTrue(PacketLineIn.isEnd(pckIn.readString()));
+	}
+
+	@Test
+	public void testV2FetchWithWaitForDoneOnlyDoesNegotiationAndNothingToAck()
+			throws Exception {
+		String commonInBlob = "abcdefghijklmnopqrstuvwxyz";
+
+		RevCommit localParent = null;
+		RevCommit localChild = null;
+		try (TestRepository<InMemoryRepository> local = new TestRepository<>(
+				client)) {
+			RevBlob localParentBlob = local.blob(commonInBlob + "a");
+			localParent = local
+					.commit(local.tree(local.file("foo", localParentBlob)));
+			RevBlob localChildBlob = local.blob(commonInBlob + "b");
+			localChild = local.commit(
+					local.tree(local.file("foo", localChildBlob)), localParent);
+			local.update("branch1", localChild);
+		}
+
+		ByteArrayInputStream recvStream = uploadPackV2("command=fetch\n",
+				PacketLineIn.delimiter(), "wait-for-done\n",
+				"have " + localParent.toObjectId().getName() + "\n",
+				"have " + localChild.toObjectId().getName() + "\n",
+				PacketLineIn.end());
+		PacketLineIn pckIn = new PacketLineIn(recvStream);
+		assertThat(pckIn.readString(), is("acknowledgments"));
+		assertThat(pckIn.readString(), is("NAK"));
+		assertTrue(PacketLineIn.isEnd(pckIn.readString()));
+	}
+
+	@Test
 	public void testV2FetchThinPack() throws Exception {
 		String commonInBlob = "abcdefghijklmnopqrstuvwxyz";
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
index 6fbb4c5..228c25f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
@@ -482,12 +482,18 @@
 
 		private final List<String> patterns;
 
-		// Constructor used to build the merged entry; never matches anything
-		HostEntry() {
+		/**
+		 * Constructor used to build the merged entry; never matches anything
+		 */
+		public HostEntry() {
 			this.patterns = Collections.emptyList();
 		}
 
-		HostEntry(List<String> patterns) {
+		/**
+		 * @param patterns
+		 *            to be used in matching against host name.
+		 */
+		public HostEntry(List<String> patterns) {
 			this.patterns = patterns;
 		}
 
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 ea63933..50fb9d2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchV2Request.java
@@ -36,6 +36,8 @@
 
 	private final boolean doneReceived;
 
+	private final boolean waitForDone;
+
 	@NonNull
 	private final List<String> serverOptions;
 
@@ -50,7 +52,8 @@
 			@NonNull Set<ObjectId> clientShallowCommits, int deepenSince,
 			@NonNull List<String> deepenNotRefs, int depth,
 			@NonNull FilterSpec filterSpec,
-			boolean doneReceived, @NonNull Set<String> clientCapabilities,
+			boolean doneReceived, boolean waitForDone,
+			@NonNull Set<String> clientCapabilities,
 			@Nullable String agent, @NonNull List<String> serverOptions,
 			boolean sidebandAll, @NonNull List<String> packfileUriProtocols) {
 		super(wantIds, depth, clientShallowCommits, filterSpec,
@@ -59,6 +62,7 @@
 		this.peerHas = requireNonNull(peerHas);
 		this.wantedRefs = requireNonNull(wantedRefs);
 		this.doneReceived = doneReceived;
+		this.waitForDone = waitForDone;
 		this.serverOptions = requireNonNull(serverOptions);
 		this.sidebandAll = sidebandAll;
 		this.packfileUriProtocols = packfileUriProtocols;
@@ -90,7 +94,14 @@
 	}
 
 	/**
-	 * Options received in server-option lines. The caller can choose to act on
+	 * @return true if the request had a "wait-for-done" line
+	 */
+	boolean wasWaitForDoneReceived() {
+		return waitForDone;
+	}
+
+	/**
+	 * Options received in server-option lines. The caller can choose to	 act on
 	 * these in an application-specific way
 	 *
 	 * @return Immutable list of server options received in the request
@@ -141,6 +152,8 @@
 
 		boolean doneReceived;
 
+		boolean waitForDone;
+
 		@Nullable
 		String agent;
 
@@ -280,6 +293,16 @@
 		}
 
 		/**
+		 * Mark that the "wait-for-done" line has been received.
+		 *
+		 * @return this builder
+		 */
+		Builder setWaitForDone() {
+			waitForDone = true;
+			return this;
+		}
+
+		/**
 		 * Value of an agent line received after the command and before the
 		 * arguments. E.g. "agent=a.b.c/1.0" should set "a.b.c/1.0".
 		 *
@@ -328,7 +351,7 @@
 		FetchV2Request build() {
 			return new FetchV2Request(peerHas, wantedRefs, wantIds,
 					clientShallowCommits, deepenSince, deepenNotRefs,
-					depth, filterSpec, doneReceived, clientCapabilities,
+					depth, filterSpec, doneReceived, waitForDone, clientCapabilities,
 					agent, Collections.unmodifiableList(serverOptions),
 					sidebandAll,
 					Collections.unmodifiableList(packfileUriProtocols));
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 36fce7a..0480971 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
@@ -149,6 +149,13 @@
 	public static final String OPTION_SIDEBAND_ALL = "sideband-all"; //$NON-NLS-1$
 
 	/**
+	 * The server waits for client to send "done" before sending any packs back.
+	 *
+	 * @since 5.12
+	 */
+	public static final String OPTION_WAIT_FOR_DONE = "wait-for-done"; //$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/ProtocolV2Parser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java
index faccc25..92f0133 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProtocolV2Parser.java
@@ -19,6 +19,7 @@
 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_WAIT_FOR_DONE;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_WANT_REF;
 
 import java.io.IOException;
@@ -123,6 +124,8 @@
 				reqBuilder.addPeerHas(ObjectId.fromString(line2.substring(5)));
 			} else if (line2.equals("done")) { //$NON-NLS-1$
 				reqBuilder.setDoneReceived();
+			} else if (line2.equals(OPTION_WAIT_FOR_DONE)) {
+				reqBuilder.setWaitForDone();
 			} else if (line2.equals(OPTION_THIN_PACK)) {
 				reqBuilder.addClientCapability(OPTION_THIN_PACK);
 			} else if (line2.equals(OPTION_NO_PROGRESS)) {
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 83ffd41..c1d630c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
@@ -120,7 +120,10 @@
 	private final boolean allowReachableSha1InWant;
 	private final boolean allowFilter;
 	private final boolean allowSidebandAll;
+
 	private final boolean advertiseSidebandAll;
+	private final boolean advertiseWaitForDone;
+
 	final @Nullable ProtocolVersion protocolVersion;
 	final String[] hideRefs;
 
@@ -206,6 +209,8 @@
 				"uploadpack", "allowsidebandall", false);
 		advertiseSidebandAll = rc.getBoolean("uploadpack",
 				"advertisesidebandall", false);
+		advertiseWaitForDone = rc.getBoolean("uploadpack",
+				"advertisewaitfordone", false);
 	}
 
 	/**
@@ -305,6 +310,14 @@
 	}
 
 	/**
+	 * @return true to advertise wait-for-done all to the clients
+	 * @since 5.12
+	 */
+	public boolean isAdvertiseWaitForDone() {
+		return advertiseWaitForDone;
+	}
+
+	/**
 	 * Get {@link org.eclipse.jgit.transport.RefFilter} respecting configured
 	 * hidden refs.
 	 *
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 7f1ddaa..ecf1751 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
@@ -33,6 +33,7 @@
 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;
+import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_WAIT_FOR_DONE;
 import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_2_REQUEST;
 import static org.eclipse.jgit.util.RefMap.toRefMap;
 
@@ -1192,9 +1193,10 @@
 			walk.assumeShallow(req.getClientShallowCommits());
 
 		if (req.wasDoneReceived()) {
-			processHaveLines(req.getPeerHas(), ObjectId.zeroId(),
+			processHaveLines(
+					req.getPeerHas(), ObjectId.zeroId(),
 					new PacketLineOut(NullOutputStream.INSTANCE, false),
-					accumulator);
+					accumulator, req.wasWaitForDoneReceived() ? Option.WAIT_FOR_DONE : Option.NONE);
 		} else {
 			pckOut.writeString(
 					GitProtocolConstants.SECTION_ACKNOWLEDGMENTS + '\n');
@@ -1205,8 +1207,8 @@
 			}
 			processHaveLines(req.getPeerHas(), ObjectId.zeroId(),
 					new PacketLineOut(NullOutputStream.INSTANCE, false),
-					accumulator);
-			if (okToGiveUp()) {
+					accumulator, Option.NONE);
+			if (!req.wasWaitForDoneReceived() && okToGiveUp()) {
 				pckOut.writeString("ready\n"); //$NON-NLS-1$
 			} else if (commonBase.isEmpty()) {
 				pckOut.writeString("NAK\n"); //$NON-NLS-1$
@@ -1214,7 +1216,7 @@
 			sectionSent = true;
 		}
 
-		if (req.wasDoneReceived() || okToGiveUp()) {
+		if (req.wasDoneReceived() || (!req.wasWaitForDoneReceived() && okToGiveUp())) {
 			if (mayHaveShallow) {
 				if (sectionSent)
 					pckOut.writeDelim();
@@ -1312,6 +1314,9 @@
 						? OPTION_SIDEBAND_ALL + ' '
 						: "")
 				+ (cachedPackUriProvider != null ? "packfile-uris " : "")
+				+ (transferConfig.isAdvertiseWaitForDone()
+						? OPTION_WAIT_FOR_DONE + ' '
+						: "")
 				+ OPTION_SHALLOW);
 		caps.add(CAPABILITY_SERVER_OPTION);
 		return caps;
@@ -1656,7 +1661,7 @@
 			}
 
 			if (PacketLineIn.isEnd(line)) {
-				last = processHaveLines(peerHas, last, pckOut, accumulator);
+				last = processHaveLines(peerHas, last, pckOut, accumulator, Option.NONE);
 				if (commonBase.isEmpty() || multiAck != MultiAck.OFF)
 					pckOut.writeString("NAK\n"); //$NON-NLS-1$
 				if (noDone && sentReady) {
@@ -1671,7 +1676,7 @@
 				peerHas.add(ObjectId.fromString(line.substring(5)));
 				accumulator.haves++;
 			} else if (line.equals("done")) { //$NON-NLS-1$
-				last = processHaveLines(peerHas, last, pckOut, accumulator);
+				last = processHaveLines(peerHas, last, pckOut, accumulator, Option.NONE);
 
 				if (commonBase.isEmpty())
 					pckOut.writeString("NAK\n"); //$NON-NLS-1$
@@ -1687,8 +1692,14 @@
 		}
 	}
 
+	private enum Option {
+		WAIT_FOR_DONE,
+		NONE;
+	}
+
 	private ObjectId processHaveLines(List<ObjectId> peerHas, ObjectId last,
-			PacketLineOut out, PackStatistics.Accumulator accumulator)
+			PacketLineOut out, PackStatistics.Accumulator accumulator,
+			Option option)
 			throws IOException {
 		preUploadHook.onBeginNegotiateRound(this, wantIds, peerHas.size());
 		if (wantAll.isEmpty() && !wantIds.isEmpty())
@@ -1754,6 +1765,18 @@
 		// create a pack at this point, let the client know so it stops
 		// telling us about its history.
 		//
+		if (option != Option.WAIT_FOR_DONE) {
+			sentReady = shouldGiveUp(peerHas, out, missCnt);
+		}
+
+		preUploadHook.onEndNegotiateRound(this, wantAll, haveCnt, missCnt, sentReady);
+		peerHas.clear();
+		return last;
+	}
+
+	private boolean shouldGiveUp(List<ObjectId> peerHas, PacketLineOut out, int missCnt)
+			throws IOException {
+		boolean sentReady = false;
 		boolean didOkToGiveUp = false;
 		if (0 < missCnt) {
 			for (int i = peerHas.size() - 1; i >= 0; i--) {
@@ -1765,10 +1788,12 @@
 						case OFF:
 							break;
 						case CONTINUE:
-							out.writeString("ACK " + id.name() + " continue\n"); //$NON-NLS-1$ //$NON-NLS-2$
+							out.writeString(
+									"ACK " + id.name() + " continue\n"); //$NON-NLS-1$ //$NON-NLS-2$
 							break;
 						case DETAILED:
-							out.writeString("ACK " + id.name() + " ready\n"); //$NON-NLS-1$ //$NON-NLS-2$
+							out.writeString(
+									"ACK " + id.name() + " ready\n"); //$NON-NLS-1$ //$NON-NLS-2$
 							sentReady = true;
 							break;
 						}
@@ -1778,15 +1803,14 @@
 			}
 		}
 
-		if (multiAck == MultiAck.DETAILED && !didOkToGiveUp && okToGiveUp()) {
+		if (multiAck == MultiAck.DETAILED && !didOkToGiveUp
+				&& okToGiveUp()) {
 			ObjectId id = peerHas.get(peerHas.size() - 1);
 			out.writeString("ACK " + id.name() + " ready\n"); //$NON-NLS-1$ //$NON-NLS-2$
 			sentReady = true;
 		}
 
-		preUploadHook.onEndNegotiateRound(this, wantAll, haveCnt, missCnt, sentReady);
-		peerHas.clear();
-		return last;
+		return sentReady;
 	}
 
 	private void parseWants(PackStatistics.Accumulator accumulator) throws IOException {