]> source.dussan.org Git - jgit.git/commitdiff
Client-side protocol V2 support for fetching 95/172595/9
authorThomas Wolf <thomas.wolf@paranor.ch>
Sun, 2 Aug 2020 17:22:05 +0000 (19:22 +0200)
committerMatthias Sohn <matthias.sohn@sap.com>
Fri, 1 Jan 2021 20:22:30 +0000 (21:22 +0100)
Make all transports request protocol V2 when fetching. Depending on
the transport, set the GIT_PROTOCOL environment variable (file and
ssh), pass the Git-Protocol header (http), or set the hidden
"\0version=2\0" (git anon). We'll fall back to V0 if the server
doesn't reply with a version 2 answer.

A user can control which protocol the client requests via the git
config protocol.version; if not set, JGit requests protocol V2 for
fetching. Pushing always uses protocol V0 still.

In the API, there is only a new Transport.openFetch() version that
takes a collection of RefSpecs plus additional patterns to construct
the Ref prefixes for the "ls-refs" command in protocol V2. If none
are given, the server will still advertise all refs, even in protocol
V2.

BasePackConnection.readAdvertisedRefs() handles falling back to
protocol V0. It newly returns true if V0 was used and the advertised
refs were read, and false if V2 is used and an explicit "ls-refs" is
needed. (This can't be done transparently inside readAdvertisedRefs()
because a "stateless RPC" transport like TransportHttp may need to
open a new connection for writing.)

BasePackFetchConnection implements the changes needed for the protocol
V2 "fetch" command (stateless protocol, simplified ACK handling,
delimiters, section headers).

In TransportHttp, change readSmartHeaders() to also recognize the
"version 2" packet line as a valid smart server indication.

Adapt tests, and run all the HTTP tests not only with both HTTP
connection factories (JDK and Apache HttpClient) but also with both
protocol V0 and V2. The SSH tests are much slower and much more
focused on the SSH protocol and SSH key handling. Factor out two
very simple cloning and pulling tests and make those run with
protocol V2.

Bug: 553083
Change-Id: I357c7f5daa7efb2872f1c64ee6f6d54229031ae1
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
33 files changed:
org.eclipse.jgit.http.test/build.properties
org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/AllProtocolsHttpTestCase.java [new file with mode: 0644]
org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java
org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientSmartServerTest.java
org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerSslTest.java
org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshBasicTestBase.java [new file with mode: 0644]
org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshTestBase.java
org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshProtocol2Test.java [new file with mode: 0644]
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java
org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/JSchSshProtocol2Test.java [new file with mode: 0644]
org.eclipse.jgit.ssh.jsch/src/org/eclipse/jgit/transport/JschSession.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BasePackConnectionTest.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PacketLineInTest.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransferConfigTest.java [new file with mode: 0644]
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineIn.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/PacketLineOut.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession2.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitAnon.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportGitSsh.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportLocal.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java

index e8bacac9ac4134e30ddf9e460fc7d871c2a57745..a909f1301f26e5c618c177207a726ed8b6d6156d 100644 (file)
@@ -4,3 +4,5 @@ output.. = bin/
 bin.includes = META-INF/,\
                .,\
                plugin.properties
+additional.bundles = org.apache.log4j,\
+                     org.slf4j.binding.log4j12
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/AllProtocolsHttpTestCase.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/AllProtocolsHttpTestCase.java
new file mode 100644 (file)
index 0000000..c6931ad
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2020, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.http.test;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.jgit.junit.http.HttpTestCase;
+import org.eclipse.jgit.transport.HttpTransport;
+import org.eclipse.jgit.transport.http.HttpConnectionFactory;
+import org.eclipse.jgit.transport.http.JDKHttpConnectionFactory;
+import org.eclipse.jgit.transport.http.apache.HttpClientConnectionFactory;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Abstract test base class for running HTTP-related tests with all connection
+ * factories provided in JGit and with both protocol V0 and V2.
+ */
+@Ignore
+@RunWith(Parameterized.class)
+public abstract class AllProtocolsHttpTestCase extends HttpTestCase {
+
+       protected static class TestParameters {
+
+               public final HttpConnectionFactory factory;
+
+               public final boolean enableProtocolV2;
+
+               public TestParameters(HttpConnectionFactory factory,
+                               boolean enableProtocolV2) {
+                       this.factory = factory;
+                       this.enableProtocolV2 = enableProtocolV2;
+               }
+
+               @Override
+               public String toString() {
+                       return factory.toString() + " protocol "
+                                       + (enableProtocolV2 ? "V2" : "V0");
+               }
+       }
+
+       @Parameters(name = "{0}")
+       public static Collection<TestParameters> data() {
+               // run all tests with both connection factories we have
+               HttpConnectionFactory[] factories = new HttpConnectionFactory[] {
+                               new JDKHttpConnectionFactory() {
+
+                                       @Override
+                                       public String toString() {
+                                               return this.getClass().getSuperclass().getName();
+                                       }
+                               }, new HttpClientConnectionFactory() {
+
+                                       @Override
+                                       public String toString() {
+                                               return this.getClass().getSuperclass().getName();
+                                       }
+                               } };
+               List<TestParameters> result = new ArrayList<>();
+               for (HttpConnectionFactory factory : factories) {
+                       result.add(new TestParameters(factory, false));
+                       result.add(new TestParameters(factory, true));
+               }
+               return result;
+       }
+
+       protected final boolean enableProtocolV2;
+
+       protected AllProtocolsHttpTestCase(TestParameters params) {
+               HttpTransport.setConnectionFactory(params.factory);
+               enableProtocolV2 = params.enableProtocolV2;
+       }
+
+       private static HttpConnectionFactory originalFactory;
+
+       @BeforeClass
+       public static void saveConnectionFactory() {
+               originalFactory = HttpTransport.getConnectionFactory();
+       }
+
+       @AfterClass
+       public static void restoreConnectionFactory() {
+               HttpTransport.setConnectionFactory(originalFactory);
+       }
+
+}
index 6da5f86b3e38915dca4d20eb4d259ba178bd9c4c..8b28c4292c9394426decc4bc3e272ad2bfe7d504 100644 (file)
@@ -35,6 +35,7 @@ import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.FetchConnection;
@@ -77,6 +78,9 @@ public class DumbClientDumbServerTest extends AllFactoriesHttpTestCase {
 
                remoteRepository = src.getRepository();
                remoteURI = toURIish(app, srcGit.getName());
+               StoredConfig cfg = remoteRepository.getConfig();
+               cfg.setInt("protocol", null, "version", 0);
+               cfg.save();
 
                A_txt = src.blob("A");
                A = src.commit().add("A_txt", A_txt).create();
index ccde1fe55c50ac351ae9dbb0678486f00e315b97..986b5ca92bcf9ad04863600a8d513320ad60fb16 100644 (file)
@@ -35,6 +35,7 @@ import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.FetchConnection;
@@ -42,11 +43,10 @@ import org.eclipse.jgit.transport.HttpTransport;
 import org.eclipse.jgit.transport.Transport;
 import org.eclipse.jgit.transport.TransportHttp;
 import org.eclipse.jgit.transport.URIish;
-import org.eclipse.jgit.transport.http.HttpConnectionFactory;
 import org.junit.Before;
 import org.junit.Test;
 
-public class DumbClientSmartServerTest extends AllFactoriesHttpTestCase {
+public class DumbClientSmartServerTest extends AllProtocolsHttpTestCase {
        private Repository remoteRepository;
 
        private URIish remoteURI;
@@ -55,8 +55,8 @@ public class DumbClientSmartServerTest extends AllFactoriesHttpTestCase {
 
        private RevCommit A, B;
 
-       public DumbClientSmartServerTest(HttpConnectionFactory cf) {
-               super(cf);
+       public DumbClientSmartServerTest(TestParameters params) {
+               super(params);
        }
 
        @Override
@@ -76,6 +76,9 @@ public class DumbClientSmartServerTest extends AllFactoriesHttpTestCase {
 
                remoteRepository = src.getRepository();
                remoteURI = toURIish(app, srcName);
+               StoredConfig cfg = remoteRepository.getConfig();
+               cfg.setInt("protocol", null, "version", enableProtocolV2 ? 2 : 0);
+               cfg.save();
 
                A_txt = src.blob("A");
                A = src.commit().add("A_txt", A_txt).create();
index 597fb2e507c531283e666b45593f4a0c97d5a8dc..7bc50cad89c3042436217e58e70a43fe5b01d3d3 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2017, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -41,6 +41,7 @@ import org.eclipse.jgit.junit.http.AppServer;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.CredentialItem;
@@ -48,7 +49,6 @@ import org.eclipse.jgit.transport.CredentialsProvider;
 import org.eclipse.jgit.transport.Transport;
 import org.eclipse.jgit.transport.URIish;
 import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
-import org.eclipse.jgit.transport.http.HttpConnectionFactory;
 import org.eclipse.jgit.util.HttpSupport;
 import org.junit.Before;
 import org.junit.Test;
@@ -56,7 +56,7 @@ import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
 @RunWith(Parameterized.class)
-public class SmartClientSmartServerSslTest extends AllFactoriesHttpTestCase {
+public class SmartClientSmartServerSslTest extends AllProtocolsHttpTestCase {
 
        // We run these tests with a server on localhost with a self-signed
        // certificate. We don't do authentication tests here, so there's no need
@@ -112,8 +112,8 @@ public class SmartClientSmartServerSslTest extends AllFactoriesHttpTestCase {
 
        private RevCommit A, B;
 
-       public SmartClientSmartServerSslTest(HttpConnectionFactory cf) {
-               super(cf);
+       public SmartClientSmartServerSslTest(TestParameters params) {
+               super(params);
        }
 
        @Override
@@ -128,10 +128,11 @@ public class SmartClientSmartServerSslTest extends AllFactoriesHttpTestCase {
 
                final TestRepository<Repository> src = createTestRepository();
                final String srcName = src.getRepository().getDirectory().getName();
-               src.getRepository()
-                               .getConfig()
-                               .setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
-                                               ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
+               StoredConfig cfg = src.getRepository().getConfig();
+               cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+                               ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
+               cfg.setInt("protocol", null, "version", enableProtocolV2 ? 2 : 0);
+               cfg.save();
 
                GitServlet gs = new GitServlet();
 
@@ -238,7 +239,7 @@ public class SmartClientSmartServerSslTest extends AllFactoriesHttpTestCase {
                fsck(dst, B);
 
                List<AccessEvent> requests = getRequests();
-               assertEquals(2, requests.size());
+               assertEquals(enableProtocolV2 ? 3 : 2, requests.size());
        }
 
        @Test
@@ -256,7 +257,7 @@ public class SmartClientSmartServerSslTest extends AllFactoriesHttpTestCase {
                fsck(dst, B);
 
                List<AccessEvent> requests = getRequests();
-               assertEquals(3, requests.size());
+               assertEquals(enableProtocolV2 ? 4 : 3, requests.size());
        }
 
        @Test
index 8d1870a87eacf84b51d59bf458a1d1fcb0894db1..f3e56f85d35480e08f797709d8bfd22ce97f5c0e 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010, 2017 Google Inc. and others
+ * Copyright (C) 2010, 2020 Google Inc. and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -86,13 +86,12 @@ import org.eclipse.jgit.transport.TransportHttp;
 import org.eclipse.jgit.transport.URIish;
 import org.eclipse.jgit.transport.UploadPack;
 import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
-import org.eclipse.jgit.transport.http.HttpConnectionFactory;
 import org.eclipse.jgit.util.HttpSupport;
 import org.eclipse.jgit.util.SystemReader;
 import org.junit.Before;
 import org.junit.Test;
 
-public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
+public class SmartClientSmartServerTest extends AllProtocolsHttpTestCase {
        private static final String HDR_TRANSFER_ENCODING = "Transfer-Encoding";
 
        private AdvertiseRefsHook advertiseRefsHook;
@@ -120,8 +119,8 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
 
        private RevCommit A, B, unreachableCommit;
 
-       public SmartClientSmartServerTest(HttpConnectionFactory cf) {
-               super(cf);
+       public SmartClientSmartServerTest(TestParameters params) {
+               super(params);
        }
 
        @Override
@@ -131,10 +130,11 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
 
                final TestRepository<Repository> src = createTestRepository();
                final String srcName = src.getRepository().getDirectory().getName();
-               src.getRepository()
-                               .getConfig()
-                               .setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
-                                               ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
+               StoredConfig cfg = src.getRepository().getConfig();
+               cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+                               ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
+               cfg.setInt("protocol", null, "version", enableProtocolV2 ? 2 : 0);
+               cfg.save();
 
                GitServlet gs = new GitServlet();
                gs.setUploadPackFactory((HttpServletRequest req, Repository db) -> {
@@ -448,7 +448,7 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                assertEquals(B, map.get(Constants.HEAD).getObjectId());
 
                List<AccessEvent> requests = getRequests();
-               assertEquals(1, requests.size());
+               assertEquals(enableProtocolV2 ? 2 : 1, requests.size());
 
                AccessEvent info = requests.get(0);
                assertEquals("GET", info.getMethod());
@@ -458,7 +458,22 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                assertEquals(200, info.getStatus());
                assertEquals("application/x-git-upload-pack-advertisement", info
                                .getResponseHeader(HDR_CONTENT_TYPE));
-               assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+               if (!enableProtocolV2) {
+                       assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+               } else {
+                       AccessEvent lsRefs = requests.get(1);
+                       assertEquals("POST", lsRefs.getMethod());
+                       assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath());
+                       assertEquals(0, lsRefs.getParameters().size());
+                       assertNotNull("has content-length",
+                                       lsRefs.getRequestHeader(HDR_CONTENT_LENGTH));
+                       assertNull("not chunked",
+                                       lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING));
+                       assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol"));
+                       assertEquals(200, lsRefs.getStatus());
+                       assertEquals("application/x-git-upload-pack-result",
+                                       lsRefs.getResponseHeader(HDR_CONTENT_TYPE));
+               }
        }
 
        @Test
@@ -576,9 +591,10 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                }
 
                List<AccessEvent> requests = getRequests();
-               assertEquals(2, requests.size());
+               assertEquals(enableProtocolV2 ? 3 : 2, requests.size());
 
-               AccessEvent info = requests.get(0);
+               int requestNumber = 0;
+               AccessEvent info = requests.get(requestNumber++);
                assertEquals("GET", info.getMethod());
                assertEquals(join(remoteURI, "info/refs"), info.getPath());
                assertEquals(1, info.getParameters().size());
@@ -586,9 +602,24 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                assertEquals(200, info.getStatus());
                assertEquals("application/x-git-upload-pack-advertisement", info
                                .getResponseHeader(HDR_CONTENT_TYPE));
-               assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+               if (!enableProtocolV2) {
+                       assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+               } else {
+                       AccessEvent lsRefs = requests.get(requestNumber++);
+                       assertEquals("POST", lsRefs.getMethod());
+                       assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath());
+                       assertEquals(0, lsRefs.getParameters().size());
+                       assertNotNull("has content-length",
+                                       lsRefs.getRequestHeader(HDR_CONTENT_LENGTH));
+                       assertNull("not chunked",
+                                       lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING));
+                       assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol"));
+                       assertEquals(200, lsRefs.getStatus());
+                       assertEquals("application/x-git-upload-pack-result",
+                                       lsRefs.getResponseHeader(HDR_CONTENT_TYPE));
+               }
 
-               AccessEvent service = requests.get(1);
+               AccessEvent service = requests.get(requestNumber);
                assertEquals("POST", service.getMethod());
                assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
                assertEquals(0, service.getParameters().size());
@@ -628,7 +659,8 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                }
 
                List<AccessEvent> requests = getRequests();
-               assertEquals(2 + nofRedirects, requests.size());
+               assertEquals((enableProtocolV2 ? 3 : 2) + nofRedirects,
+                               requests.size());
 
                int n = 0;
                while (n < nofRedirects) {
@@ -644,7 +676,22 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                assertEquals(200, info.getStatus());
                assertEquals("application/x-git-upload-pack-advertisement",
                                info.getResponseHeader(HDR_CONTENT_TYPE));
-               assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+               if (!enableProtocolV2) {
+                       assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+               } else {
+                       AccessEvent lsRefs = requests.get(n++);
+                       assertEquals("POST", lsRefs.getMethod());
+                       assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath());
+                       assertEquals(0, lsRefs.getParameters().size());
+                       assertNotNull("has content-length",
+                                       lsRefs.getRequestHeader(HDR_CONTENT_LENGTH));
+                       assertNull("not chunked",
+                                       lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING));
+                       assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol"));
+                       assertEquals(200, lsRefs.getStatus());
+                       assertEquals("application/x-git-upload-pack-result",
+                                       lsRefs.getResponseHeader(HDR_CONTENT_TYPE));
+               }
 
                AccessEvent service = requests.get(n++);
                assertEquals("POST", service.getMethod());
@@ -756,7 +803,7 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                }
 
                List<AccessEvent> requests = getRequests();
-               assertEquals(3, requests.size());
+               assertEquals(enableProtocolV2 ? 4 : 3, requests.size());
 
                AccessEvent info = requests.get(0);
                assertEquals("GET", info.getMethod());
@@ -766,24 +813,27 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                assertEquals(200, info.getStatus());
                assertEquals("application/x-git-upload-pack-advertisement",
                                info.getResponseHeader(HDR_CONTENT_TYPE));
-               assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+               if (!enableProtocolV2) {
+                       assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+               }
 
                AccessEvent redirect = requests.get(1);
                assertEquals("POST", redirect.getMethod());
                assertEquals(301, redirect.getStatus());
 
-               AccessEvent service = requests.get(2);
-               assertEquals("POST", service.getMethod());
-               assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
-               assertEquals(0, service.getParameters().size());
-               assertNotNull("has content-length",
-                               service.getRequestHeader(HDR_CONTENT_LENGTH));
-               assertNull("not chunked",
-                               service.getRequestHeader(HDR_TRANSFER_ENCODING));
-
-               assertEquals(200, service.getStatus());
-               assertEquals("application/x-git-upload-pack-result",
-                               service.getResponseHeader(HDR_CONTENT_TYPE));
+               for (int i = 2; i < requests.size(); i++) {
+                       AccessEvent service = requests.get(i);
+                       assertEquals("POST", service.getMethod());
+                       assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
+                       assertEquals(0, service.getParameters().size());
+                       assertNotNull("has content-length",
+                                       service.getRequestHeader(HDR_CONTENT_LENGTH));
+                       assertNull("not chunked",
+                                       service.getRequestHeader(HDR_TRANSFER_ENCODING));
+                       assertEquals(200, service.getStatus());
+                       assertEquals("application/x-git-upload-pack-result",
+                                       service.getResponseHeader(HDR_CONTENT_TYPE));
+               }
        }
 
        @Test
@@ -830,7 +880,7 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                }
 
                List<AccessEvent> requests = getRequests();
-               assertEquals(3, requests.size());
+               assertEquals(enableProtocolV2 ? 4 : 3, requests.size());
 
                AccessEvent info = requests.get(0);
                assertEquals("GET", info.getMethod());
@@ -844,20 +894,24 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                assertEquals(200, info.getStatus());
                assertEquals("application/x-git-upload-pack-advertisement",
                                info.getResponseHeader(HDR_CONTENT_TYPE));
-               assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
-
-               AccessEvent service = requests.get(2);
-               assertEquals("POST", service.getMethod());
-               assertEquals(join(authURI, "git-upload-pack"), service.getPath());
-               assertEquals(0, service.getParameters().size());
-               assertNotNull("has content-length",
-                               service.getRequestHeader(HDR_CONTENT_LENGTH));
-               assertNull("not chunked",
-                               service.getRequestHeader(HDR_TRANSFER_ENCODING));
+               if (!enableProtocolV2) {
+                       assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+               }
 
-               assertEquals(200, service.getStatus());
-               assertEquals("application/x-git-upload-pack-result",
-                               service.getResponseHeader(HDR_CONTENT_TYPE));
+               for (int i = 2; i < requests.size(); i++) {
+                       AccessEvent service = requests.get(i);
+                       assertEquals("POST", service.getMethod());
+                       assertEquals(join(authURI, "git-upload-pack"), service.getPath());
+                       assertEquals(0, service.getParameters().size());
+                       assertNotNull("has content-length",
+                                       service.getRequestHeader(HDR_CONTENT_LENGTH));
+                       assertNull("not chunked",
+                                       service.getRequestHeader(HDR_TRANSFER_ENCODING));
+
+                       assertEquals(200, service.getStatus());
+                       assertEquals("application/x-git-upload-pack-result",
+                                       service.getResponseHeader(HDR_CONTENT_TYPE));
+               }
        }
 
        @Test
@@ -937,19 +991,20 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                }
 
                List<AccessEvent> requests = getRequests();
-               assertEquals(4, requests.size());
+               assertEquals(enableProtocolV2 ? 5 : 4, requests.size());
 
-               AccessEvent redirect = requests.get(0);
+               int requestNumber = 0;
+               AccessEvent redirect = requests.get(requestNumber++);
                assertEquals("GET", redirect.getMethod());
                assertEquals(join(cloneFrom, "info/refs"), redirect.getPath());
                assertEquals(301, redirect.getStatus());
 
-               AccessEvent info = requests.get(1);
+               AccessEvent info = requests.get(requestNumber++);
                assertEquals("GET", info.getMethod());
                assertEquals(join(authURI, "info/refs"), info.getPath());
                assertEquals(401, info.getStatus());
 
-               info = requests.get(2);
+               info = requests.get(requestNumber++);
                assertEquals("GET", info.getMethod());
                assertEquals(join(authURI, "info/refs"), info.getPath());
                assertEquals(1, info.getParameters().size());
@@ -957,9 +1012,24 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                assertEquals(200, info.getStatus());
                assertEquals("application/x-git-upload-pack-advertisement",
                                info.getResponseHeader(HDR_CONTENT_TYPE));
-               assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+               if (!enableProtocolV2) {
+                       assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+               } else {
+                       AccessEvent lsRefs = requests.get(requestNumber++);
+                       assertEquals("POST", lsRefs.getMethod());
+                       assertEquals(join(authURI, "git-upload-pack"), lsRefs.getPath());
+                       assertEquals(0, lsRefs.getParameters().size());
+                       assertNotNull("has content-length",
+                                       lsRefs.getRequestHeader(HDR_CONTENT_LENGTH));
+                       assertNull("not chunked",
+                                       lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING));
+                       assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol"));
+                       assertEquals(200, lsRefs.getStatus());
+                       assertEquals("application/x-git-upload-pack-result",
+                                       lsRefs.getResponseHeader(HDR_CONTENT_TYPE));
+               }
 
-               AccessEvent service = requests.get(3);
+               AccessEvent service = requests.get(requestNumber);
                assertEquals("POST", service.getMethod());
                assertEquals(join(authURI, "git-upload-pack"), service.getPath());
                assertEquals(0, service.getParameters().size());
@@ -987,7 +1057,7 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                }
 
                List<AccessEvent> requests = getRequests();
-               assertEquals(3, requests.size());
+               assertEquals(enableProtocolV2 ? 4 : 3, requests.size());
 
                AccessEvent info = requests.get(0);
                assertEquals("GET", info.getMethod());
@@ -997,25 +1067,30 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                assertEquals(200, info.getStatus());
                assertEquals("application/x-git-upload-pack-advertisement",
                                info.getResponseHeader(HDR_CONTENT_TYPE));
-               assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+               if (!enableProtocolV2) {
+                       assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+               }
 
                AccessEvent service = requests.get(1);
                assertEquals("POST", service.getMethod());
                assertEquals(join(authOnPostURI, "git-upload-pack"), service.getPath());
                assertEquals(401, service.getStatus());
 
-               service = requests.get(2);
-               assertEquals("POST", service.getMethod());
-               assertEquals(join(authOnPostURI, "git-upload-pack"), service.getPath());
-               assertEquals(0, service.getParameters().size());
-               assertNotNull("has content-length",
-                               service.getRequestHeader(HDR_CONTENT_LENGTH));
-               assertNull("not chunked",
-                               service.getRequestHeader(HDR_TRANSFER_ENCODING));
-
-               assertEquals(200, service.getStatus());
-               assertEquals("application/x-git-upload-pack-result",
-                               service.getResponseHeader(HDR_CONTENT_TYPE));
+               for (int i = 2; i < requests.size(); i++) {
+                       service = requests.get(i);
+                       assertEquals("POST", service.getMethod());
+                       assertEquals(join(authOnPostURI, "git-upload-pack"),
+                                       service.getPath());
+                       assertEquals(0, service.getParameters().size());
+                       assertNotNull("has content-length",
+                                       service.getRequestHeader(HDR_CONTENT_LENGTH));
+                       assertNull("not chunked",
+                                       service.getRequestHeader(HDR_TRANSFER_ENCODING));
+
+                       assertEquals(200, service.getStatus());
+                       assertEquals("application/x-git-upload-pack-result",
+                                       service.getResponseHeader(HDR_CONTENT_TYPE));
+               }
        }
 
        @Test
@@ -1052,9 +1127,11 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
 
                List<AccessEvent> requests = getRequests();
                requests.removeAll(cloneRequests);
-               assertEquals(2, requests.size());
 
-               AccessEvent info = requests.get(0);
+               assertEquals(enableProtocolV2 ? 3 : 2, requests.size());
+
+               int requestNumber = 0;
+               AccessEvent info = requests.get(requestNumber++);
                assertEquals("GET", info.getMethod());
                assertEquals(join(remoteURI, "info/refs"), info.getPath());
                assertEquals(1, info.getParameters().size());
@@ -1063,9 +1140,24 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                assertEquals("application/x-git-upload-pack-advertisement",
                                info.getResponseHeader(HDR_CONTENT_TYPE));
 
+               if (enableProtocolV2) {
+                       AccessEvent lsRefs = requests.get(requestNumber++);
+                       assertEquals("POST", lsRefs.getMethod());
+                       assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath());
+                       assertEquals(0, lsRefs.getParameters().size());
+                       assertNotNull("has content-length",
+                                       lsRefs.getRequestHeader(HDR_CONTENT_LENGTH));
+                       assertNull("not chunked",
+                                       lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING));
+                       assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol"));
+                       assertEquals(200, lsRefs.getStatus());
+                       assertEquals("application/x-git-upload-pack-result",
+                                       lsRefs.getResponseHeader(HDR_CONTENT_TYPE));
+               }
+
                // We should have needed one request to perform the fetch.
                //
-               AccessEvent service = requests.get(1);
+               AccessEvent service = requests.get(requestNumber);
                assertEquals("POST", service.getMethod());
                assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
                assertEquals(0, service.getParameters().size());
@@ -1116,9 +1208,10 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
 
                List<AccessEvent> requests = getRequests();
                requests.removeAll(cloneRequests);
-               assertEquals(3, requests.size());
+               assertEquals(enableProtocolV2 ? 4 : 3, requests.size());
 
-               AccessEvent info = requests.get(0);
+               int requestNumber = 0;
+               AccessEvent info = requests.get(requestNumber++);
                assertEquals("GET", info.getMethod());
                assertEquals(join(remoteURI, "info/refs"), info.getPath());
                assertEquals(1, info.getParameters().size());
@@ -1127,10 +1220,25 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                assertEquals("application/x-git-upload-pack-advertisement", info
                                .getResponseHeader(HDR_CONTENT_TYPE));
 
+               if (enableProtocolV2) {
+                       AccessEvent lsRefs = requests.get(requestNumber++);
+                       assertEquals("POST", lsRefs.getMethod());
+                       assertEquals(join(remoteURI, "git-upload-pack"), lsRefs.getPath());
+                       assertEquals(0, lsRefs.getParameters().size());
+                       assertNotNull("has content-length",
+                                       lsRefs.getRequestHeader(HDR_CONTENT_LENGTH));
+                       assertNull("not chunked",
+                                       lsRefs.getRequestHeader(HDR_TRANSFER_ENCODING));
+                       assertEquals("version=2", lsRefs.getRequestHeader("Git-Protocol"));
+                       assertEquals(200, lsRefs.getStatus());
+                       assertEquals("application/x-git-upload-pack-result",
+                                       lsRefs.getResponseHeader(HDR_CONTENT_TYPE));
+               }
+
                // We should have needed two requests to perform the fetch
                // due to the high number of local unknown commits.
                //
-               AccessEvent service = requests.get(1);
+               AccessEvent service = requests.get(requestNumber++);
                assertEquals("POST", service.getMethod());
                assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
                assertEquals(0, service.getParameters().size());
@@ -1143,7 +1251,7 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                assertEquals("application/x-git-upload-pack-result", service
                                .getResponseHeader(HDR_CONTENT_TYPE));
 
-               service = requests.get(2);
+               service = requests.get(requestNumber);
                assertEquals("POST", service.getMethod());
                assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
                assertEquals(0, service.getParameters().size());
@@ -1211,7 +1319,8 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                                        Collections.<ObjectId> emptySet());
                        fail("Server accepted want " + id.name());
                } catch (TransportException err) {
-                       assertEquals("want " + id.name() + " not valid", err.getMessage());
+                       assertTrue(err.getMessage()
+                                       .contains("want " + id.name() + " not valid"));
                }
        }
 
@@ -1429,5 +1538,4 @@ public class SmartClientSmartServerTest extends AllFactoriesHttpTestCase {
                cfg.setBoolean("http", null, "receivepack", true);
                cfg.save();
        }
-
 }
diff --git a/org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshBasicTestBase.java b/org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshBasicTestBase.java
new file mode 100644 (file)
index 0000000..f9ca0b8
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.junit.ssh;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+
+import org.eclipse.jgit.api.Git;
+import org.junit.Test;
+
+/**
+ * Some minimal cloning and fetching tests. Concrete subclasses can implement
+ * the abstract operations from {@link SshTestHarness} to run with different SSH
+ * implementations.
+ */
+public abstract class SshBasicTestBase extends SshTestHarness {
+
+       protected File defaultCloneDir;
+
+       @Override
+       public void setUp() throws Exception {
+               super.setUp();
+               defaultCloneDir = new File(getTemporaryDirectory(), "cloned");
+       }
+
+       @Test
+       public void testSshCloneWithConfig() throws Exception {
+               cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, null, //
+                               "Host localhost", //
+                               "HostName localhost", //
+                               "Port " + testPort, //
+                               "User " + TEST_USER, //
+                               "IdentityFile " + privateKey1.getAbsolutePath());
+       }
+
+       @Test
+       public void testSshFetchWithConfig() throws Exception {
+               File localClone = cloneWith("ssh://localhost/doesntmatter",
+                               defaultCloneDir, null, //
+                               "Host localhost", //
+                               "HostName localhost", //
+                               "Port " + testPort, //
+                               "User " + TEST_USER, //
+                               "IdentityFile " + privateKey1.getAbsolutePath());
+               // Do a commit in the upstream repo
+               try (Git git = new Git(db)) {
+                       writeTrashFile("SomeOtherFile.txt", "Other commit");
+                       git.add().addFilepattern("SomeOtherFile.txt").call();
+                       git.commit().setMessage("New commit").call();
+               }
+               // Pull in the clone
+               try (Git git = Git.open(localClone)) {
+                       File f = new File(git.getRepository().getWorkTree(),
+                                       "SomeOtherFile.txt");
+                       assertFalse(f.exists());
+                       git.pull().setRemote("origin").call();
+                       assertTrue(f.exists());
+                       assertEquals("Other commit", read(f));
+               }
+       }
+}
index 3784741195e8301c964582f3f38fc9f7372e0335..6fa82f1d681de0466b8b4c510606e87ba23364ea 100644 (file)
@@ -40,7 +40,7 @@ import org.junit.experimental.theories.Theory;
  * abstract operations from {@link SshTestHarness}. This gives a way to test
  * different ssh clients against a unified test suite.
  */
-public abstract class SshTestBase extends SshTestHarness {
+public abstract class SshTestBase extends SshBasicTestBase {
 
        @DataPoints
        public static String[] KEY_RESOURCES = { //
@@ -65,14 +65,6 @@ public abstract class SshTestBase extends SshTestHarness {
                        "id_ed25519_testpass", //
                        "id_ed25519_expensive_testpass" };
 
-       protected File defaultCloneDir;
-
-       @Override
-       public void setUp() throws Exception {
-               super.setUp();
-               defaultCloneDir = new File(getTemporaryDirectory(), "cloned");
-       }
-
        @Test
        public void testSshWithoutConfig() throws Exception {
                assertThrows(TransportException.class,
@@ -132,16 +124,6 @@ public abstract class SshTestBase extends SshTestHarness {
                                + "/doesntmatter", defaultCloneDir, null);
        }
 
-       @Test
-       public void testSshWithConfig() throws Exception {
-               cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, null, //
-                               "Host localhost", //
-                               "HostName localhost", //
-                               "Port " + testPort, //
-                               "User " + TEST_USER, //
-                               "IdentityFile " + privateKey1.getAbsolutePath());
-       }
-
        @Test
        public void testSshWithConfigEncryptedUnusedKey() throws Exception {
                // Copy the encrypted test key from the bundle.
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshProtocol2Test.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshProtocol2Test.java
new file mode 100644 (file)
index 0000000..0ad96b9
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.transport.sshd;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.util.Arrays;
+
+import org.eclipse.jgit.junit.ssh.SshBasicTestBase;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.transport.SshSessionFactory;
+import org.eclipse.jgit.util.FS;
+
+public class ApacheSshProtocol2Test extends SshBasicTestBase {
+
+       @Override
+       protected SshSessionFactory createSessionFactory() {
+               SshdSessionFactory result = new SshdSessionFactory(new JGitKeyCache(),
+                               null);
+               // The home directory is mocked at this point!
+               result.setHomeDirectory(FS.DETECTED.userHome());
+               result.setSshDirectory(sshDir);
+               return result;
+       }
+
+       @Override
+       protected void installConfig(String... config) {
+               File configFile = new File(sshDir, Constants.CONFIG);
+               if (config != null) {
+                       try {
+                               Files.write(configFile.toPath(), Arrays.asList(config));
+                       } catch (IOException e) {
+                               throw new UncheckedIOException(e);
+                       }
+               }
+       }
+
+       @Override
+       public void setUp() throws Exception {
+               super.setUp();
+               StoredConfig config = ((Repository) db).getConfig();
+               config.setInt("protocol", null, "version", 2);
+               config.save();
+       }
+}
index 0fb0610b991243bd5ed84b849d8b0898ecb82680..5a50cc8f27e5cf094d357342a92ff018dc9baa55 100644 (file)
@@ -24,6 +24,7 @@ import java.util.Collections;
 import java.util.EnumSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
@@ -53,7 +54,7 @@ import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
 import org.eclipse.jgit.internal.transport.sshd.SshdText;
 import org.eclipse.jgit.transport.FtpChannel;
-import org.eclipse.jgit.transport.RemoteSession;
+import org.eclipse.jgit.transport.RemoteSession2;
 import org.eclipse.jgit.transport.SshConstants;
 import org.eclipse.jgit.transport.URIish;
 import org.eclipse.jgit.util.StringUtils;
@@ -61,11 +62,12 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * An implementation of {@link RemoteSession} based on Apache MINA sshd.
+ * An implementation of {@link org.eclipse.jgit.transport.RemoteSession
+ * RemoteSession} based on Apache MINA sshd.
  *
  * @since 5.2
  */
-public class SshdSession implements RemoteSession {
+public class SshdSession implements RemoteSession2 {
 
        private static final Logger LOG = LoggerFactory
                        .getLogger(SshdSession.class);
@@ -290,8 +292,15 @@ public class SshdSession implements RemoteSession {
 
        @Override
        public Process exec(String commandName, int timeout) throws IOException {
+               return exec(commandName, Collections.emptyMap(), timeout);
+       }
+
+       @Override
+       public Process exec(String commandName, Map<String, String> environment,
+                       int timeout) throws IOException {
                @SuppressWarnings("resource")
-               ChannelExec exec = session.createExecChannel(commandName);
+               ChannelExec exec = session.createExecChannel(commandName, null,
+                               environment);
                if (timeout <= 0) {
                        try {
                                exec.open().verify();
diff --git a/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/JSchSshProtocol2Test.java b/org.eclipse.jgit.ssh.jsch.test/tst/org/eclipse/jgit/transport/JSchSshProtocol2Test.java
new file mode 100644 (file)
index 0000000..0929c55
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+//TODO(ms): move to org.eclipse.jgit.ssh.jsch in 6.0
+package org.eclipse.jgit.transport;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.util.Arrays;
+
+import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.junit.ssh.SshBasicTestBase;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.transport.OpenSshConfig.Host;
+import org.eclipse.jgit.util.FS;
+
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+
+public class JSchSshProtocol2Test extends SshBasicTestBase {
+
+       private class TestSshSessionFactory extends JschConfigSessionFactory {
+
+               @Override
+               protected void configure(Host hc, Session session) {
+                       // Nothing
+               }
+
+               @Override
+               public synchronized RemoteSession getSession(URIish uri,
+                               CredentialsProvider credentialsProvider, FS fs, int tms)
+                               throws TransportException {
+                       return super.getSession(uri, credentialsProvider, fs, tms);
+               }
+
+               @Override
+               protected JSch createDefaultJSch(FS fs) throws JSchException {
+                       JSch defaultJSch = super.createDefaultJSch(fs);
+                       if (knownHosts.exists()) {
+                               defaultJSch.setKnownHosts(knownHosts.getAbsolutePath());
+                       }
+                       return defaultJSch;
+               }
+       }
+
+       @Override
+       protected SshSessionFactory createSessionFactory() {
+               return new TestSshSessionFactory();
+       }
+
+       @Override
+       protected void installConfig(String... config) {
+               SshSessionFactory factory = getSessionFactory();
+               assertTrue(factory instanceof JschConfigSessionFactory);
+               JschConfigSessionFactory j = (JschConfigSessionFactory) factory;
+               try {
+                       j.setConfig(createConfig(config));
+               } catch (IOException e) {
+                       throw new UncheckedIOException(e);
+               }
+       }
+
+       private OpenSshConfig createConfig(String... content) throws IOException {
+               File configFile = new File(sshDir, Constants.CONFIG);
+               if (content != null) {
+                       Files.write(configFile.toPath(), Arrays.asList(content));
+               }
+               return new OpenSshConfig(getTemporaryDirectory(), configFile);
+       }
+
+       @Override
+       public void setUp() throws Exception {
+               super.setUp();
+               StoredConfig config = ((Repository) db).getConfig();
+               config.setInt("protocol", null, "version", 2);
+               config.save();
+       }
+}
index 858bdf3f7f586753cc4bacb6c8d9d68b00e3286e..c7d0941b6298e6dd1f31c85412cea7f2b5d05dee 100644 (file)
@@ -22,7 +22,9 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 
@@ -44,7 +46,7 @@ import com.jcraft.jsch.SftpException;
  * {@link org.eclipse.jgit.transport.JschConfigSessionFactory} is used to create
  * the actual session passed to the constructor.
  */
-public class JschSession implements RemoteSession {
+public class JschSession implements RemoteSession2 {
        final Session sock;
        final URIish uri;
 
@@ -65,7 +67,14 @@ public class JschSession implements RemoteSession {
        /** {@inheritDoc} */
        @Override
        public Process exec(String command, int timeout) throws IOException {
-               return new JschProcess(command, timeout);
+               return exec(command, Collections.emptyMap(), timeout);
+       }
+
+       /** {@inheritDoc} */
+       @Override
+       public Process exec(String command, Map<String, String> environment,
+                       int timeout) throws IOException {
+               return new JschProcess(command, environment, timeout);
        }
 
        /** {@inheritDoc} */
@@ -124,6 +133,8 @@ public class JschSession implements RemoteSession {
                 *
                 * @param commandName
                 *            the command to execute
+                * @param environment
+                *            environment variables to pass on
                 * @param tms
                 *            the timeout value, in seconds, for the command.
                 * @throws TransportException
@@ -132,11 +143,17 @@ public class JschSession implements RemoteSession {
                 * @throws IOException
                 *             on problems opening streams
                 */
-               JschProcess(String commandName, int tms)
-                               throws TransportException, IOException {
+               JschProcess(String commandName, Map<String, String> environment,
+                               int tms) throws TransportException, IOException {
                        timeout = tms;
                        try {
                                channel = (ChannelExec) sock.openChannel("exec"); //$NON-NLS-1$
+                               if (environment != null) {
+                                       for (Map.Entry<String, String> envVar : environment
+                                                       .entrySet()) {
+                                               channel.setEnv(envVar.getKey(), envVar.getValue());
+                                       }
+                               }
                                channel.setCommand(commandName);
                                setupStreams();
                                channel.connect(timeout > 0 ? timeout * 1000 : 0);
index 64b16f659a1abb8a3a30b62e6fc3f20bac5b8156..7d438c1dd84f16fb296914c36893a70eeeedd92f 100644 (file)
@@ -16,11 +16,11 @@ import static org.hamcrest.Matchers.not;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertSame;
 
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
 import org.eclipse.jgit.lib.Ref;
@@ -29,18 +29,6 @@ import org.junit.Test;
 
 public class BasePackConnectionTest {
 
-       @Test
-       public void testExtractSymRefsFromCapabilities() {
-               final Map<String, String> symRefs = BasePackConnection
-                               .extractSymRefsFromCapabilities(
-                                               Arrays.asList("symref=HEAD:refs/heads/main",
-                                                               "symref=refs/heads/sym:refs/heads/other"));
-
-               assertEquals(2, symRefs.size());
-               assertEquals("refs/heads/main", symRefs.get("HEAD"));
-               assertEquals("refs/heads/other", symRefs.get("refs/heads/sym"));
-       }
-
        @Test
        public void testUpdateWithSymRefsAdds() {
                final Ref mainRef = new ObjectIdRef.Unpeeled(Ref.Storage.LOOSE,
@@ -230,4 +218,30 @@ public class BasePackConnectionTest {
                assertThat(refMap, not(hasKey("refs/heads/sym1")));
                assertThat(refMap, not(hasKey("refs/heads/sym2")));
        }
+
+       @Test
+       public void testUpdateWithSymRefsFillInHead() {
+               final String oidName = "0000000000000000000000000000000000000001";
+               final Ref advertised = new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK,
+                               Constants.HEAD, ObjectId.fromString(oidName));
+
+               final Map<String, Ref> refMap = new HashMap<>();
+               refMap.put(advertised.getName(), advertised);
+
+               final Map<String, String> symRefs = new HashMap<>();
+               symRefs.put("HEAD", "refs/heads/main");
+
+               BasePackConnection.updateWithSymRefs(refMap, symRefs);
+
+               assertThat(refMap, hasKey("HEAD"));
+               assertThat(refMap, hasKey("refs/heads/main"));
+               final Ref headRef = refMap.get("HEAD");
+               final Ref mainRef = refMap.get("refs/heads/main");
+               assertThat(headRef, instanceOf(SymbolicRef.class));
+               final SymbolicRef headSymRef = (SymbolicRef) headRef;
+               assertEquals(Constants.HEAD, headSymRef.getName());
+               assertSame(mainRef, headSymRef.getTarget());
+               assertEquals(oidName, headRef.getObjectId().name());
+               assertEquals(oidName, mainRef.getObjectId().name());
+       }
 }
\ No newline at end of file
index 7f03357e9c2173367137367892d2adf1282e7d20..505e0088c420cf055bb1cd83941558d60aaa01ac 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009, Google Inc. and others
+ * Copyright (C) 2009, 2020 Google Inc. and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -13,6 +13,7 @@ package org.eclipse.jgit.transport;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -297,6 +298,58 @@ public class PacketLineInTest {
                }
        }
 
+       // parseACKv2
+
+       @Test
+       public void testParseAckV2_NAK() throws IOException {
+               final ObjectId expid = ObjectId
+                               .fromString("fcfcfb1fd94829c1a1704f894fc111d14770d34e");
+               final MutableObjectId actid = new MutableObjectId();
+               actid.fromString(expid.name());
+
+               assertSame(PacketLineIn.AckNackResult.NAK,
+                               PacketLineIn.parseACKv2("NAK", actid));
+               assertEquals(expid, actid);
+       }
+
+       @Test
+       public void testParseAckV2_ACK() throws IOException {
+               final ObjectId expid = ObjectId
+                               .fromString("fcfcfb1fd94829c1a1704f894fc111d14770d34e");
+               final MutableObjectId actid = new MutableObjectId();
+
+               assertSame(PacketLineIn.AckNackResult.ACK_COMMON,
+                               PacketLineIn.parseACKv2(
+                                               "ACK fcfcfb1fd94829c1a1704f894fc111d14770d34e", actid));
+               assertEquals(expid, actid);
+       }
+
+       @Test
+       public void testParseAckV2_Ready() throws IOException {
+               final ObjectId expid = ObjectId
+                               .fromString("fcfcfb1fd94829c1a1704f894fc111d14770d34e");
+               final MutableObjectId actid = new MutableObjectId();
+               actid.fromString(expid.name());
+
+               assertSame(PacketLineIn.AckNackResult.ACK_READY,
+                               PacketLineIn.parseACKv2("ready", actid));
+               assertEquals(expid, actid);
+       }
+
+       @Test
+       public void testParseAckV2_ERR() {
+               IOException e = assertThrows(IOException.class, () -> PacketLineIn
+                               .parseACKv2("ERR want is not valid", new MutableObjectId()));
+               assertTrue(e.getMessage().contains("want is not valid"));
+       }
+
+       @Test
+       public void testParseAckV2_Invalid() {
+               IOException e = assertThrows(IOException.class,
+                               () -> PacketLineIn.parseACKv2("HELO", new MutableObjectId()));
+               assertTrue(e.getMessage().contains("xpected ACK/NAK"));
+       }
+
        // test support
 
        private void init(String msg) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransferConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransferConfigTest.java
new file mode 100644 (file)
index 0000000..d9b85fb
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.transport;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+/**
+ * Tests for {@link TransferConfig} parsing.
+ */
+public class TransferConfigTest {
+
+       @Test
+       public void testParseProtocolV0() {
+               Config rc = new Config();
+               rc.setInt("protocol", null, "version", 0);
+               TransferConfig tc = new TransferConfig(rc);
+               assertEquals(TransferConfig.ProtocolVersion.V0, tc.protocolVersion);
+       }
+
+       @Test
+       public void testParseProtocolV1() {
+               Config rc = new Config();
+               rc.setInt("protocol", null, "version", 1);
+               TransferConfig tc = new TransferConfig(rc);
+               assertEquals(TransferConfig.ProtocolVersion.V0, tc.protocolVersion);
+       }
+
+       @Test
+       public void testParseProtocolV2() {
+               Config rc = new Config();
+               rc.setInt("protocol", null, "version", 2);
+               TransferConfig tc = new TransferConfig(rc);
+               assertEquals(TransferConfig.ProtocolVersion.V2, tc.protocolVersion);
+       }
+
+       @Test
+       public void testParseProtocolNotSet() {
+               Config rc = new Config();
+               TransferConfig tc = new TransferConfig(rc);
+               assertNull(tc.protocolVersion);
+       }
+
+       @Test
+       public void testParseProtocolUnknown() {
+               Config rc = new Config();
+               rc.setInt("protocol", null, "version", 3);
+               TransferConfig tc = new TransferConfig(rc);
+               assertNull(tc.protocolVersion);
+       }
+
+       @Test
+       public void testParseProtocolInvalid() {
+               Config rc = new Config();
+               rc.setString("protocol", null, "version", "foo");
+               TransferConfig tc = new TransferConfig(rc);
+               assertNull(tc.protocolVersion);
+       }
+}
index 2b5f929dd528e913a6bf68c5abcea19b223567f0..bfb29a835f24b8653cb942846a8e96d1e6cf85a3 100644 (file)
@@ -233,6 +233,7 @@ downloadCancelled=Download cancelled
 downloadCancelledDuringIndexing=Download cancelled during indexing
 duplicateAdvertisementsOf=duplicate advertisements of {0}
 duplicateRef=Duplicate ref: {0}
+duplicateRefAttribute=Duplicate ref attribute: {0}
 duplicateRemoteRefUpdateIsIllegal=Duplicate remote ref update is illegal. Affected remote name: {0}
 duplicateStagesNotAllowed=Duplicate stages not allowed
 eitherGitDirOrWorkTreeRequired=One of setGitDir or setWorkTree must be called.
index a4ca309095fbffeeb52cafecca082b08bc08eb8c..0c691062f952d30fe23bc0390dddc9135dd23019 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011, Christoph Brill <egore911@egore911.de> and others
+ * Copyright (C) 2011, 2020 Christoph Brill <egore911@egore911.de> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -164,7 +164,7 @@ public class LsRemoteCommand extends
                                refSpecs.add(new RefSpec("refs/heads/*:refs/remotes/origin/*")); //$NON-NLS-1$
                        Collection<Ref> refs;
                        Map<String, Ref> refmap = new HashMap<>();
-                       try (FetchConnection fc = transport.openFetch()) {
+                       try (FetchConnection fc = transport.openFetch(refSpecs)) {
                                refs = fc.getRefs();
                                if (refSpecs.isEmpty())
                                        for (Ref r : refs)
index 154f32c25fda85ef57daa87ac8fdcf60eb3a9161..09ec529fc9ce6bdf14d7920a585c78f54a6a11ae 100644 (file)
@@ -261,6 +261,7 @@ public class JGitText extends TranslationBundle {
        /***/ public String downloadCancelledDuringIndexing;
        /***/ public String duplicateAdvertisementsOf;
        /***/ public String duplicateRef;
+       /***/ public String duplicateRefAttribute;
        /***/ public String duplicateRemoteRefUpdateIsIllegal;
        /***/ public String duplicateStagesNotAllowed;
        /***/ public String eitherGitDirOrWorkTreeRequired;
index 3a36398629e2652ecfebbcddff6609817774f460..3826bf74012d3aed1ecca0dff50ebecc2501b0a5 100644 (file)
@@ -1,8 +1,8 @@
 /*
- * Copyright (C) 2008-2010, Google Inc.
+ * Copyright (C) 2008, 2010 Google Inc.
  * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
 
 package org.eclipse.jgit.transport;
 
+import static org.eclipse.jgit.transport.GitProtocolConstants.COMMAND_LS_REFS;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT;
+import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_PEELED;
+import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_SYMREF_TARGET;
+import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_1;
+import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_2;
 
 import java.io.EOFException;
 import java.io.IOException;
@@ -22,23 +27,29 @@ import java.io.OutputStream;
 import java.text.MessageFormat;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
+import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.errors.NoRemoteRepositoryException;
 import org.eclipse.jgit.errors.PackProtocolException;
 import org.eclipse.jgit.errors.RemoteRepositoryException;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.util.StringUtils;
 import org.eclipse.jgit.util.io.InterruptTimer;
 import org.eclipse.jgit.util.io.TimeoutInputStream;
 import org.eclipse.jgit.util.io.TimeoutOutputStream;
@@ -92,17 +103,27 @@ abstract class BasePackConnection extends BaseConnection {
        protected boolean statelessRPC;
 
        /** Capability tokens advertised by the remote side. */
-       private final Set<String> remoteCapablities = new HashSet<>();
+       private final Map<String, String> remoteCapabilities = new HashMap<>();
 
        /** Extra objects the remote has, but which aren't offered as refs. */
        protected final Set<ObjectId> additionalHaves = new HashSet<>();
 
+       private TransferConfig.ProtocolVersion protocol = TransferConfig.ProtocolVersion.V0;
+
        BasePackConnection(PackTransport packTransport) {
                transport = (Transport) packTransport;
                local = transport.local;
                uri = transport.uri;
        }
 
+       TransferConfig.ProtocolVersion getProtocolVersion() {
+               return protocol;
+       }
+
+       void setProtocolVersion(@NonNull TransferConfig.ProtocolVersion protocol) {
+               this.protocol = protocol;
+       }
+
        /**
         * Configure this connection with the directional pipes.
         *
@@ -147,12 +168,15 @@ abstract class BasePackConnection extends BaseConnection {
         * {@link #close()} and the exception is wrapped (if necessary) and thrown
         * as a {@link org.eclipse.jgit.errors.TransportException}.
         *
+        * @return {@code true} if the refs were read; {@code false} otherwise
+        *         indicating that {@link #lsRefs} must be called
+        *
         * @throws org.eclipse.jgit.errors.TransportException
         *             the reference list could not be scanned.
         */
-       protected void readAdvertisedRefs() throws TransportException {
+       protected boolean readAdvertisedRefs() throws TransportException {
                try {
-                       readAdvertisedRefsImpl();
+                       return readAdvertisedRefsImpl();
                } catch (TransportException err) {
                        close();
                        throw err;
@@ -162,35 +186,79 @@ abstract class BasePackConnection extends BaseConnection {
                }
        }
 
-       private void readAdvertisedRefsImpl() throws IOException {
-               final LinkedHashMap<String, Ref> avail = new LinkedHashMap<>();
-               for (;;) {
-                       String line;
-
-                       try {
-                               line = pckIn.readString();
-                       } catch (EOFException eof) {
-                               if (avail.isEmpty())
-                                       throw noRepository();
-                               throw eof;
-                       }
-                       if (PacketLineIn.isEnd(line))
-                               break;
+       private String readLine() throws IOException {
+               String line = pckIn.readString();
+               if (PacketLineIn.isEnd(line)) {
+                       return null;
+               }
+               if (line.startsWith("ERR ")) { //$NON-NLS-1$
+                       // This is a customized remote service error.
+                       // Users should be informed about it.
+                       throw new RemoteRepositoryException(uri, line.substring(4));
+               }
+               return line;
+       }
 
-                       if (line.startsWith("ERR ")) { //$NON-NLS-1$
-                               // This is a customized remote service error.
-                               // Users should be informed about it.
-                               throw new RemoteRepositoryException(uri, line.substring(4));
-                       }
+       private boolean readAdvertisedRefsImpl() throws IOException {
+               final Map<String, Ref> avail = new LinkedHashMap<>();
+               final Map<String, String> symRefs = new LinkedHashMap<>();
+               for (boolean first = true;; first = false) {
+                       String line;
 
-                       if (avail.isEmpty()) {
+                       if (first) {
+                               boolean isV1 = false;
+                               try {
+                                       line = readLine();
+                               } catch (EOFException e) {
+                                       TransportException noRepo = noRepository();
+                                       noRepo.initCause(e);
+                                       throw noRepo;
+                               }
+                               if (line != null && VERSION_1.equals(line)) {
+                                       // Same as V0, except for this extra line. We shouldn't get
+                                       // it since we never request V1.
+                                       setProtocolVersion(TransferConfig.ProtocolVersion.V0);
+                                       isV1 = true;
+                                       line = readLine();
+                               }
+                               if (line == null) {
+                                       break;
+                               }
                                final int nul = line.indexOf('\0');
                                if (nul >= 0) {
-                                       // The first line (if any) may contain "hidden"
-                                       // capability values after a NUL byte.
-                                       remoteCapablities.addAll(
-                                                       Arrays.asList(line.substring(nul + 1).split(" "))); //$NON-NLS-1$
+                                       // Protocol V0: The first line (if any) may contain
+                                       // "hidden" capability values after a NUL byte.
+                                       for (String capability : line.substring(nul + 1)
+                                                       .split(" ")) { //$NON-NLS-1$
+                                               if (capability.startsWith(CAPABILITY_SYMREF_PREFIX)) {
+                                                       String[] parts = capability
+                                                                       .substring(
+                                                                                       CAPABILITY_SYMREF_PREFIX.length())
+                                                                       .split(":", 2); //$NON-NLS-1$
+                                                       if (parts.length == 2) {
+                                                               symRefs.put(parts[0], parts[1]);
+                                                       }
+                                               } else {
+                                                       addCapability(capability);
+                                               }
+                                       }
                                        line = line.substring(0, nul);
+                                       setProtocolVersion(TransferConfig.ProtocolVersion.V0);
+                               } else if (!isV1 && VERSION_2.equals(line)) {
+                                       // Protocol V2: remaining lines are capabilities as
+                                       // key=value pairs
+                                       setProtocolVersion(TransferConfig.ProtocolVersion.V2);
+                                       readCapabilitiesV2();
+                                       // Break out here so that stateless RPC transports get a
+                                       // chance to set up the output stream.
+                                       return false;
+                               } else {
+                                       setProtocolVersion(TransferConfig.ProtocolVersion.V0);
+                               }
+                       } else {
+                               line = readLine();
+                               if (line == null) {
+                                       break;
                                }
                        }
 
@@ -199,73 +267,214 @@ abstract class BasePackConnection extends BaseConnection {
                                throw invalidRefAdvertisementLine(line);
                        }
                        String name = line.substring(41, line.length());
-                       if (avail.isEmpty() && name.equals("capabilities^{}")) { //$NON-NLS-1$
-                               // special line from git-receive-pack to show
+                       if (first && name.equals("capabilities^{}")) { //$NON-NLS-1$
+                               // special line from git-receive-pack (protocol V0) to show
                                // capabilities when there are no refs to advertise
                                continue;
                        }
 
-                       final ObjectId id;
-                       try {
-                               id  = ObjectId.fromString(line.substring(0, 40));
-                       } catch (InvalidObjectIdException e) {
-                               PackProtocolException ppe = invalidRefAdvertisementLine(line);
-                               ppe.initCause(e);
-                               throw ppe;
-                       }
+                       final ObjectId id = toId(line, line.substring(0, 40));
                        if (name.equals(".have")) { //$NON-NLS-1$
                                additionalHaves.add(id);
-                       } else if (name.endsWith("^{}")) { //$NON-NLS-1$
-                               name = name.substring(0, name.length() - 3);
-                               final Ref prior = avail.get(name);
-                               if (prior == null)
-                                       throw new PackProtocolException(uri, MessageFormat.format(
-                                                       JGitText.get().advertisementCameBefore, name, name));
-
-                               if (prior.getPeeledObjectId() != null)
-                                       throw duplicateAdvertisement(name + "^{}"); //$NON-NLS-1$
-
-                               avail.put(name, new ObjectIdRef.PeeledTag(
-                                               Ref.Storage.NETWORK, name, prior.getObjectId(), id));
                        } else {
-                               final Ref prior = avail.put(name, new ObjectIdRef.PeeledNonTag(
-                                               Ref.Storage.NETWORK, name, id));
-                               if (prior != null)
-                                       throw duplicateAdvertisement(name);
+                               processLineV1(name, id, avail);
                        }
                }
-               updateWithSymRefs(avail, extractSymRefsFromCapabilities(remoteCapablities));
+               updateWithSymRefs(avail, symRefs);
                available(avail);
+               return true;
        }
 
        /**
-        * Finds values in the given capabilities of the form:
-        *
-        * <pre>
-        * symref=<em>source</em>:<em>target</em>
-        * </pre>
+        * Issue a protocol V2 ls-refs command and read its response.
         *
-        * And returns a Map of source->target entries.
-        *
-        * @param capabilities
-        *            the capabilities lines
-        * @return a Map of the symref entries from capabilities
-        * @throws NullPointerException
-        *             if capabilities, or any entry in it, is null
+        * @param refSpecs
+        *            to produce ref prefixes from if the server supports git
+        *            protocol V2
+        * @param additionalPatterns
+        *            to use for ref prefixes if the server supports git protocol V2
+        * @throws TransportException
+        *             if the command could not be run or its output not be read
         */
-       static Map<String, String> extractSymRefsFromCapabilities(Collection<String> capabilities) {
+       protected void lsRefs(Collection<RefSpec> refSpecs,
+                       String... additionalPatterns) throws TransportException {
+               try {
+                       lsRefsImpl(refSpecs, additionalPatterns);
+               } catch (TransportException err) {
+                       close();
+                       throw err;
+               } catch (IOException | RuntimeException err) {
+                       close();
+                       throw new TransportException(err.getMessage(), err);
+               }
+       }
+
+       private void lsRefsImpl(Collection<RefSpec> refSpecs,
+                       String... additionalPatterns) throws IOException {
+               pckOut.writeString("command=" + COMMAND_LS_REFS); //$NON-NLS-1$
+               // Add the user-agent
+               String agent = UserAgent.get();
+               if (agent != null && isCapableOf(OPTION_AGENT)) {
+                       pckOut.writeString(OPTION_AGENT + '=' + agent);
+               }
+               pckOut.writeDelim();
+               pckOut.writeString("peel"); //$NON-NLS-1$
+               pckOut.writeString("symrefs"); //$NON-NLS-1$
+               for (String refPrefix : getRefPrefixes(refSpecs, additionalPatterns)) {
+                       pckOut.writeString("ref-prefix " + refPrefix); //$NON-NLS-1$
+               }
+               pckOut.end();
+               final Map<String, Ref> avail = new LinkedHashMap<>();
                final Map<String, String> symRefs = new LinkedHashMap<>();
-               for (String option : capabilities) {
-                       if (option.startsWith(CAPABILITY_SYMREF_PREFIX)) {
-                               String[] symRef = option
-                                               .substring(CAPABILITY_SYMREF_PREFIX.length())
-                                               .split(":", 2); //$NON-NLS-1$
-                               if (symRef.length == 2) {
-                                       symRefs.put(symRef[0], symRef[1]);
+               for (;;) {
+                       String line = readLine();
+                       if (line == null) {
+                               break;
+                       }
+                       // Expecting to get a line in the form "sha1 refname"
+                       if (line.length() < 41 || line.charAt(40) != ' ') {
+                               throw invalidRefAdvertisementLine(line);
+                       }
+                       String name = line.substring(41, line.length());
+                       final ObjectId id = toId(line, line.substring(0, 40));
+                       if (name.equals(".have")) { //$NON-NLS-1$
+                               additionalHaves.add(id);
+                       } else {
+                               processLineV2(line, id, name, avail, symRefs);
+                       }
+               }
+               updateWithSymRefs(avail, symRefs);
+               available(avail);
+       }
+
+       private Collection<String> getRefPrefixes(Collection<RefSpec> refSpecs,
+                       String... additionalPatterns) {
+               if (refSpecs.isEmpty() && (additionalPatterns == null
+                               || additionalPatterns.length == 0)) {
+                       return Collections.emptyList();
+               }
+               Set<String> patterns = new HashSet<>();
+               if (additionalPatterns != null) {
+                       Arrays.stream(additionalPatterns).filter(Objects::nonNull)
+                                       .forEach(patterns::add);
+               }
+               for (RefSpec spec : refSpecs) {
+                       // TODO: for now we only do protocol V2 for fetch. For push
+                       // RefSpecs, the logic would need to be different. (At the
+                       // minimum, take spec.getDestination().)
+                       String src = spec.getSource();
+                       if (ObjectId.isId(src)) {
+                               continue;
+                       }
+                       if (spec.isWildcard()) {
+                               patterns.add(src.substring(0, src.indexOf('*')));
+                       } else {
+                               patterns.add(src);
+                               patterns.add(Constants.R_REFS + src);
+                               patterns.add(Constants.R_HEADS + src);
+                               patterns.add(Constants.R_TAGS + src);
+                       }
+               }
+               return patterns;
+       }
+
+       private void readCapabilitiesV2() throws IOException {
+               // In git protocol V2, capabilities are different. If it's a key-value
+               // pair, the key may be a command name, and the value a space-separated
+               // list of capabilities for that command. We still store it in the same
+               // map as for protocol v0/v1. Protocol v2 code has to account for this.
+               for (;;) {
+                       String line = readLine();
+                       if (line == null) {
+                               break;
+                       }
+                       addCapability(line);
+               }
+       }
+
+       private void addCapability(String capability) {
+               String parts[] = capability.split("=", 2); //$NON-NLS-1$
+               if (parts.length == 2) {
+                       remoteCapabilities.put(parts[0], parts[1]);
+               }
+               remoteCapabilities.put(capability, null);
+       }
+
+       private ObjectId toId(String line, String value)
+                       throws PackProtocolException {
+               try {
+                       return ObjectId.fromString(value);
+               } catch (InvalidObjectIdException e) {
+                       PackProtocolException ppe = invalidRefAdvertisementLine(line);
+                       ppe.initCause(e);
+                       throw ppe;
+               }
+       }
+
+       private void processLineV1(String name, ObjectId id, Map<String, Ref> avail)
+                       throws IOException {
+               if (name.endsWith("^{}")) { //$NON-NLS-1$
+                       name = name.substring(0, name.length() - 3);
+                       final Ref prior = avail.get(name);
+                       if (prior == null) {
+                               throw new PackProtocolException(uri, MessageFormat.format(
+                                               JGitText.get().advertisementCameBefore, name, name));
+                       }
+                       if (prior.getPeeledObjectId() != null) {
+                               throw duplicateAdvertisement(name + "^{}"); //$NON-NLS-1$
+                       }
+                       avail.put(name, new ObjectIdRef.PeeledTag(Ref.Storage.NETWORK, name,
+                                       prior.getObjectId(), id));
+               } else {
+                       final Ref prior = avail.put(name, new ObjectIdRef.PeeledNonTag(
+                                       Ref.Storage.NETWORK, name, id));
+                       if (prior != null) {
+                               throw duplicateAdvertisement(name);
+                       }
+               }
+       }
+
+       private void processLineV2(String line, ObjectId id, String rest,
+                       Map<String, Ref> avail, Map<String, String> symRefs)
+                       throws IOException {
+               String[] parts = rest.split(" "); //$NON-NLS-1$
+               String name = parts[0];
+               // Two attributes possible, symref-target or peeled
+               String symRefTarget = null;
+               String peeled = null;
+               for (int i = 1; i < parts.length; i++) {
+                       if (parts[i].startsWith(REF_ATTR_SYMREF_TARGET)) {
+                               if (symRefTarget != null) {
+                                       throw new PackProtocolException(uri, MessageFormat.format(
+                                                       JGitText.get().duplicateRefAttribute, line));
+                               }
+                               symRefTarget = parts[i]
+                                               .substring(REF_ATTR_SYMREF_TARGET.length());
+                       } else if (parts[i].startsWith(REF_ATTR_PEELED)) {
+                               if (peeled != null) {
+                                       throw new PackProtocolException(uri, MessageFormat.format(
+                                                       JGitText.get().duplicateRefAttribute, line));
                                }
+                               peeled = parts[i].substring(REF_ATTR_PEELED.length());
                        }
+                       if (peeled != null && symRefTarget != null) {
+                               break;
+                       }
+               }
+               Ref idRef;
+               if (peeled != null) {
+                       idRef = new ObjectIdRef.PeeledTag(Ref.Storage.NETWORK, name, id,
+                                       toId(line, peeled));
+               } else {
+                       idRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK, name, id);
+               }
+               Ref prior = avail.put(name, idRef);
+               if (prior != null) {
+                       throw duplicateAdvertisement(name);
+               }
+               if (!StringUtils.isEmptyOrNull(symRefTarget)) {
+                       symRefs.put(name, symRefTarget);
                }
-               return symRefs;
        }
 
        /**
@@ -334,6 +543,22 @@ abstract class BasePackConnection extends BaseConnection {
                                }
                        }
                }
+               // If HEAD is still in the symRefs map here, the real ref was not
+               // reported, but we know it must point to the object reported for HEAD.
+               // So fill it in in the refMap.
+               String headRefName = symRefs.get(Constants.HEAD);
+               if (headRefName != null && !refMap.containsKey(headRefName)) {
+                       Ref headRef = refMap.get(Constants.HEAD);
+                       if (headRef != null) {
+                               ObjectId headObj = headRef.getObjectId();
+                               headRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK,
+                                               headRefName, headObj);
+                               refMap.put(headRefName, headRef);
+                               headRef = new SymbolicRef(Constants.HEAD, headRef);
+                               refMap.put(Constants.HEAD, headRef);
+                               symRefs.remove(Constants.HEAD);
+                       }
+               }
        }
 
        /**
@@ -357,7 +582,7 @@ abstract class BasePackConnection extends BaseConnection {
         * @return whether this option is supported
         */
        protected boolean isCapableOf(String option) {
-               return remoteCapablities.contains(option);
+               return remoteCapabilities.containsKey(option);
        }
 
        /**
@@ -377,6 +602,17 @@ abstract class BasePackConnection extends BaseConnection {
                return true;
        }
 
+       /**
+        * Return a capability value.
+        *
+        * @param option
+        *            to get
+        * @return the value stored, if any.
+        */
+       protected String getCapability(String option) {
+               return remoteCapabilities.get(option);
+       }
+
        /**
         * Add user agent capability
         *
@@ -385,7 +621,7 @@ abstract class BasePackConnection extends BaseConnection {
         */
        protected void addUserAgentCapability(StringBuilder b) {
                String a = UserAgent.get();
-               if (a != null && UserAgent.hasAgent(remoteCapablities)) {
+               if (a != null && remoteCapabilities.get(OPTION_AGENT) != null) {
                        b.append(' ').append(OPTION_AGENT).append('=').append(a);
                }
        }
@@ -393,7 +629,8 @@ abstract class BasePackConnection extends BaseConnection {
        /** {@inheritDoc} */
        @Override
        public String getPeerUserAgent() {
-               return UserAgent.getAgent(remoteCapablities, super.getPeerUserAgent());
+               String agent = remoteCapabilities.get(OPTION_AGENT);
+               return agent != null ? agent : super.getPeerUserAgent();
        }
 
        private PackProtocolException duplicateAdvertisement(String name) {
index a2fb51f46d54b13ce609ff0237ff8d6d0a7ce866..fa0c0c6670cf6d9175de432fc425a446c46d788e 100644 (file)
@@ -1,7 +1,7 @@
 /*
- * Copyright (C) 2008-2010, Google Inc.
+ * Copyright (C) 2008, 2010 Google Inc.
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -16,18 +16,21 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.text.MessageFormat;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.Set;
 
 import org.eclipse.jgit.errors.PackProtocolException;
+import org.eclipse.jgit.errors.RemoteRepositoryException;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.file.PackLock;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.MutableObjectId;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
@@ -44,6 +47,7 @@ import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter;
 import org.eclipse.jgit.revwalk.filter.RevFilter;
 import org.eclipse.jgit.transport.GitProtocolConstants.MultiAck;
 import org.eclipse.jgit.transport.PacketLineIn.AckNackResult;
+import org.eclipse.jgit.util.StringUtils;
 import org.eclipse.jgit.util.TemporaryBuffer;
 
 /**
@@ -207,7 +211,10 @@ public abstract class BasePackFetchConnection extends BasePackConnection
 
        private int maxHaves;
 
-       /** RPC state, if {@link BasePackConnection#statelessRPC} is true. */
+       /**
+        * RPC state, if {@link BasePackConnection#statelessRPC} is true or protocol
+        * V2 is used.
+        */
        private TemporaryBuffer.Heap state;
 
        private PacketLineOut pckState;
@@ -321,6 +328,13 @@ public abstract class BasePackFetchConnection extends BasePackConnection
                return Collections.<PackLock> emptyList();
        }
 
+       private void clearState() {
+               walk.dispose();
+               reachableCommits = null;
+               state = null;
+               pckState = null;
+       }
+
        /**
         * Execute common ancestor negotiation and fetch the objects.
         *
@@ -349,18 +363,34 @@ public abstract class BasePackFetchConnection extends BasePackConnection
                        markRefsAdvertised();
                        markReachable(have, maxTimeWanted(want));
 
+                       if (TransferConfig.ProtocolVersion.V2
+                                       .equals(getProtocolVersion())) {
+                               // Protocol V2 always is a "stateless" protocol, even over a
+                               // bidirectional pipe: the server serves one "fetch" request and
+                               // then forgets anything it has learned, so the next fetch
+                               // request has to re-send all wants and previously determined
+                               // common objects as "have"s again.
+                               state = new TemporaryBuffer.Heap(Integer.MAX_VALUE);
+                               pckState = new PacketLineOut(state);
+                               try {
+                                       doFetchV2(monitor, want, outputStream);
+                               } finally {
+                                       clearState();
+                               }
+                               return;
+                       }
+                       // Protocol V0/1
                        if (statelessRPC) {
                                state = new TemporaryBuffer.Heap(Integer.MAX_VALUE);
                                pckState = new PacketLineOut(state);
                        }
-
-                       if (sendWants(want)) {
+                       PacketLineOut output = statelessRPC ? pckState : pckOut;
+                       if (sendWants(want, output)) {
+                               output.end();
+                               outNeedsEnd = false;
                                negotiate(monitor);
 
-                               walk.dispose();
-                               reachableCommits = null;
-                               state = null;
-                               pckState = null;
+                               clearState();
 
                                receivePack(monitor, outputStream);
                        }
@@ -373,6 +403,180 @@ public abstract class BasePackFetchConnection extends BasePackConnection
                }
        }
 
+       private void doFetchV2(ProgressMonitor monitor, Collection<Ref> want,
+                       OutputStream outputStream) throws IOException, CancelledException {
+               sideband = true;
+               negotiateBegin();
+
+               pckState.writeString("command=" + GitProtocolConstants.COMMAND_FETCH); //$NON-NLS-1$
+               // Capabilities are sent as command arguments in protocol V2
+               String agent = UserAgent.get();
+               if (agent != null && isCapableOf(GitProtocolConstants.OPTION_AGENT)) {
+                       pckState.writeString(
+                                       GitProtocolConstants.OPTION_AGENT + '=' + agent);
+               }
+               Set<String> capabilities = new HashSet<>();
+               String advertised = getCapability(GitProtocolConstants.COMMAND_FETCH);
+               if (!StringUtils.isEmptyOrNull(advertised)) {
+                       capabilities.addAll(Arrays.asList(advertised.split("\\s+"))); //$NON-NLS-1$
+               }
+               // Arguments
+               pckState.writeDelim();
+               for (String capability : getCapabilitiesV2(capabilities)) {
+                       pckState.writeString(capability);
+               }
+               if (!sendWants(want, pckState)) {
+                       // We already have everything we wanted.
+                       return;
+               }
+               // If we send something, we always close it properly ourselves.
+               outNeedsEnd = false;
+
+               FetchStateV2 fetchState = new FetchStateV2();
+               boolean sentDone = false;
+               for (;;) {
+                       // The "state" buffer contains the full fetch request with all
+                       // common objects found so far.
+                       state.writeTo(out, monitor);
+                       sentDone = sendNextHaveBatch(fetchState, pckOut, monitor);
+                       if (sentDone) {
+                               break;
+                       }
+                       if (readAcknowledgments(fetchState, pckIn, monitor)) {
+                               // We got a "ready": next should be a patch file.
+                               break;
+                       }
+                       // Note: C git reads and requires here (and after a packfile) a
+                       // "0002" packet in stateless RPC transports (https). This "response
+                       // end" packet is even mentioned in the protocol V2 technical
+                       // documentation. However, it is not actually part of the public
+                       // protocol; it occurs only in an internal protocol wrapper in the C
+                       // git implementation.
+               }
+               clearState();
+               String line = pckIn.readString();
+               // If we sent a done, we may have an error reply here.
+               if (sentDone && line.startsWith("ERR ")) { //$NON-NLS-1$
+                       throw new RemoteRepositoryException(uri, line.substring(4));
+               }
+               // "shallow-info", "wanted-refs", and "packfile-uris" would have to be
+               // handled here in that order.
+               if (!GitProtocolConstants.SECTION_PACKFILE.equals(line)) {
+                       throw new PackProtocolException(
+                                       MessageFormat.format(JGitText.get().expectedGot,
+                                                       GitProtocolConstants.SECTION_PACKFILE, line));
+               }
+               receivePack(monitor, outputStream);
+       }
+
+       /**
+        * Sends the next batch of "have"s and terminates the {@code output}.
+        *
+        * @param fetchState
+        *            is updated with information about the number of items written,
+        *            and whether to expect a packfile next
+        * @param output
+        *            to write to
+        * @param monitor
+        *            for progress reporting and cancellation
+        * @return {@code true} if a "done" was written and we should thus expect a
+        *         packfile next
+        * @throws IOException
+        *             on errors
+        * @throws CancelledException
+        *             on cancellation
+        */
+       private boolean sendNextHaveBatch(FetchStateV2 fetchState,
+                       PacketLineOut output, ProgressMonitor monitor)
+                       throws IOException, CancelledException {
+               long n = 0;
+               while (n < fetchState.havesToSend) {
+                       final RevCommit c = walk.next();
+                       if (c == null) {
+                               break;
+                       }
+                       output.writeString("have " + c.getId().name() + '\n'); //$NON-NLS-1$
+                       n++;
+                       if (n % 10 == 0 && monitor.isCancelled()) {
+                               throw new CancelledException();
+                       }
+               }
+               fetchState.havesTotal += n;
+               if (n == 0
+                               || fetchState.havesWithoutAck > MAX_HAVES
+                               || fetchState.havesTotal > maxHaves) {
+                       output.writeString("done\n"); //$NON-NLS-1$
+                       output.end();
+                       return true;
+               }
+               fetchState.havesWithoutAck += n;
+               output.end();
+               fetchState.incHavesToSend(statelessRPC);
+               return false;
+       }
+
+       /**
+        * Reads and processes acknowledgments, adding ACKed objects as "have"s to
+        * the global state {@link TemporaryBuffer}.
+        *
+        * @param fetchState
+        *            to update
+        * @param input
+        *            to read from
+        * @param monitor
+        *            for progress reporting and cancellation
+        * @return {@code true} if a "ready" was received and a packfile is expected
+        *         next
+        * @throws IOException
+        *             on errors
+        * @throws CancelledException
+        *             on cancellation
+        */
+       private boolean readAcknowledgments(FetchStateV2 fetchState,
+                       PacketLineIn input, ProgressMonitor monitor)
+                       throws IOException, CancelledException {
+               String line = input.readString();
+               if (!GitProtocolConstants.SECTION_ACKNOWLEDGMENTS.equals(line)) {
+                       throw new PackProtocolException(MessageFormat.format(
+                                       JGitText.get().expectedGot,
+                                       GitProtocolConstants.SECTION_ACKNOWLEDGMENTS, line));
+               }
+               MutableObjectId returnedId = new MutableObjectId();
+               line = input.readString();
+               boolean gotReady = false;
+               long n = 0;
+               while (!PacketLineIn.isEnd(line) && !PacketLineIn.isDelimiter(line)) {
+                       AckNackResult ack = PacketLineIn.parseACKv2(line, returnedId);
+                       // If we got a "ready", we just skip the remaining lines after
+                       // having checked them for being valid. (Normally, the "ready"
+                       // should be the last line anyway.)
+                       if (!gotReady) {
+                               if (ack == AckNackResult.ACK_COMMON) {
+                                       // markCommon appends the object to the "state"
+                                       markCommon(walk.parseAny(returnedId), ack, true);
+                                       fetchState.havesWithoutAck = 0;
+                               } else if (ack == AckNackResult.ACK_READY) {
+                                       gotReady = true;
+                               }
+                       }
+                       n++;
+                       if (n % 10 == 0 && monitor.isCancelled()) {
+                               throw new CancelledException();
+                       }
+                       line = input.readString();
+               }
+               if (gotReady) {
+                       if (!PacketLineIn.isDelimiter(line)) {
+                               throw new PackProtocolException(MessageFormat
+                                               .format(JGitText.get().expectedGot, "0001", line)); //$NON-NLS-1$
+                       }
+               } else if (!PacketLineIn.isEnd(line)) {
+                       throw new PackProtocolException(MessageFormat
+                                       .format(JGitText.get().expectedGot, "0000", line)); //$NON-NLS-1$
+               }
+               return gotReady;
+       }
+
        /** {@inheritDoc} */
        @Override
        public void close() {
@@ -456,8 +660,8 @@ public abstract class BasePackFetchConnection extends BasePackConnection
                }
        }
 
-       private boolean sendWants(Collection<Ref> want) throws IOException {
-               final PacketLineOut p = statelessRPC ? pckState : pckOut;
+       private boolean sendWants(Collection<Ref> want, PacketLineOut p)
+                       throws IOException {
                boolean first = true;
                for (Ref r : want) {
                        ObjectId objectId = r.getObjectId();
@@ -479,10 +683,11 @@ public abstract class BasePackFetchConnection extends BasePackConnection
                        final StringBuilder line = new StringBuilder(46);
                        line.append("want "); //$NON-NLS-1$
                        line.append(objectId.name());
-                       if (first) {
+                       if (first && TransferConfig.ProtocolVersion.V0
+                                       .equals(getProtocolVersion())) {
                                line.append(enableCapabilities());
-                               first = false;
                        }
+                       first = false;
                        line.append('\n');
                        p.writeString(line.toString());
                }
@@ -492,11 +697,34 @@ public abstract class BasePackFetchConnection extends BasePackConnection
                if (!filterSpec.isNoOp()) {
                        p.writeString(filterSpec.filterLine());
                }
-               p.end();
-               outNeedsEnd = false;
                return true;
        }
 
+       private Set<String> getCapabilitiesV2(Set<String> advertisedCapabilities)
+                       throws TransportException {
+               Set<String> capabilities = new LinkedHashSet<>();
+               // Protocol V2 is implicitly capable of all these.
+               if (noProgress) {
+                       capabilities.add(OPTION_NO_PROGRESS);
+               }
+               if (includeTags) {
+                       capabilities.add(OPTION_INCLUDE_TAG);
+               }
+               if (allowOfsDelta) {
+                       capabilities.add(OPTION_OFS_DELTA);
+               }
+               if (thinPack) {
+                       capabilities.add(OPTION_THIN_PACK);
+               }
+               if (!filterSpec.isNoOp()
+                               && !advertisedCapabilities.contains(OPTION_FILTER)) {
+                       throw new PackProtocolException(uri,
+                                       JGitText.get().filterRequiresCapability);
+               }
+               // The FilterSpec will be added later in sendWants().
+               return capabilities;
+       }
+
        private String enableCapabilities() throws TransportException {
                final StringBuilder line = new StringBuilder();
                if (noProgress)
@@ -622,7 +850,7 @@ public abstract class BasePackFetchConnection extends BasePackConnection
                                        // we need to continue to talk about other parts of
                                        // our local history.
                                        //
-                                       markCommon(walk.parseAny(ackId), anr);
+                                       markCommon(walk.parseAny(ackId), anr, statelessRPC);
                                        receivedAck = true;
                                        receivedContinue = true;
                                        havesSinceLastContinue = 0;
@@ -757,16 +985,10 @@ public abstract class BasePackFetchConnection extends BasePackConnection
                }
        }
 
-       private void markCommon(RevObject obj, AckNackResult anr)
+       private void markCommon(RevObject obj, AckNackResult anr, boolean useState)
                        throws IOException {
-               if (statelessRPC && anr == AckNackResult.ACK_COMMON && !obj.has(STATE)) {
-                       StringBuilder s;
-
-                       s = new StringBuilder(6 + Constants.OBJECT_ID_STRING_LENGTH);
-                       s.append("have "); //$NON-NLS-1$
-                       s.append(obj.name());
-                       s.append('\n');
-                       pckState.writeString(s.toString());
+               if (useState && anr == AckNackResult.ACK_COMMON && !obj.has(STATE)) {
+                       pckState.writeString("have " + obj.name() + '\n'); //$NON-NLS-1$
                        obj.add(STATE);
                }
                obj.add(COMMON);
@@ -806,4 +1028,26 @@ public abstract class BasePackFetchConnection extends BasePackConnection
        private static class CancelledException extends Exception {
                private static final long serialVersionUID = 1L;
        }
+
+       private static class FetchStateV2 {
+
+               long havesToSend = 32;
+
+               long havesTotal;
+
+               long havesWithoutAck;
+
+               void incHavesToSend(boolean statelessRPC) {
+                       if (statelessRPC) {
+                               // Increase this quicker since connection setup costs accumulate
+                               if (havesToSend < 16384) {
+                                       havesToSend *= 2;
+                               } else {
+                                       havesToSend = havesToSend * 11 / 10;
+                               }
+                       } else {
+                               havesToSend += 32;
+                       }
+               }
+       }
 }
index 00b726e06317b2323bd29b7e170dd6f7a4a0ffe5..bdebfa607b9781b701303cecac0d44bba1d33982 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -110,7 +110,21 @@ class FetchProcess {
        private void executeImp(final ProgressMonitor monitor,
                        final FetchResult result) throws NotSupportedException,
                        TransportException {
-               conn = transport.openFetch();
+               final TagOpt tagopt = transport.getTagOpt();
+               String getTags = (tagopt == TagOpt.NO_TAGS) ? null : Constants.R_TAGS;
+               String getHead = null;
+               try {
+                       // If we don't have a HEAD yet, we're cloning and need to get the
+                       // upstream HEAD, too.
+                       Ref head = transport.local.exactRef(Constants.HEAD);
+                       ObjectId id = head != null ? head.getObjectId() : null;
+                       if (id == null || id.equals(ObjectId.zeroId())) {
+                               getHead = Constants.HEAD;
+                       }
+               } catch (IOException e) {
+                       // Ignore
+               }
+               conn = transport.openFetch(toFetch, getTags, getHead);
                try {
                        result.setAdvertisedRefs(transport.getURI(), conn.getRefsMap());
                        result.peerUserAgent = conn.getPeerUserAgent();
@@ -127,7 +141,6 @@ class FetchProcess {
                        }
 
                        Collection<Ref> additionalTags = Collections.<Ref> emptyList();
-                       final TagOpt tagopt = transport.getTagOpt();
                        if (tagopt == TagOpt.AUTO_FOLLOW)
                                additionalTags = expandAutoFollowTags();
                        else if (tagopt == TagOpt.FETCH_TAGS)
@@ -261,7 +274,17 @@ class FetchProcess {
                if (conn != null)
                        return;
 
-               conn = transport.openFetch();
+               // Build prefixes
+               Set<String> prefixes = new HashSet<>();
+               for (Ref toGet : askFor.values()) {
+                       String src = toGet.getName();
+                       prefixes.add(src);
+                       prefixes.add(Constants.R_REFS + src);
+                       prefixes.add(Constants.R_HEADS + src);
+                       prefixes.add(Constants.R_TAGS + src);
+               }
+               conn = transport.openFetch(Collections.emptyList(),
+                               prefixes.toArray(new String[0]));
 
                // Since we opened a new connection we cannot be certain
                // that the system we connected to has the same exact set
index 35e2978bc49fd2fbf7e3652c2bd6c9d4800ac0ee..36fce7a3f5425f791b63e880d7ce73fa488e0852 100644 (file)
@@ -1,7 +1,7 @@
 /*
- * Copyright (C) 2008-2013, Google Inc.
+ * Copyright (C) 2008, 2013 Google Inc.
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -247,6 +247,74 @@ public final class GitProtocolConstants {
         */
        public static final String COMMAND_FETCH = "fetch"; //$NON-NLS-1$
 
+       /**
+        * HTTP header to set by clients to request a specific git protocol version
+        * in the HTTP transport.
+        *
+        * @since 5.11
+        */
+       public static final String PROTOCOL_HEADER = "Git-Protocol"; //$NON-NLS-1$
+
+       /**
+        * Environment variable to set by clients to request a specific git protocol
+        * in the file:// and ssh:// transports.
+        *
+        * @since 5.11
+        */
+       public static final String PROTOCOL_ENVIRONMENT_VARIABLE = "GIT_PROTOCOL"; //$NON-NLS-1$
+
+       /**
+        * Protocol V2 ref advertisement attribute containing the peeled object id
+        * for annotated tags.
+        *
+        * @since 5.11
+        */
+       public static final String REF_ATTR_PEELED = "peeled:"; //$NON-NLS-1$
+
+       /**
+        * Protocol V2 ref advertisement attribute containing the name of the ref
+        * for symbolic refs.
+        *
+        * @since 5.11
+        */
+       public static final String REF_ATTR_SYMREF_TARGET = "symref-target:"; //$NON-NLS-1$
+
+       /**
+        * Protocol V2 acknowledgments section header.
+        *
+        * @since 5.11
+        */
+       public static final String SECTION_ACKNOWLEDGMENTS = "acknowledgments"; //$NON-NLS-1$
+
+       /**
+        * Protocol V2 packfile section header.
+        *
+        * @since 5.11
+        */
+       public static final String SECTION_PACKFILE = "packfile"; //$NON-NLS-1$
+
+       /**
+        * Protocol announcement for protocol version 1. This is the same as V0,
+        * except for this initial line.
+        *
+        * @since 5.11
+        */
+       public static final String VERSION_1 = "version 1"; //$NON-NLS-1$
+
+       /**
+        * Protocol announcement for protocol version 2.
+        *
+        * @since 5.11
+        */
+       public static final String VERSION_2 = "version 2"; //$NON-NLS-1$
+
+       /**
+        * Protocol request for protocol version 2.
+        *
+        * @since 5.11
+        */
+       public static final String VERSION_2_REQUEST = "version=2"; //$NON-NLS-1$
+
        enum MultiAck {
                OFF, CONTINUE, DETAILED;
        }
index 350311ecc8279c67ed32e5474d8bf1b03f387904..68c5b348ad1e022c9ca32103dafde299211e4386 100644 (file)
@@ -103,6 +103,38 @@ public class PacketLineIn {
                this.limit = limit;
        }
 
+       /**
+        * Parses a ACK/NAK line in protocol V2.
+        *
+        * @param line
+        *            to parse
+        * @param returnedId
+        *            in case of {@link AckNackResult#ACK_COMMON ACK_COMMON}
+        * @return one of {@link AckNackResult#NAK NAK},
+        *         {@link AckNackResult#ACK_COMMON ACK_COMMON}, or
+        *         {@link AckNackResult#ACK_READY ACK_READY}
+        * @throws IOException
+        *             on protocol or transport errors
+        */
+       static AckNackResult parseACKv2(String line, MutableObjectId returnedId)
+                       throws IOException {
+               if ("NAK".equals(line)) { //$NON-NLS-1$
+                       return AckNackResult.NAK;
+               }
+               if (line.startsWith("ACK ") && line.length() == 44) { //$NON-NLS-1$
+                       returnedId.fromString(line.substring(4, 44));
+                       return AckNackResult.ACK_COMMON;
+               }
+               if ("ready".equals(line)) { //$NON-NLS-1$
+                       return AckNackResult.ACK_READY;
+               }
+               if (line.startsWith("ERR ")) { //$NON-NLS-1$
+                       throw new PackProtocolException(line.substring(4));
+               }
+               throw new PackProtocolException(
+                               MessageFormat.format(JGitText.get().expectedACKNAKGot, line));
+       }
+
        AckNackResult readACK(MutableObjectId returnedId) throws IOException {
                final String line = readString();
                if (line.length() == 0)
index 6fc2042e1f8735ad009cd6925826e1ed9136ad8d..9fe3f0defc15c1dfba3a8f688f5ea41f69f10a74 100644 (file)
@@ -1,7 +1,7 @@
 /*
- * Copyright (C) 2008-2010, Google Inc.
- * Copyright (C) 2008-2009, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2010 Google Inc.
+ * Copyright (C) 2008, 2009 Robin Rosenberg <robin.rosenberg@dewire.com>
+ * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -140,8 +140,14 @@ public class PacketLineOut {
                }
                out.write(buf, pos, len);
                if (log.isDebugEnabled()) {
-                       String s = RawParseUtils.decode(UTF_8, buf, pos, len);
-                       log.debug("git> " + s); //$NON-NLS-1$
+                       // Escape a trailing \n to avoid empty lines in the log.
+                       if (len > 0 && buf[pos + len - 1] == '\n') {
+                               log.debug(
+                                               "git> " + RawParseUtils.decode(UTF_8, buf, pos, len - 1) //$NON-NLS-1$
+                                                               + "\\n"); //$NON-NLS-1$
+                       } else {
+                               log.debug("git> " + RawParseUtils.decode(UTF_8, buf, pos, len)); //$NON-NLS-1$
+                       }
                }
        }
 
index 3adebba03c8e70c547c45382ccd7419381004eb0..c525e668482f8ccb80c7907678b5563cee23a726 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008-2010, Google Inc. and others
+ * Copyright (C) 2008, 2020 Google Inc. and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -13,6 +13,8 @@ package org.eclipse.jgit.transport;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SYMREF;
+import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_PEELED;
+import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_SYMREF_TARGET;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -287,7 +289,8 @@ public abstract class RefAdvertiser {
 
                        if (useProtocolV2) {
                                String symrefPart = symrefs.containsKey(ref.getName())
-                                               ? (" symref-target:" + symrefs.get(ref.getName())) //$NON-NLS-1$
+                                               ? (' ' + REF_ATTR_SYMREF_TARGET
+                                                               + symrefs.get(ref.getName()))
                                                : ""; //$NON-NLS-1$
                                String peelPart = ""; //$NON-NLS-1$
                                if (derefTags) {
@@ -296,7 +299,8 @@ public abstract class RefAdvertiser {
                                        }
                                        ObjectId peeledObjectId = ref.getPeeledObjectId();
                                        if (peeledObjectId != null) {
-                                               peelPart = " peeled:" + peeledObjectId.getName(); //$NON-NLS-1$
+                                               peelPart = ' ' + REF_ATTR_PEELED
+                                                               + peeledObjectId.getName();
                                        }
                                }
                                writeOne(objectId.getName() + " " + ref.getName() + symrefPart //$NON-NLS-1$
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession2.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession2.java
new file mode 100644 (file)
index 0000000..23f670a
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.transport;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * A {@link RemoteSession} that supports passing environment variables to
+ * commands.
+ *
+ * @since 5.11
+ */
+public interface RemoteSession2 extends RemoteSession {
+
+       /**
+        * Creates a new remote {@link Process} to execute the given command. The
+        * returned process's streams exist and are connected, and execution of the
+        * process is already started.
+        *
+        * @param commandName
+        *            command to execute
+        * @param environment
+        *            environment variables to pass on
+        * @param timeout
+        *            timeout value, in seconds, for creating the remote process
+        * @return a new remote process, already started
+        * @throws java.io.IOException
+        *             may be thrown in several cases. For example, on problems
+        *             opening input or output streams or on problems connecting or
+        *             communicating with the remote host. For the latter two cases,
+        *             a TransportException may be thrown (a subclass of
+        *             java.io.IOException).
+        */
+       Process exec(String commandName, Map<String, String> environment,
+                       int timeout) throws IOException;
+}
index 0b38159c094a7a7f157ad41e8065a53d38f83536..83ffd4123aca69abebafad53802169c4dff54b6d 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008-2009, Google Inc. and others
+ * Copyright (C) 2008, 2020 Google Inc. and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -101,6 +101,9 @@ public class TransferConfig {
                                        return v;
                                }
                        }
+                       if ("1".equals(name)) { //$NON-NLS-1$
+                               return V0;
+                       }
                        return null;
                }
        }
index 2ddd0a61286445125a68c56ed78dafbb4bd6ea83..1c998f4e8ccf0eaa885cd2f482562f674540ef66 100644 (file)
@@ -1,8 +1,8 @@
 /*
- * Copyright (C) 2008-2009, Google Inc.
+ * Copyright (C) 2008, 2009 Google Inc.
  * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -39,6 +39,7 @@ import java.util.Vector;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.api.errors.AbortedByHookException;
 import org.eclipse.jgit.errors.NotSupportedException;
 import org.eclipse.jgit.errors.TransportException;
@@ -774,6 +775,10 @@ public abstract class Transport implements AutoCloseable {
        private PrintStream hookOutRedirect;
 
        private PrePushHook prePush;
+
+       @Nullable
+       TransferConfig.ProtocolVersion protocol;
+
        /**
         * Create a new transport instance.
         *
@@ -789,6 +794,7 @@ public abstract class Transport implements AutoCloseable {
                final TransferConfig tc = local.getConfig().get(TransferConfig.KEY);
                this.local = local;
                this.uri = uri;
+               this.protocol = tc.protocolVersion;
                this.objectChecker = tc.newObjectChecker();
                this.credentialsProvider = CredentialsProvider.getDefault();
                prePush = Hooks.prePush(local, hookOutRedirect);
@@ -1452,6 +1458,43 @@ public abstract class Transport implements AutoCloseable {
        public abstract FetchConnection openFetch() throws NotSupportedException,
                        TransportException;
 
+       /**
+        * Begins a new connection for fetching from the remote repository.
+        * <p>
+        * If the transport has no local repository, the fetch connection can only
+        * be used for reading remote refs.
+        * </p>
+        * <p>
+        * If the server supports git protocol V2, the {@link RefSpec}s and the
+        * additional patterns, if any, are used to restrict the server's ref
+        * advertisement to matching refs only.
+        * </p>
+        * <p>
+        * Transports that want to support git protocol V2 <em>must</em> override
+        * this; the default implementation ignores its arguments and calls
+        * {@link #openFetch()}.
+        * </p>
+        *
+        * @param refSpecs
+        *            that will be fetched via
+        *            {@link FetchConnection#fetch(ProgressMonitor, Collection, java.util.Set, OutputStream)} later
+        * @param additionalPatterns
+        *            that will be set as ref prefixes if the server supports git
+        *            protocol V2; {@code null} values are ignored
+        *
+        * @return a fresh connection to fetch from the remote repository.
+        * @throws org.eclipse.jgit.errors.NotSupportedException
+        *             the implementation does not support fetching.
+        * @throws org.eclipse.jgit.errors.TransportException
+        *             the remote connection could not be established.
+        * @since 5.11
+        */
+       public FetchConnection openFetch(Collection<RefSpec> refSpecs,
+                       String... additionalPatterns)
+                       throws NotSupportedException, TransportException {
+               return openFetch();
+       }
+
        /**
         * Begins a new connection for pushing into the remote repository.
         *
index 820ec1a67a0ef62ed3501fb17b9d0ba90a3c1b20..fa4392d5aa9ef565286792c7544ad9984220bc60 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -22,6 +22,7 @@ import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.net.UnknownHostException;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.Set;
@@ -94,6 +95,13 @@ class TransportGitAnon extends TcpTransport implements PackTransport {
                return new TcpFetchConnection();
        }
 
+       @Override
+       public FetchConnection openFetch(Collection<RefSpec> refSpecs,
+                       String... additionalPatterns)
+                       throws NotSupportedException, TransportException {
+               return new TcpFetchConnection(refSpecs, additionalPatterns);
+       }
+
        /** {@inheritDoc} */
        @Override
        public PushConnection openPush() throws TransportException {
@@ -130,7 +138,8 @@ class TransportGitAnon extends TcpTransport implements PackTransport {
                return s;
        }
 
-       void service(String name, PacketLineOut pckOut)
+       void service(String name, PacketLineOut pckOut,
+                       TransferConfig.ProtocolVersion gitProtocol)
                        throws IOException {
                final StringBuilder cmd = new StringBuilder();
                cmd.append(name);
@@ -144,6 +153,11 @@ class TransportGitAnon extends TcpTransport implements PackTransport {
                        cmd.append(uri.getPort());
                }
                cmd.append('\0');
+               if (TransferConfig.ProtocolVersion.V2.equals(gitProtocol)) {
+                       cmd.append('\0');
+                       cmd.append(GitProtocolConstants.VERSION_2_REQUEST);
+                       cmd.append('\0');
+               }
                pckOut.writeString(cmd.toString());
                pckOut.flush();
        }
@@ -152,6 +166,11 @@ class TransportGitAnon extends TcpTransport implements PackTransport {
                private Socket sock;
 
                TcpFetchConnection() throws TransportException {
+                       this(Collections.emptyList());
+               }
+
+               TcpFetchConnection(Collection<RefSpec> refSpecs,
+                               String... additionalPatterns) throws TransportException {
                        super(TransportGitAnon.this);
                        sock = openConnection();
                        try {
@@ -162,13 +181,19 @@ class TransportGitAnon extends TcpTransport implements PackTransport {
                                sOut = new BufferedOutputStream(sOut);
 
                                init(sIn, sOut);
-                               service("git-upload-pack", pckOut); //$NON-NLS-1$
+                               TransferConfig.ProtocolVersion gitProtocol = protocol;
+                               if (gitProtocol == null) {
+                                       gitProtocol = TransferConfig.ProtocolVersion.V2;
+                               }
+                               service("git-upload-pack", pckOut, gitProtocol); //$NON-NLS-1$
                        } catch (IOException err) {
                                close();
                                throw new TransportException(uri,
                                                JGitText.get().remoteHungUpUnexpectedly, err);
                        }
-                       readAdvertisedRefs();
+                       if (!readAdvertisedRefs()) {
+                               lsRefs(refSpecs, additionalPatterns);
+                       }
                }
 
                @Override
@@ -201,7 +226,7 @@ class TransportGitAnon extends TcpTransport implements PackTransport {
                                sOut = new BufferedOutputStream(sOut);
 
                                init(sIn, sOut);
-                               service("git-receive-pack", pckOut); //$NON-NLS-1$
+                               service("git-receive-pack", pckOut, null); //$NON-NLS-1$
                        } catch (IOException err) {
                                close();
                                throw new TransportException(uri,
index b9cb2484d80b926460c6a9a7450b513895a8ce5b..19ed4fbcc11c7b1f36342e137334aee6498db9ca 100644 (file)
@@ -1,8 +1,8 @@
 /*
- * Copyright (C) 2008-2010, Google Inc.
+ * Copyright (C) 2008, 2010 Google Inc.
  * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -19,11 +19,13 @@ import java.io.InputStream;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 
 import org.eclipse.jgit.errors.NoRemoteRepositoryException;
@@ -144,6 +146,13 @@ public class TransportGitSsh extends SshTransport implements PackTransport {
                return new SshFetchConnection();
        }
 
+       @Override
+       public FetchConnection openFetch(Collection<RefSpec> refSpecs,
+                       String... additionalPatterns)
+                       throws NotSupportedException, TransportException {
+               return new SshFetchConnection(refSpecs, additionalPatterns);
+       }
+
        /** {@inheritDoc} */
        @Override
        public PushConnection openPush() throws TransportException {
@@ -196,29 +205,38 @@ public class TransportGitSsh extends SshTransport implements PackTransport {
                return SystemReader.getInstance().getenv("GIT_SSH") != null; //$NON-NLS-1$
        }
 
-       private class ExtSession implements RemoteSession {
+       private class ExtSession implements RemoteSession2 {
+
                @Override
                public Process exec(String command, int timeout)
                                throws TransportException {
+                       return exec(command, null, timeout);
+               }
+
+               @Override
+               public Process exec(String command, Map<String, String> environment,
+                               int timeout) throws TransportException {
                        String ssh = SystemReader.getInstance().getenv("GIT_SSH"); //$NON-NLS-1$
                        boolean putty = ssh.toLowerCase(Locale.ROOT).contains("plink"); //$NON-NLS-1$
 
                        List<String> args = new ArrayList<>();
                        args.add(ssh);
-                       if (putty
-                                       && !ssh.toLowerCase(Locale.ROOT).contains("tortoiseplink")) //$NON-NLS-1$
+                       if (putty && !ssh.toLowerCase(Locale.ROOT)
+                                       .contains("tortoiseplink")) {//$NON-NLS-1$
                                args.add("-batch"); //$NON-NLS-1$
+                       }
                        if (0 < getURI().getPort()) {
                                args.add(putty ? "-P" : "-p"); //$NON-NLS-1$ //$NON-NLS-2$
                                args.add(String.valueOf(getURI().getPort()));
                        }
-                       if (getURI().getUser() != null)
+                       if (getURI().getUser() != null) {
                                args.add(getURI().getUser() + "@" + getURI().getHost()); //$NON-NLS-1$
-                       else
+                       } else {
                                args.add(getURI().getHost());
+                       }
                        args.add(command);
 
-                       ProcessBuilder pb = createProcess(args);
+                       ProcessBuilder pb = createProcess(args, environment);
                        try {
                                return pb.start();
                        } catch (IOException err) {
@@ -226,9 +244,13 @@ public class TransportGitSsh extends SshTransport implements PackTransport {
                        }
                }
 
-               private ProcessBuilder createProcess(List<String> args) {
+               private ProcessBuilder createProcess(List<String> args,
+                               Map<String, String> environment) {
                        ProcessBuilder pb = new ProcessBuilder();
                        pb.command(args);
+                       if (environment != null) {
+                               pb.environment().putAll(environment);
+                       }
                        File directory = local != null ? local.getDirectory() : null;
                        if (directory != null) {
                                pb.environment().put(Constants.GIT_DIR_KEY,
@@ -249,10 +271,31 @@ public class TransportGitSsh extends SshTransport implements PackTransport {
                private StreamCopyThread errorThread;
 
                SshFetchConnection() throws TransportException {
+                       this(Collections.emptyList());
+               }
+
+               SshFetchConnection(Collection<RefSpec> refSpecs,
+                               String... additionalPatterns) throws TransportException {
                        super(TransportGitSsh.this);
                        try {
-                               process = getSession().exec(commandFor(getOptionUploadPack()),
-                                               getTimeout());
+                               RemoteSession session = getSession();
+                               TransferConfig.ProtocolVersion gitProtocol = protocol;
+                               if (gitProtocol == null) {
+                                       gitProtocol = TransferConfig.ProtocolVersion.V2;
+                               }
+                               if (session instanceof RemoteSession2
+                                               && TransferConfig.ProtocolVersion.V2
+                                                               .equals(gitProtocol)) {
+                                       process = ((RemoteSession2) session).exec(
+                                                       commandFor(getOptionUploadPack()), Collections
+                                                                       .singletonMap(
+                                                                                       GitProtocolConstants.PROTOCOL_ENVIRONMENT_VARIABLE,
+                                                                                       GitProtocolConstants.VERSION_2_REQUEST),
+                                                       getTimeout());
+                               } else {
+                                       process = session.exec(commandFor(getOptionUploadPack()),
+                                                       getTimeout());
+                               }
                                final MessageWriter msg = new MessageWriter();
                                setMessageWriter(msg);
 
@@ -272,7 +315,9 @@ public class TransportGitSsh extends SshTransport implements PackTransport {
                        }
 
                        try {
-                               readAdvertisedRefs();
+                               if (!readAdvertisedRefs()) {
+                                       lsRefs(refSpecs, additionalPatterns);
+                               }
                        } catch (NoRemoteRepositoryException notFound) {
                                final String msgs = getMessages();
                                checkExecFailure(process.exitValue(), getOptionUploadPack(),
index 6768387e6532d4fb26757fdef1a9c98179e2d223..9d40f0246624eb097e9d7486ad4a87d95b92fbf1 100644 (file)
@@ -1,8 +1,8 @@
 /*
- * Copyright (C) 2008-2010, Google Inc.
+ * Copyright (C) 2008, 2010 Google Inc.
  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
  * Copyright (C) 2013, Matthias Sohn <matthias.sohn@sap.com>
- * Copyright (C) 2017, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2017, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -33,8 +33,8 @@ import static org.eclipse.jgit.util.HttpSupport.HDR_WWW_AUTHENTICATE;
 import static org.eclipse.jgit.util.HttpSupport.METHOD_GET;
 import static org.eclipse.jgit.util.HttpSupport.METHOD_POST;
 
+import java.io.BufferedInputStream;
 import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
@@ -132,6 +132,9 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
 
        private static final String SVC_RECEIVE_PACK = "git-receive-pack"; //$NON-NLS-1$
 
+       private static final byte[] VERSION = "version" //$NON-NLS-1$
+                       .getBytes(StandardCharsets.US_ASCII);
+
        /**
         * Accept-Encoding header in the HTTP request
         * (https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html).
@@ -339,11 +342,15 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
 
        @SuppressWarnings("resource") // Closed by caller
        private FetchConnection getConnection(HttpConnection c, InputStream in,
-                       String service) throws IOException {
+                       String service, Collection<RefSpec> refSpecs,
+                       String... additionalPatterns) throws IOException {
                BaseConnection f;
                if (isSmartHttp(c, service)) {
-                       readSmartHeaders(in, service);
-                       f = new SmartHttpFetchConnection(in);
+                       InputStream withMark = in.markSupported() ? in
+                                       : new BufferedInputStream(in);
+                       readSmartHeaders(withMark, service);
+                       f = new SmartHttpFetchConnection(withMark, refSpecs,
+                                       additionalPatterns);
                } else {
                        // Assume this server doesn't support smart HTTP fetch
                        // and fall back on dumb object walking.
@@ -357,11 +364,23 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
        @Override
        public FetchConnection openFetch() throws TransportException,
                        NotSupportedException {
+               return openFetch(Collections.emptyList());
+       }
+
+       @Override
+       public FetchConnection openFetch(Collection<RefSpec> refSpecs,
+                       String... additionalPatterns)
+                       throws NotSupportedException, TransportException {
                final String service = SVC_UPLOAD_PACK;
                try {
-                       final HttpConnection c = connect(service);
+                       TransferConfig.ProtocolVersion gitProtocol = protocol;
+                       if (gitProtocol == null) {
+                               gitProtocol = TransferConfig.ProtocolVersion.V2;
+                       }
+                       HttpConnection c = connect(service, gitProtocol);
                        try (InputStream in = openInputStream(c)) {
-                               return getConnection(c, in, service);
+                               return getConnection(c, in, service, refSpecs,
+                                               additionalPatterns);
                        }
                } catch (NotSupportedException | TransportException err) {
                        throw err;
@@ -456,8 +475,9 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
 
        private PushConnection smartPush(String service, HttpConnection c,
                        InputStream in) throws IOException, TransportException {
-               readSmartHeaders(in, service);
-               SmartHttpPushConnection p = new SmartHttpPushConnection(in);
+               BufferedInputStream inBuf = new BufferedInputStream(in);
+               readSmartHeaders(inBuf, service);
+               SmartHttpPushConnection p = new SmartHttpPushConnection(inBuf);
                p.setPeerUserAgent(c.getHeaderField(HttpSupport.HDR_SERVER));
                return p;
        }
@@ -494,6 +514,12 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
 
        private HttpConnection connect(String service)
                        throws TransportException, NotSupportedException {
+               return connect(service, null);
+       }
+
+       private HttpConnection connect(String service,
+                       TransferConfig.ProtocolVersion protocolVersion)
+                       throws TransportException, NotSupportedException {
                URL u = getServiceURL(service);
                int authAttempts = 1;
                int redirects = 0;
@@ -507,6 +533,11 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
                                } else {
                                        conn.setRequestProperty(HDR_ACCEPT, "*/*"); //$NON-NLS-1$
                                }
+                               if (TransferConfig.ProtocolVersion.V2.equals(protocolVersion)) {
+                                       conn.setRequestProperty(
+                                                       GitProtocolConstants.PROTOCOL_HEADER,
+                                                       GitProtocolConstants.VERSION_2_REQUEST);
+                               }
                                final int status = HttpSupport.response(conn);
                                processResponseCookies(conn);
                                switch (status) {
@@ -1148,20 +1179,37 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
 
        private void readSmartHeaders(InputStream in, String service)
                        throws IOException {
-               // A smart reply will have a '#' after the first 4 bytes, but
-               // a dumb reply cannot contain a '#' until after byte 41. Do a
+               // A smart protocol V0 reply will have a '#' after the first 4 bytes,
+               // but a dumb reply cannot contain a '#' until after byte 41. Do a
                // quick check to make sure its a smart reply before we parse
                // as a pkt-line stream.
                //
-               final byte[] magic = new byte[5];
+               // There appears to be a confusion about this in protocol V2. Github
+               // sends the # service line as a git (not http) header also when
+               // protocol V2 is used. Gitlab also does so. JGit's UploadPack doesn't,
+               // and thus Gerrit also does not.
+               final byte[] magic = new byte[14];
+               if (!in.markSupported()) {
+                       throw new TransportException(uri,
+                                       JGitText.get().inputStreamMustSupportMark);
+               }
+               in.mark(14);
                IO.readFully(in, magic, 0, magic.length);
+               // Did we get 000dversion 2 or similar? (Canonical is 000eversion 2\n,
+               // but JGit and thus Gerrit omits the \n.)
+               if (Arrays.equals(Arrays.copyOfRange(magic, 4, 11), VERSION)
+                               && magic[12] >= '1' && magic[12] <= '9') {
+                       // It's a smart server doing version 1 or greater, but not sending
+                       // the # service line header. Don't consume the version line.
+                       in.reset();
+                       return;
+               }
                if (magic[4] != '#') {
                        throw new TransportException(uri, MessageFormat.format(
                                        JGitText.get().expectedPktLineWithService, RawParseUtils.decode(magic)));
                }
-
-               final PacketLineIn pckIn = new PacketLineIn(new UnionInputStream(
-                               new ByteArrayInputStream(magic), in));
+               in.reset();
+               final PacketLineIn pckIn = new PacketLineIn(in);
                final String exp = "# service=" + service; //$NON-NLS-1$
                final String act = pckIn.readString();
                if (!exp.equals(act)) {
@@ -1327,12 +1375,24 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
 
                SmartHttpFetchConnection(InputStream advertisement)
                                throws TransportException {
+                       this(advertisement, Collections.emptyList());
+               }
+
+               SmartHttpFetchConnection(InputStream advertisement,
+                               Collection<RefSpec> refSpecs, String... additionalPatterns)
+                               throws TransportException {
                        super(TransportHttp.this);
                        statelessRPC = true;
 
                        init(advertisement, DisabledOutputStream.INSTANCE);
                        outNeedsEnd = false;
-                       readAdvertisedRefs();
+                       if (!readAdvertisedRefs()) {
+                               // Must be protocol V2
+                               LongPollService service = new LongPollService(SVC_UPLOAD_PACK,
+                                               getProtocolVersion());
+                               init(service.getInputStream(), service.getOutputStream());
+                               lsRefs(refSpecs, additionalPatterns);
+                       }
                }
 
                @Override
@@ -1340,7 +1400,8 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
                                final Collection<Ref> want, final Set<ObjectId> have,
                                final OutputStream outputStream) throws TransportException {
                        try {
-                               svc = new MultiRequestService(SVC_UPLOAD_PACK);
+                               svc = new MultiRequestService(SVC_UPLOAD_PACK,
+                                               getProtocolVersion());
                                init(svc.getInputStream(), svc.getOutputStream());
                                super.doFetch(monitor, want, have, outputStream);
                        } finally {
@@ -1369,7 +1430,8 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
                protected void doPush(final ProgressMonitor monitor,
                                final Map<String, RemoteRefUpdate> refUpdates,
                                OutputStream outputStream) throws TransportException {
-                       final Service svc = new MultiRequestService(SVC_RECEIVE_PACK);
+                       final Service svc = new MultiRequestService(SVC_RECEIVE_PACK,
+                                       getProtocolVersion());
                        init(svc.getInputStream(), svc.getOutputStream());
                        super.doPush(monitor, refUpdates, outputStream);
                }
@@ -1389,10 +1451,14 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
 
                protected final HttpExecuteStream execute;
 
+               protected final TransferConfig.ProtocolVersion protocolVersion;
+
                final UnionInputStream in;
 
-               Service(String serviceName) {
+               Service(String serviceName,
+                               TransferConfig.ProtocolVersion protocolVersion) {
                        this.serviceName = serviceName;
+                       this.protocolVersion = protocolVersion;
                        this.requestType = "application/x-" + serviceName + "-request"; //$NON-NLS-1$ //$NON-NLS-2$
                        this.responseType = "application/x-" + serviceName + "-result"; //$NON-NLS-1$ //$NON-NLS-2$
 
@@ -1408,6 +1474,10 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
                        conn.setDoOutput(true);
                        conn.setRequestProperty(HDR_CONTENT_TYPE, requestType);
                        conn.setRequestProperty(HDR_ACCEPT, responseType);
+                       if (TransferConfig.ProtocolVersion.V2.equals(protocolVersion)) {
+                               conn.setRequestProperty(GitProtocolConstants.PROTOCOL_HEADER,
+                                               GitProtocolConstants.VERSION_2_REQUEST);
+                       }
                }
 
                void sendRequest() throws IOException {
@@ -1663,8 +1733,9 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
        class MultiRequestService extends Service {
                boolean finalRequest;
 
-               MultiRequestService(String serviceName) {
-                       super(serviceName);
+               MultiRequestService(String serviceName,
+                               TransferConfig.ProtocolVersion protocolVersion) {
+                       super(serviceName, protocolVersion);
                }
 
                /** Keep opening send-receive pairs to the given URI. */
@@ -1701,11 +1772,10 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
 
        /** Service for maintaining a single long-poll connection. */
        class LongPollService extends Service {
-               /**
-                * @param serviceName
-                */
-               LongPollService(String serviceName) {
-                       super(serviceName);
+
+               LongPollService(String serviceName,
+                               TransferConfig.ProtocolVersion protocolVersion) {
+                       super(serviceName, protocolVersion);
                }
 
                /** Only open one send-receive request. */
index 403f98d869b53ac9203828d30e1c5808849e82d7..77d1419ea2b40f2ff632ac236bebc1bf6cfe18a4 100644 (file)
@@ -1,9 +1,9 @@
 /*
  * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
- * Copyright (C) 2008-2010, Google Inc.
+ * Copyright (C) 2008, 2010 Google Inc.
  * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -20,6 +20,7 @@ import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
@@ -153,11 +154,17 @@ class TransportLocal extends Transport implements PackTransport {
        /** {@inheritDoc} */
        @Override
        public FetchConnection openFetch() throws TransportException {
+               return openFetch(Collections.emptyList());
+       }
+
+       @Override
+       public FetchConnection openFetch(Collection<RefSpec> refSpecs,
+                       String... additionalPatterns) throws TransportException {
                final String up = getOptionUploadPack();
                if (!"git-upload-pack".equals(up) //$NON-NLS-1$
-                               && !"git upload-pack".equals(up)) //$NON-NLS-1$
-                       return new ForkLocalFetchConnection();
-
+                               && !"git upload-pack".equals(up)) {//$NON-NLS-1$
+                       return new ForkLocalFetchConnection(refSpecs, additionalPatterns);
+               }
                UploadPackFactory<Void> upf = (Void req,
                                Repository db) -> createUploadPack(db);
                return new InternalFetchConnection<>(this, upf, null, openRepo());
@@ -193,6 +200,23 @@ class TransportLocal extends Transport implements PackTransport {
         */
        protected Process spawn(String cmd)
                        throws TransportException {
+               return spawn(cmd, null);
+       }
+
+       /**
+        * Spawn process
+        *
+        * @param cmd
+        *            command
+        * @param protocolVersion
+        *            to use
+        * @return a {@link java.lang.Process} object.
+        * @throws org.eclipse.jgit.errors.TransportException
+        *             if any.
+        */
+       private Process spawn(String cmd,
+                       TransferConfig.ProtocolVersion protocolVersion)
+                       throws TransportException {
                try {
                        String[] args = { "." }; //$NON-NLS-1$
                        ProcessBuilder proc = local.getFS().runInShell(cmd, args);
@@ -208,7 +232,10 @@ class TransportLocal extends Transport implements PackTransport {
                        env.remove("GIT_GRAFT_FILE"); //$NON-NLS-1$
                        env.remove("GIT_INDEX_FILE"); //$NON-NLS-1$
                        env.remove("GIT_NO_REPLACE_OBJECTS"); //$NON-NLS-1$
-
+                       if (TransferConfig.ProtocolVersion.V2.equals(protocolVersion)) {
+                               env.put(GitProtocolConstants.PROTOCOL_ENVIRONMENT_VARIABLE,
+                                               GitProtocolConstants.VERSION_2_REQUEST);
+                       }
                        return proc.start();
                } catch (IOException err) {
                        throw new TransportException(uri, err.getMessage(), err);
@@ -221,12 +248,21 @@ class TransportLocal extends Transport implements PackTransport {
                private Thread errorReaderThread;
 
                ForkLocalFetchConnection() throws TransportException {
+                       this(Collections.emptyList());
+               }
+
+               ForkLocalFetchConnection(Collection<RefSpec> refSpecs,
+                               String... additionalPatterns) throws TransportException {
                        super(TransportLocal.this);
 
                        final MessageWriter msg = new MessageWriter();
                        setMessageWriter(msg);
 
-                       uploadPack = spawn(getOptionUploadPack());
+                       TransferConfig.ProtocolVersion gitProtocol = protocol;
+                       if (gitProtocol == null) {
+                               gitProtocol = TransferConfig.ProtocolVersion.V2;
+                       }
+                       uploadPack = spawn(getOptionUploadPack(), gitProtocol);
 
                        final InputStream upErr = uploadPack.getErrorStream();
                        errorReaderThread = new StreamCopyThread(upErr, msg.getRawStream());
@@ -239,7 +275,9 @@ class TransportLocal extends Transport implements PackTransport {
                        upOut = new BufferedOutputStream(upOut);
 
                        init(upIn, upOut);
-                       readAdvertisedRefs();
+                       if (!readAdvertisedRefs()) {
+                               lsRefs(refSpecs, additionalPatterns);
+                       }
                }
 
                @Override
index 1242ef1b4af5525ec6245e155d5ef85ec10a748b..afe4d3fd9fb887f1aec733669df61316797b3460 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008-2010, Google Inc. and others
+ * Copyright (C) 2008, 2020 Google Inc. and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -33,6 +33,7 @@ import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_SIDEBAND_AL
 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.VERSION_2_REQUEST;
 import static org.eclipse.jgit.util.RefMap.toRefMap;
 
 import java.io.ByteArrayOutputStream;
@@ -709,7 +710,7 @@ public class UploadPack {
         * @since 5.0
         */
        public void setExtraParameters(Collection<String> params) {
-               this.clientRequestedV2 = params.contains("version=2"); //$NON-NLS-1$
+               this.clientRequestedV2 = params.contains(VERSION_2_REQUEST);
        }
 
        /**
@@ -1194,7 +1195,8 @@ public class UploadPack {
                                        new PacketLineOut(NullOutputStream.INSTANCE),
                                        accumulator);
                } else {
-                       pckOut.writeString("acknowledgments\n"); //$NON-NLS-1$
+                       pckOut.writeString(
+                                       GitProtocolConstants.SECTION_ACKNOWLEDGMENTS + '\n');
                        for (ObjectId id : req.getPeerHas()) {
                                if (walk.getObjectReader().has(id)) {
                                        pckOut.writeString("ACK " + id.getName() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
@@ -1243,7 +1245,8 @@ public class UploadPack {
                        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$
+                               pckOut.writeString(
+                                               GitProtocolConstants.SECTION_PACKFILE + '\n');
                        }
 
                        accumulator.timeNegotiating = Duration
@@ -2327,7 +2330,8 @@ public class UploadPack {
                                        // for us if provided a PackfileUriConfig. In this case, we
                                        // are not providing a PackfileUriConfig, so we have to
                                        // write this line ourselves.
-                                       pckOut.writeString("packfile\n"); //$NON-NLS-1$
+                                       pckOut.writeString(
+                                                       GitProtocolConstants.SECTION_PACKFILE + '\n');
                                }
                        }
                        pw.writePack(pm, NullProgressMonitor.INSTANCE, packOut);