Support configurable git binary path for SSH remotes Some SSH destinations may not expose 'git' on the non-interactive shell's PATH, causing 'create project' and 'update head' operations to fail. Add a new 'remote.NAME.gitPath' config option that lets admins provide the absolute path to the 'git' binary on the remote host. Change-Id: I01e32cff5240cb55cf9e0fc0539344b340361ac4
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/AdminApiFactory.java b/src/main/java/com/googlesource/gerrit/plugins/replication/AdminApiFactory.java index 30e8245..25e9d18 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/AdminApiFactory.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/AdminApiFactory.java
@@ -14,41 +14,54 @@ package com.googlesource.gerrit.plugins.replication; +import com.google.gerrit.common.Nullable; import com.google.inject.Inject; import com.google.inject.Singleton; +import com.googlesource.gerrit.plugins.replication.api.ReplicationConfig; import java.util.Optional; import org.eclipse.jgit.transport.URIish; /** Factory for creating an {@link AdminApi} instance for a remote URI. */ public interface AdminApiFactory { /** - * Create an {@link AdminApi} for the given remote URI. + * Create an {@link AdminApi} for the given remote URI with knowledge of which {@code + * remote.<remoteName>} section in {@code replication.config} the URI belongs to. * * @param uri the remote URI. + * @param remoteName the name of the {@code remote} section, or {@code null} when unknown. * @return An API for the given remote URI, or {@code Optional.empty} if there is no appropriate * API for the URI. */ - Optional<AdminApi> create(URIish uri); + Optional<AdminApi> create(URIish uri, @Nullable String remoteName); @Singleton static class DefaultAdminApiFactory implements AdminApiFactory { protected final SshHelper sshHelper; private final GerritRestApi.Factory gerritRestApiFactory; + private final ReplicationConfig replicationConfig; @Inject - public DefaultAdminApiFactory(SshHelper sshHelper, GerritRestApi.Factory gerritRestApiFactory) { + public DefaultAdminApiFactory( + SshHelper sshHelper, + GerritRestApi.Factory gerritRestApiFactory, + ReplicationConfig replicationConfig) { this.sshHelper = sshHelper; this.gerritRestApiFactory = gerritRestApiFactory; + this.replicationConfig = replicationConfig; } @Override - public Optional<AdminApi> create(URIish uri) { + public Optional<AdminApi> create(URIish uri, @Nullable String remoteName) { if (isGerrit(uri)) { return Optional.of(new GerritSshApi(sshHelper, uri)); } else if (!uri.isRemote()) { return Optional.of(new LocalFS(uri)); } else if (isSSH(uri)) { - return Optional.of(new RemoteSsh(sshHelper, uri)); + String gitPath = + remoteName == null + ? null + : replicationConfig.getConfig().getString("remote", remoteName, "gitPath"); + return Optional.of(new RemoteSsh(sshHelper, uri, gitPath)); } else if (isGerritHttp(uri)) { return Optional.of(gerritRestApiFactory.create(uri)); }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/CreateProjectTask.java b/src/main/java/com/googlesource/gerrit/plugins/replication/CreateProjectTask.java index 32903ab..04f9869 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/CreateProjectTask.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/CreateProjectTask.java
@@ -61,7 +61,7 @@ private boolean createProject( URIish replicateURI, Project.NameKey projectName, String head, boolean storeRefLog) { - Optional<AdminApi> adminApi = adminApiFactory.get().create(replicateURI); + Optional<AdminApi> adminApi = adminApiFactory.get().create(replicateURI, config.getName()); if (adminApi.isPresent() && adminApi.get().createProject(projectName, head, storeRefLog)) { return true; }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/DeleteProjectTask.java b/src/main/java/com/googlesource/gerrit/plugins/replication/DeleteProjectTask.java index 965ca94..f3fcafe 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/DeleteProjectTask.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/DeleteProjectTask.java
@@ -24,6 +24,7 @@ import com.google.inject.assistedinject.Assisted; import com.googlesource.gerrit.plugins.replication.events.ProjectDeletionState; import java.util.Optional; +import org.eclipse.jgit.transport.RemoteConfig; import org.eclipse.jgit.transport.URIish; public class DeleteProjectTask implements Runnable { @@ -33,6 +34,7 @@ URIish replicateURI, Project.NameKey project, ProjectDeletionState state); } + private final RemoteConfig config; private final DynamicItem<AdminApiFactory> adminApiFactory; private final int id; private final URIish replicateURI; @@ -41,11 +43,13 @@ @Inject DeleteProjectTask( + RemoteConfig config, DynamicItem<AdminApiFactory> adminApiFactory, IdGenerator ig, @Assisted ProjectDeletionState state, @Assisted URIish replicateURI, @Assisted Project.NameKey project) { + this.config = config; this.adminApiFactory = adminApiFactory; this.id = ig.next(); this.replicateURI = replicateURI; @@ -55,7 +59,7 @@ @Override public void run() { - Optional<AdminApi> adminApi = adminApiFactory.get().create(replicateURI); + Optional<AdminApi> adminApi = adminApiFactory.get().create(replicateURI, config.getName()); if (adminApi.isPresent()) { if (adminApi.get().deleteProject(project)) { state.setSucceeded(replicateURI);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java index f0b26f9..9c1bfba 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/Destination.java
@@ -898,6 +898,18 @@ return config.storeRefLog(); } + String getUploadPack() { + return config.getUploadPack(); + } + + String getReceivePack() { + return config.getReceivePack(); + } + + String getGitPath() { + return config.getGitPath(); + } + private static boolean matches(URIish uri, String urlMatch) { if (urlMatch == null || urlMatch.equals("") || urlMatch.equals("*")) { return true;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java index b9e1be9..d2b9686 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/DestinationConfiguration.java
@@ -57,6 +57,9 @@ private final Supplier<Integer> pushBatchSize; private final ImmutableList<Pattern> excludedRefsPattern; private final boolean storeRefLog; + private final String uploadPack; + private final String receivePack; + private final String gitPath; protected DestinationConfiguration(RemoteConfig remoteConfig, Config cfg) { this.remoteConfig = remoteConfig; @@ -124,6 +127,9 @@ }); excludedRefsPattern = getExcludedRefsPattern(cfg, name); storeRefLog = cfg.getBoolean("remote", name, "storeRefLog", false); + uploadPack = cfg.getString("remote", name, "uploadpack"); + receivePack = cfg.getString("remote", name, "receivepack"); + gitPath = cfg.getString("remote", name, "gitPath"); } @Override @@ -234,6 +240,18 @@ return storeRefLog; } + public String getUploadPack() { + return uploadPack; + } + + public String getReceivePack() { + return receivePack; + } + + public String getGitPath() { + return gitPath; + } + private ImmutableList<Pattern> getExcludedRefsPattern(Config cfg, String name) { List<Pattern> patterns = new ArrayList<>(); for (String regex : cfg.getStringList("remote", name, "excludedRefsPattern")) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java index 030ba1c..0e04e1e 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/PushOne.java
@@ -24,6 +24,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; +import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableSet; import com.google.common.collect.LinkedListMultimap; @@ -558,6 +559,7 @@ private PushResult pushVia(Repository git, Transport tn) throws IOException, PermissionBackendException { tn.applyConfig(config); + setUploadAndReceivePack(tn); tn.setCredentialsProvider(credentialsFactory.create(config.getName())); List<RemoteRefUpdate> todo = generateUpdates(git, tn); @@ -612,6 +614,24 @@ return result; } + private void setUploadAndReceivePack(Transport tn) { + String gitPath = pool.getGitPath(); + if (Strings.isNullOrEmpty(gitPath)) { + return; + } + int lastSlash = gitPath.lastIndexOf('/'); + if (lastSlash < 0) { + return; + } + String binDir = gitPath.substring(0, lastSlash + 1); + if (pool.getUploadPack() == null) { + tn.setOptionUploadPack(binDir + "git-upload-pack"); + } + if (pool.getReceivePack() == null) { + tn.setOptionReceivePack(binDir + "git-receive-pack"); + } + } + private static String refUpdatesForLogging(List<RemoteRefUpdate> refUpdates) { return refUpdates.stream().map(PushOne::refUpdateForLogging).collect(joining(", ")); }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteSsh.java b/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteSsh.java index f96c157..2c9cb03 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteSsh.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/RemoteSsh.java
@@ -16,6 +16,8 @@ import static com.googlesource.gerrit.plugins.replication.ReplicationQueue.repLog; +import com.google.common.base.Strings; +import com.google.gerrit.common.Nullable; import com.google.gerrit.entities.Project; import java.io.IOException; import java.io.OutputStream; @@ -26,18 +28,24 @@ private final SshHelper sshHelper; private URIish uri; + private final String git; RemoteSsh(SshHelper sshHelper, URIish uri) { + this(sshHelper, uri, null); + } + + RemoteSsh(SshHelper sshHelper, URIish uri, @Nullable String gitPath) { this.sshHelper = sshHelper; this.uri = uri; + this.git = Strings.isNullOrEmpty(gitPath) ? "git" : QuotedString.BOURNE.quote(gitPath); } @Override public boolean createProject(Project.NameKey project, String head) { String quotedPath = QuotedString.BOURNE.quote(uri.getPath()); - String cmd = "mkdir -p " + quotedPath + " && cd " + quotedPath + " && git init --bare"; + String cmd = "mkdir -p " + quotedPath + " && cd " + quotedPath + " && " + git + " init --bare"; if (head != null) { - cmd = cmd + " && git symbolic-ref HEAD " + QuotedString.BOURNE.quote(head); + cmd = cmd + " && " + git + " symbolic-ref HEAD " + QuotedString.BOURNE.quote(head); } OutputStream errStream = sshHelper.newErrorBufferStream(); try { @@ -79,7 +87,12 @@ public boolean updateHead(Project.NameKey project, String newHead) { String quotedPath = QuotedString.BOURNE.quote(uri.getPath()); String cmd = - "cd " + quotedPath + " && git symbolic-ref HEAD " + QuotedString.BOURNE.quote(newHead); + "cd " + + quotedPath + + " && " + + git + + " symbolic-ref HEAD " + + QuotedString.BOURNE.quote(newHead); OutputStream errStream = sshHelper.newErrorBufferStream(); try { sshHelper.executeRemoteSsh(uri, cmd, errStream);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/UpdateHeadTask.java b/src/main/java/com/googlesource/gerrit/plugins/replication/UpdateHeadTask.java index fdbd5e7..ce6370c 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/UpdateHeadTask.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/UpdateHeadTask.java
@@ -23,10 +23,12 @@ import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import java.util.Optional; +import org.eclipse.jgit.transport.RemoteConfig; import org.eclipse.jgit.transport.URIish; public class UpdateHeadTask implements Runnable { private final DynamicItem<AdminApiFactory> adminApiFactory; + private final RemoteConfig remoteConfig; private final int id; private final URIish replicateURI; private final Project.NameKey project; @@ -39,11 +41,13 @@ @Inject UpdateHeadTask( DynamicItem<AdminApiFactory> adminApiFactory, + RemoteConfig remoteConfig, IdGenerator ig, @Assisted URIish replicateURI, @Assisted Project.NameKey project, @Assisted String newHead) { this.adminApiFactory = adminApiFactory; + this.remoteConfig = remoteConfig; this.id = ig.next(); this.replicateURI = replicateURI; this.project = project; @@ -52,7 +56,8 @@ @Override public void run() { - Optional<AdminApi> adminApi = adminApiFactory.get().create(replicateURI); + Optional<AdminApi> adminApi = + adminApiFactory.get().create(replicateURI, remoteConfig.getName()); if (adminApi.isPresent()) { adminApi.get().updateHead(project, newHead); return;
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index 150810e..a575f03 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -342,7 +342,9 @@ : Path of the `git-receive-pack` executable on the remote system, if using the SSH transport. - Defaults to `git-receive-pack`. + If not set and `remote.NAME.gitPath` is configured, defaults to + `git-receive-pack` in the directory of `gitPath`. Otherwise defaults + to `git-receive-pack`. remote.NAME.storeRefLog : `true` if the remote repositories should be enabled for storing @@ -359,7 +361,9 @@ : Path of the `git-upload-pack` executable on the remote system, if using the SSH transport. - Defaults to `git-upload-pack`. + If not set and `remote.NAME.gitPath` is configured, defaults to + `git-upload-pack` in the directory of `gitPath`. Otherwise defaults + to `git-upload-pack`. remote.NAME.push : Standard Git refspec denoting what should be replicated. @@ -500,6 +504,21 @@ By default, true, missing repositories are created. +remote.NAME.gitPath +: Absolute path to the `git` binary on this SSH destination, used when + creating a missing repository or updating its HEAD. Set this when the + non-interactive SSH session on the remote host does not have `git` + in its `PATH`. + + When set, the directory of `gitPath` is also used as the default + location for `git-upload-pack` and `git-receive-pack`, unless + `remote.NAME.uploadpack` or `remote.NAME.receivepack` are configured + explicitly. + + Only applies to SSH destinations. + + Default: `git` (resolved via the remote's `PATH`) + remote.NAME.replicatePermissions : If true, permissions-only projects and the refs/meta/config branch will also be replicated to the remote site. These