Merge changes from topic "git-wire-protocol-v2"
* changes:
Support tracing on clone, fetch and ls-refs
GitProtocolV2IT: Fix git version check for Google environment
Add integration test for git protocol version 2
diff --git a/Documentation/user-request-tracing.txt b/Documentation/user-request-tracing.txt
index d422dd9..b26f4c1 100644
--- a/Documentation/user-request-tracing.txt
+++ b/Documentation/user-request-tracing.txt
@@ -22,12 +22,19 @@
`--trace` option. More information about this can be found in
the link:cmd-index.html#trace[Trace] section of the
link:cmd-index.html[SSH command documentation].
-* Git: For Git pushes tracing can be enabled by setting the
- `trace` push option, the trace ID is returned in the command output.
- More information about this can be found in
- the link:user-upload.html#trace[Trace] section of the
- link:user-upload.html[upload documentation]. Tracing for Git requests
- other than Git push is not supported.
+* Git Push (requires usage of git protocol v2): For Git pushes tracing
+ can be enabled by setting the `trace` push option, the trace ID is
+ returned in the command output. More information about this can be
+ found in the link:user-upload.html#trace[Trace] section of the
+ link:user-upload.html[upload documentation].
+* Git Clone/Fetch/Ls-Remote (requires usage of git protocol v2): For
+ Git clone/fetch/ls-remote tracing can be enabled by setting the
+ `trace` server option. Use '-o trace=<TRACE-ID>' for `git fetch` and
+ `git ls-remote`, and '--server-option trace=<TRACE-ID>' for
+ `git clone`. If the `trace` server option is set without a value
+ (without trace ID) a trace ID is generated but the generated trace ID
+ is not returned to the client (hence a trace ID should always be
+ set).
When request tracing is enabled it is possible to provide an ID that
should be used as trace ID. If a trace ID is not provided a trace ID is
diff --git a/java/com/google/gerrit/acceptance/GitClientVersion.java b/java/com/google/gerrit/acceptance/GitClientVersion.java
new file mode 100644
index 0000000..4c9a32d
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/GitClientVersion.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static java.util.stream.Collectors.joining;
+
+import java.util.stream.IntStream;
+
+/** Class to parse and represent version of git-core client */
+public class GitClientVersion implements Comparable<GitClientVersion> {
+ private final int v[];
+
+ /**
+ * Constructor to represent instance for minimum supported git-core version
+ *
+ * @param parts version passed as single digits
+ */
+ public GitClientVersion(int... parts) {
+ this.v = parts;
+ }
+
+ /**
+ * Parse the git-core version as returned by git version command
+ *
+ * @param version String returned by git version command
+ */
+ public GitClientVersion(String version) {
+ // "git version x.y.z", at Google "git version x.y.z.gXXXXXXXXXX-goog"
+ String parts[] = version.split(" ")[2].split("\\.");
+ int numParts = Math.min(parts.length, 3); // ignore Google-specific part of the version
+ v = new int[numParts];
+ for (int i = 0; i < numParts; i++) {
+ v[i] = Integer.valueOf(parts[i]);
+ }
+ }
+
+ @Override
+ public int compareTo(GitClientVersion o) {
+ int m = Math.max(v.length, o.v.length);
+ for (int i = 0; i < m; i++) {
+ int l = i < v.length ? v[i] : 0;
+ int r = i < o.v.length ? o.v[i] : 0;
+ if (l != r) {
+ return l < r ? -1 : 1;
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return IntStream.of(v).mapToObj(String::valueOf).collect(joining("."));
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
index 933c4e1..a095daa 100644
--- a/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
+++ b/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -15,16 +15,20 @@
package com.google.gerrit.acceptance;
import static com.google.common.truth.Truth.assertWithMessage;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.joining;
import static org.junit.Assert.fail;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
+import com.google.common.io.ByteStreams;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.launcher.GerritLauncher;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.util.ManualRequestContext;
@@ -35,6 +39,9 @@
import com.google.inject.Injector;
import com.google.inject.Module;
import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
import java.util.Arrays;
import java.util.Collections;
import org.eclipse.jgit.lib.Config;
@@ -59,10 +66,10 @@
private ServerContext(GerritServer server) throws Exception {
this.server = server;
Injector i = server.getTestInjector();
- if (adminId == null) {
- adminId = i.getInstance(AccountCreator.class).admin().id();
+ if (admin == null) {
+ admin = i.getInstance(AccountCreator.class).admin();
}
- ctx = i.getInstance(OneOffRequestContext.class).openAs(adminId);
+ ctx = i.getInstance(OneOffRequestContext.class).openAs(admin.id());
GerritApi gApi = i.getInstance(GerritApi.class);
try {
@@ -117,7 +124,7 @@
@Rule public RuleChain ruleChain = RuleChain.outerRule(tempSiteDir).around(testRunner);
protected SitePaths sitePaths;
- protected Account.Id adminId;
+ protected TestAccount admin;
private GerritServer.Description serverDesc;
private SystemReader oldSystemReader;
@@ -190,4 +197,33 @@
protected static void runGerrit(Iterable<String>... multiArgs) throws Exception {
runGerrit(Arrays.stream(multiArgs).flatMap(Streams::stream).toArray(String[]::new));
}
+
+ protected static String execute(
+ ImmutableList<String> cmd, File dir, ImmutableMap<String, String> env) throws IOException {
+ ProcessBuilder pb = new ProcessBuilder(cmd);
+ pb.directory(dir).redirectErrorStream(true);
+ pb.environment().putAll(env);
+ Process p = pb.start();
+ byte[] out;
+ try (InputStream in = p.getInputStream()) {
+ out = ByteStreams.toByteArray(in);
+ } finally {
+ p.getOutputStream().close();
+ }
+
+ int status;
+ try {
+ status = p.waitFor();
+ } catch (InterruptedException e) {
+ throw new InterruptedIOException(
+ "interrupted waiting for: " + Joiner.on(' ').join(pb.command()));
+ }
+
+ String result = new String(out, UTF_8);
+ if (status != 0) {
+ throw new IOException(result);
+ }
+
+ return result.trim();
+ }
}
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index ff2a83d..858c173 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -32,6 +32,7 @@
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.PermissionAwareRepositoryManager;
+import com.google.gerrit.server.git.TracingHook;
import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.git.UploadPackInitializer;
import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
@@ -415,7 +416,11 @@
up.setPreUploadHook(
PreUploadHookChain.newChain(
Lists.newArrayList(up.getPreUploadHook(), uploadValidators)));
- next.doFilter(httpRequest, responseWrapper);
+
+ try (TracingHook tracingHook = new TracingHook()) {
+ up.setProtocolV2Hook(tracingHook);
+ next.doFilter(httpRequest, responseWrapper);
+ }
} finally {
groupAuditService.dispatch(
new HttpAuditEvent(
diff --git a/java/com/google/gerrit/server/git/TracingHook.java b/java/com/google/gerrit/server/git/TracingHook.java
new file mode 100644
index 0000000..4191373
--- /dev/null
+++ b/java/com/google/gerrit/server/git/TracingHook.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.server.logging.TraceContext;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.transport.FetchV2Request;
+import org.eclipse.jgit.transport.LsRefsV2Request;
+import org.eclipse.jgit.transport.ProtocolV2Hook;
+
+/**
+ * Git hook for ls-refs and fetch that enables Gerrit request tracing if the user sets the 'trace'
+ * server option.
+ *
+ * <p>This hook is only invoked if Git protocol v2 is used.
+ *
+ * <p>If the 'trace' server option is specified without value, this means without providing a trace
+ * ID, a trace ID is generated, but it's not returned to the client. Hence users are advised to
+ * always provide a trace ID.
+ */
+public class TracingHook implements ProtocolV2Hook, AutoCloseable {
+ private TraceContext traceContext;
+
+ @Override
+ public void onLsRefs(LsRefsV2Request req) {
+ maybeStartTrace(req.getServerOptions());
+ }
+
+ @Override
+ public void onFetch(FetchV2Request req) {
+ maybeStartTrace(req.getServerOptions());
+ }
+
+ @Override
+ public void close() {
+ if (traceContext != null) {
+ traceContext.close();
+ }
+ }
+
+ /**
+ * Starts request tracing if 'trace' server option is set.
+ *
+ * @param serverOptionList list of provided server options
+ */
+ private void maybeStartTrace(List<String> serverOptionList) {
+ checkState(traceContext == null, "Trace was already started.");
+
+ Optional<String> traceOption = parseTraceOption(serverOptionList);
+ traceContext =
+ TraceContext.newTrace(
+ traceOption.isPresent(),
+ traceOption.orElse(null),
+ (tagName, traceId) -> {
+ // TODO(ekempin): Return trace ID to client
+ });
+ }
+
+ private Optional<String> parseTraceOption(List<String> serverOptionList) {
+ if (serverOptionList == null || serverOptionList.isEmpty()) {
+ return Optional.empty();
+ }
+
+ ListMultimap<String, String> serverOptions = LinkedListMultimap.create();
+ for (String option : serverOptionList) {
+ int e = option.indexOf('=');
+ if (e > 0) {
+ serverOptions.put(option.substring(0, e), option.substring(e + 1));
+ } else {
+ serverOptions.put(option, "");
+ }
+ }
+
+ List<String> traceValues = serverOptions.get("trace");
+ if (!traceValues.isEmpty()) {
+ return Optional.of(Iterables.getLast(traceValues));
+ }
+
+ return Optional.empty();
+ }
+}
diff --git a/java/com/google/gerrit/sshd/commands/Upload.java b/java/com/google/gerrit/sshd/commands/Upload.java
index 41323dd..87ae493 100644
--- a/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/java/com/google/gerrit/sshd/commands/Upload.java
@@ -21,6 +21,7 @@
import com.google.gerrit.server.RequestInfo;
import com.google.gerrit.server.RequestListener;
import com.google.gerrit.server.git.PermissionAwareRepositoryManager;
+import com.google.gerrit.server.git.TracingHook;
import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.git.UploadPackInitializer;
import com.google.gerrit.server.git.validators.UploadValidationException;
@@ -83,13 +84,14 @@
for (UploadPackInitializer initializer : uploadPackInitializers) {
initializer.init(projectState.getNameKey(), up);
}
- try (TraceContext traceContext = TraceContext.open()) {
+ try (TraceContext traceContext = TraceContext.open();
+ TracingHook tracingHook = new TracingHook()) {
RequestInfo requestInfo =
RequestInfo.builder(RequestInfo.RequestType.GIT_UPLOAD, user, traceContext)
.project(projectState.getNameKey())
.build();
requestListeners.runEach(l -> l.onRequest(requestInfo));
-
+ up.setProtocolV2Hook(tracingHook);
up.upload(in, out, err);
session.setPeerAgent(up.getPeerUserAgent());
} catch (UploadValidationException e) {
diff --git a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
index f8176a5..f633842 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/AbstractReindexTests.java
@@ -74,13 +74,13 @@
.containsExactly(changeId);
// Query account index
assertThat(gApi.accounts().query("admin").get().stream().map(a -> a._accountId))
- .containsExactly(adminId.get());
+ .containsExactly(admin.id().get());
// Query group index
assertThat(
gApi.groups().query("Group").withOption(MEMBERS).get().stream()
.flatMap(g -> g.members.stream())
.map(a -> a._accountId))
- .containsExactly(adminId.get());
+ .containsExactly(admin.id().get());
// Query project index
assertThat(gApi.projects().query(project.get()).get().stream().map(p -> p.name))
.containsExactly(project.get());
diff --git a/javatests/com/google/gerrit/integration/git/BUILD b/javatests/com/google/gerrit/integration/git/BUILD
new file mode 100644
index 0000000..6a6f5ad
--- /dev/null
+++ b/javatests/com/google/gerrit/integration/git/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+ srcs = glob(["*IT.java"]),
+ group = "git",
+ labels = ["git"],
+)
diff --git a/javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java b/javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java
new file mode 100644
index 0000000..ece4d4a
--- /dev/null
+++ b/javatests/com/google/gerrit/integration/git/GitProtocolV2IT.java
@@ -0,0 +1,236 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.integration.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
+import com.google.gerrit.acceptance.GitClientVersion;
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.inject.Inject;
+import java.io.File;
+import java.net.InetSocketAddress;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.junit.Test;
+
+@UseSsh
+public class GitProtocolV2IT extends StandaloneSiteTest {
+ private final String[] SSH_KEYGEN_CMD =
+ new String[] {"ssh-keygen", "-t", "rsa", "-q", "-P", "", "-f"};
+ private final String[] GIT_LS_REMOTE =
+ new String[] {"git", "-c", "protocol.version=2", "ls-remote", "-o", "trace=12345"};
+ private final String GIT_SSH_COMMAND =
+ "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i";
+
+ @Inject private GerritApi gApi;
+ @Inject private AccountCreator accountCreator;
+ @Inject private ProjectOperations projectOperations;
+ @Inject private @TestSshServerAddress InetSocketAddress sshAddress;
+ @Inject private @GerritServerConfig Config config;
+
+ @Test
+ public void testGitWireProtocolV2WithSsh() throws Exception {
+ // Minimum required git-core version that supports wire protocol v2 is 2.18.0
+ GitClientVersion requiredGitVersion = new GitClientVersion(2, 18, 0);
+ GitClientVersion actualGitVersion =
+ new GitClientVersion(execute(ImmutableList.of("git", "version")));
+ // If not found, test succeeds with assumption violation
+ assume().that(actualGitVersion).isAtLeast(requiredGitVersion);
+
+ try (ServerContext ctx = startServer()) {
+ ctx.getInjector().injectMembers(this);
+
+ // Create project
+ Project.NameKey project = Project.nameKey("foo");
+ gApi.projects().create(project.get());
+
+ // Set up project permission
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(deny(Permission.READ).ref("refs/*").group(SystemGroupBackend.ANONYMOUS_USERS))
+ .add(
+ allow(Permission.READ)
+ .ref("refs/heads/master")
+ .group(SystemGroupBackend.REGISTERED_USERS))
+ .update();
+
+ // Set protocol.version=2 in target repository
+ execute(
+ ImmutableList.of("git", "config", "protocol.version", "2"),
+ sitePaths.site_path.resolve("git").resolve(project.get() + Constants.DOT_GIT).toFile());
+
+ // Retrieve HTTP url
+ String url = config.getString("gerrit", null, "canonicalweburl");
+ String urlDestinationTemplate =
+ url.substring(0, 7)
+ + "%s:secret@"
+ + url.substring(7, url.length())
+ + "/a/"
+ + project.get();
+
+ // Retrieve SSH host and port
+ String sshDestinationTemplate =
+ "ssh://%s@" + sshAddress.getHostName() + ":" + sshAddress.getPort() + "/" + project.get();
+
+ // Admin user was already created by the base class
+ setUpUserAuthentication(admin.username());
+
+ // Create non-admin user
+ TestAccount user = accountCreator.user();
+ setUpUserAuthentication(user.username());
+
+ // Prepare data for new change on master branch
+ ChangeInput in = new ChangeInput(project.get(), "master", "Test public change");
+ in.newBranch = true;
+
+ // Create new change and retrieve SHA1 for the created patch set
+ String commit =
+ gApi.changes()
+ .id(gApi.changes().create(in).info().changeId)
+ .current()
+ .commit(false)
+ .commit;
+
+ // Prepare new change on secret branch
+ in = new ChangeInput(project.get(), "secret", "Test secret change");
+ in.newBranch = true;
+
+ // Create new change and retrieve SHA1 for the created patch set
+ String secretCommit =
+ gApi.changes()
+ .id(gApi.changes().create(in).info().changeId)
+ .current()
+ .commit(false)
+ .commit;
+
+ // Read refs from target repository using git wire protocol v2 over HTTP for admin user
+ String out =
+ execute(
+ ImmutableList.<String>builder()
+ .add(GIT_LS_REMOTE)
+ .add(String.format(urlDestinationTemplate, admin.username()))
+ .build(),
+ ImmutableMap.of("GIT_TRACE_PACKET", "1"));
+
+ assertGitProtocolV2Refs(commit, out);
+ assertThat(out).contains(secretCommit);
+
+ // Read refs from target repository using git wire protocol v2 over SSH for admin user
+ out =
+ execute(
+ ImmutableList.<String>builder()
+ .add(GIT_LS_REMOTE)
+ .add(String.format(sshDestinationTemplate, admin.username()))
+ .build(),
+ ImmutableMap.of(
+ "GIT_SSH_COMMAND",
+ GIT_SSH_COMMAND
+ + sitePaths.data_dir.resolve(String.format("id_rsa_%s", admin.username())),
+ "GIT_TRACE_PACKET",
+ "1"));
+
+ assertGitProtocolV2Refs(commit, out);
+ assertThat(out).contains(secretCommit);
+
+ // Read refs from target repository using git wire protocol v2 over HTTP for non-admin user
+ out =
+ execute(
+ ImmutableList.<String>builder()
+ .add(GIT_LS_REMOTE)
+ .add(String.format(urlDestinationTemplate, user.username()))
+ .build(),
+ ImmutableMap.of("GIT_TRACE_PACKET", "1"));
+
+ assertGitProtocolV2Refs(commit, out);
+ assertThat(out).doesNotContain(secretCommit);
+
+ // Read refs from target repository using git wire protocol v2 over SSH for non-admin user
+ out =
+ execute(
+ ImmutableList.<String>builder()
+ .add(GIT_LS_REMOTE)
+ .add(String.format(sshDestinationTemplate, user.username()))
+ .build(),
+ ImmutableMap.of(
+ "GIT_SSH_COMMAND",
+ GIT_SSH_COMMAND
+ + sitePaths.data_dir.resolve(String.format("id_rsa_%s", user.username())),
+ "GIT_TRACE_PACKET",
+ "1"));
+
+ assertGitProtocolV2Refs(commit, out);
+ assertThat(out).doesNotContain(secretCommit);
+ }
+ }
+
+ private void setUpUserAuthentication(String username) throws Exception {
+ // Assign HTTP password to user
+ gApi.accounts().id(username).setHttpPassword("secret");
+
+ // Generate private/public key for user
+ execute(
+ ImmutableList.<String>builder()
+ .add(SSH_KEYGEN_CMD)
+ .add(String.format("id_rsa_%s", username))
+ .build());
+
+ // Read the content of generated public key and add it for the user in Gerrit
+ gApi.accounts()
+ .id(username)
+ .addSshKey(
+ new String(
+ java.nio.file.Files.readAllBytes(
+ sitePaths.data_dir.resolve(String.format("id_rsa_%s.pub", username))),
+ UTF_8));
+ }
+
+ private static void assertGitProtocolV2Refs(String commit, String out) {
+ assertThat(out).contains("git< version 2");
+ assertThat(out).contains("refs/changes/01/1/1");
+ assertThat(out).contains("refs/changes/01/1/meta");
+ assertThat(out).contains(commit);
+ }
+
+ private String execute(ImmutableList<String> cmd) throws Exception {
+ return execute(cmd, sitePaths.data_dir.toFile(), ImmutableMap.of());
+ }
+
+ private String execute(ImmutableList<String> cmd, ImmutableMap<String, String> env)
+ throws Exception {
+ return execute(cmd, sitePaths.data_dir.toFile(), env);
+ }
+
+ private static String execute(ImmutableList<String> cmd, File dir) throws Exception {
+ return execute(cmd, dir, ImmutableMap.of());
+ }
+}