Merge "Allow batch-fetch endpoint to delete refs"
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/AutoValueTypeAdapterFactory.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/AutoValueTypeAdapterFactory.java
new file mode 100644
index 0000000..264b87e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/AutoValueTypeAdapterFactory.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2024 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.googlesource.gerrit.plugins.replication.pull;
+
+import com.google.gson.TypeAdapterFactory;
+import com.ryanharter.auto.value.gson.GsonTypeAdapterFactory;
+
+@GsonTypeAdapterFactory
+public abstract class AutoValueTypeAdapterFactory implements TypeAdapterFactory {
+
+ public static TypeAdapterFactory create() {
+ return new AutoValueGson_AutoValueTypeAdapterFactory();
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
index a058426..ddc6258 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationModule.java
@@ -40,6 +40,7 @@
import com.googlesource.gerrit.plugins.replication.ObservableQueue;
import com.googlesource.gerrit.plugins.replication.ReplicationConfigModule;
import com.googlesource.gerrit.plugins.replication.StartReplicationCapability;
+import com.googlesource.gerrit.plugins.replication.pull.api.DeleteRefJob;
import com.googlesource.gerrit.plugins.replication.pull.api.FetchApiCapability;
import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob;
import com.googlesource.gerrit.plugins.replication.pull.auth.PullReplicationGroupModule;
@@ -82,6 +83,7 @@
bind(RevisionReader.class).in(Scopes.SINGLETON);
bind(ApplyObject.class);
install(new FactoryModuleBuilder().build(FetchJob.Factory.class));
+ install(new FactoryModuleBuilder().build(DeleteRefJob.Factory.class));
install(new ApplyObjectCacheModule());
install(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
index baeb330..d9b47d3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
@@ -40,6 +40,7 @@
import com.google.inject.Singleton;
import com.googlesource.gerrit.plugins.replication.ObservableQueue;
import com.googlesource.gerrit.plugins.replication.pull.FetchResultProcessing.GitUpdateProcessing;
+import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.RefInput;
import com.googlesource.gerrit.plugins.replication.pull.api.data.BatchApplyObjectData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException;
@@ -611,13 +612,13 @@
boolean resultIsSuccessful = true;
- List<String> filteredRefs =
+ List<RefInput> filteredRefs =
refs.stream()
- .map(ReferenceUpdatedEvent::refName)
- .filter(refName -> source.wouldFetchProject(project) && source.wouldFetchRef(refName))
+ .map(ref -> RefInput.create(ref.refName(), ref.isDelete()))
+ .filter(ref -> source.wouldFetchProject(project) && source.wouldFetchRef(ref.refName()))
.collect(Collectors.toList());
- String refsStr = String.join(",", filteredRefs);
+ String refsStr = filteredRefs.stream().map(RefInput::refName).collect(Collectors.joining(","));
FetchApiClient fetchClient = fetchClientFactory.create(source);
for (String apiUrl : source.getApis()) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
index 3a7d0ff..df13944 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefCommand.java
@@ -38,6 +38,7 @@
import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState;
import java.io.IOException;
import java.util.Optional;
+import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
@@ -67,6 +68,18 @@
this.gitManager = gitManagerProvider.get();
}
+ public void deleteRefsSync(
+ Project.NameKey name, Set<String> deletedRefNames, String sourceLabel) {
+ deletedRefNames.forEach(
+ r -> {
+ try {
+ deleteRef(name, r, sourceLabel);
+ } catch (RestApiException | IOException e) {
+ repLog.error("Could not delete ref {}:{} from source {}", name.get(), r, sourceLabel);
+ }
+ });
+ }
+
public void deleteRef(Project.NameKey name, String refName, String sourceLabel)
throws IOException, RestApiException {
Source source =
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefJob.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefJob.java
new file mode 100644
index 0000000..acc40cb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/DeleteRefJob.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2024 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.googlesource.gerrit.plugins.replication.pull.api;
+
+import com.google.gerrit.entities.Project;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.BatchInput;
+
+public class DeleteRefJob implements Runnable {
+ public interface Factory {
+ DeleteRefJob create(Project.NameKey project, BatchInput input);
+ }
+
+ private final DeleteRefCommand command;
+ private final Project.NameKey project;
+ private final BatchInput batchInput;
+
+ @Inject
+ public DeleteRefJob(
+ DeleteRefCommand command,
+ @Assisted Project.NameKey project,
+ @Assisted BatchInput batchInput) {
+ this.command = command;
+ this.project = project;
+ this.batchInput = batchInput;
+ }
+
+ @Override
+ public void run() {
+ command.deleteRefsSync(project, batchInput.getDeletedRefNames(), batchInput.label);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
index 38d0cdd..9cb9285 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchAction.java
@@ -16,7 +16,9 @@
import static com.google.common.base.Preconditions.checkState;
+import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -30,6 +32,9 @@
import com.google.gerrit.server.git.WorkQueue.Task;
import com.google.gerrit.server.ioutil.HexFormat;
import com.google.gerrit.server.project.ProjectResource;
+import com.google.gson.Gson;
+import com.google.gson.TypeAdapter;
+import com.google.gson.annotations.SerializedName;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.Input;
@@ -39,6 +44,7 @@
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jgit.errors.TransportException;
@@ -46,43 +52,92 @@
@Singleton
public class FetchAction implements RestModifyView<ProjectResource, Input> {
private final FetchCommand command;
+ private final DeleteRefCommand deleteRefCommand;
private final WorkQueue workQueue;
private final DynamicItem<UrlFormatter> urlFormatter;
private final FetchPreconditions preConditions;
private final Factory fetchJobFactory;
+ private final DeleteRefJob.Factory deleteJobFactory;
@Inject
public FetchAction(
FetchCommand command,
+ DeleteRefCommand deleteRefCommand,
WorkQueue workQueue,
DynamicItem<UrlFormatter> urlFormatter,
FetchPreconditions preConditions,
- FetchJob.Factory fetchJobFactory) {
+ FetchJob.Factory fetchJobFactory,
+ DeleteRefJob.Factory deleteJobFactory) {
this.command = command;
+ this.deleteRefCommand = deleteRefCommand;
this.workQueue = workQueue;
this.urlFormatter = urlFormatter;
this.preConditions = preConditions;
this.fetchJobFactory = fetchJobFactory;
+ this.deleteJobFactory = deleteJobFactory;
}
public static class Input {
public String label;
public String refName;
public boolean async;
+ public boolean isDelete;
+ }
+
+ @AutoValue
+ public abstract static class RefInput {
+ public static final Predicate<RefInput> IS_DELETE = RefInput::isDelete;
+
+ @Nullable
+ @SerializedName("ref_name")
+ public abstract String refName();
+
+ @SerializedName("is_delete")
+ public abstract boolean isDelete();
+
+ public static RefInput create(@Nullable String refName, boolean isDelete) {
+ return new AutoValue_FetchAction_RefInput(refName, isDelete);
+ }
+
+ public static RefInput create(@Nullable String refName) {
+ return new AutoValue_FetchAction_RefInput(refName, false);
+ }
+
+ public static TypeAdapter<RefInput> typeAdapter(Gson gson) {
+ return new AutoValue_FetchAction_RefInput.GsonTypeAdapter(gson);
+ }
}
public static class BatchInput {
public String label;
- public Set<String> refsNames;
+ public Set<RefInput> refInputs;
public boolean async;
public static BatchInput fromInput(Input... input) {
BatchInput batchInput = new BatchInput();
batchInput.async = input[0].async;
batchInput.label = input[0].label;
- batchInput.refsNames = Stream.of(input).map(i -> i.refName).collect(Collectors.toSet());
+ batchInput.refInputs =
+ Stream.of(input)
+ .map(i -> RefInput.create(i.refName, i.isDelete))
+ .collect(Collectors.toSet());
return batchInput;
}
+
+ private Set<String> getFilteredRefNames(Predicate<RefInput> filterFunc) {
+ return refInputs.stream()
+ .filter(filterFunc)
+ .map(RefInput::refName)
+ .collect(Collectors.toSet());
+ }
+
+ public Set<String> getNonDeletedRefNames() {
+ return getFilteredRefNames(RefInput.IS_DELETE.negate());
+ }
+
+ public Set<String> getDeletedRefNames() {
+ return getFilteredRefNames(RefInput.IS_DELETE);
+ }
}
@Override
@@ -101,12 +156,12 @@
throw new BadRequestException("Source label cannot be null or empty");
}
- if (batchInput.refsNames.isEmpty()) {
+ if (batchInput.refInputs.isEmpty()) {
throw new BadRequestException("Ref-update refname cannot be null or empty");
}
- for (String refName : batchInput.refsNames) {
- if (Strings.isNullOrEmpty(refName)) {
+ for (RefInput input : batchInput.refInputs) {
+ if (Strings.isNullOrEmpty(input.refName())) {
throw new BadRequestException("Ref-update refname cannot be null or empty");
}
}
@@ -129,7 +184,16 @@
private Response<?> applySync(Project.NameKey project, BatchInput input)
throws InterruptedException, ExecutionException, RemoteConfigurationMissingException,
TimeoutException, TransportException {
- command.fetchSync(project, input.label, input.refsNames);
+ command.fetchSync(project, input.label, input.getNonDeletedRefNames());
+
+ /* git fetches and deletes cannot be handled atomically within the same transaction.
+ Here we choose to handle fetches first and then deletes:
+ - If the fetch fails delete is not even attempted.
+ - If the delete fails after the fetch then the client is left with some extra refs.
+ */
+ if (!input.getDeletedRefNames().isEmpty()) {
+ deleteRefCommand.deleteRefsSync(project, input.getDeletedRefNames(), input.label);
+ }
return Response.created(input);
}
@@ -146,6 +210,10 @@
urlFormatter
.get()
.getRestUrl("a/config/server/tasks/" + HexFormat.fromInt(task.getTaskId()));
+
+ if (!batchInput.getDeletedRefNames().isEmpty()) {
+ workQueue.getDefaultQueue().submit(deleteJobFactory.create(project, batchInput));
+ }
// We're in a HTTP handler, so must be present.
checkState(url.isPresent());
return Response.accepted(url.get());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchJob.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchJob.java
index 0975045..d30ba74 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchJob.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchJob.java
@@ -52,7 +52,7 @@
@Override
public void run() {
try {
- command.fetchAsync(project, batchInput.label, batchInput.refsNames, metrics);
+ command.fetchAsync(project, batchInput.label, batchInput.getNonDeletedRefNames(), metrics);
} catch (InterruptedException
| ExecutionException
| RemoteConfigurationMissingException
@@ -60,7 +60,7 @@
| TransportException e) {
log.atSevere().withCause(e).log(
"Exception during the async fetch call for project %s, label %s and ref(s) name(s) %s",
- project.get(), batchInput.label, batchInput.refsNames);
+ project.get(), batchInput.label, batchInput.getNonDeletedRefNames());
}
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpPayloadGsonProvider.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpPayloadGsonProvider.java
new file mode 100644
index 0000000..cf1bd86
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/HttpPayloadGsonProvider.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2024 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.googlesource.gerrit.plugins.replication.pull.api;
+
+import com.google.gerrit.json.OutputFormat;
+import com.google.gson.Gson;
+import com.googlesource.gerrit.plugins.replication.pull.AutoValueTypeAdapterFactory;
+
+public class HttpPayloadGsonProvider {
+
+ public static Gson get() {
+ return OutputFormat.JSON
+ .newGsonBuilder()
+ .registerTypeAdapterFactory(AutoValueTypeAdapterFactory.create())
+ .create();
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
index 368e61a..9618168 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
@@ -38,7 +38,6 @@
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.httpd.AllRequestFilter;
import com.google.gerrit.httpd.restapi.RestApiServlet;
-import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -117,7 +116,7 @@
this.projectDeletionAction = projectDeletionAction;
this.projectCache = projectCache;
this.pluginName = pluginName;
- this.gson = OutputFormat.JSON.newGsonBuilder().create();
+ this.gson = HttpPayloadGsonProvider.get();
this.currentUserProvider = currentUserProvider;
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/util/PayloadSerDes.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/util/PayloadSerDes.java
index 414af37..d188247 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/util/PayloadSerDes.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/util/PayloadSerDes.java
@@ -22,11 +22,11 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.httpd.restapi.RestApiServlet;
-import com.google.gerrit.json.OutputFormat;
import com.google.gson.Gson;
import com.google.gson.stream.JsonReader;
import com.google.inject.TypeLiteral;
import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction;
+import com.googlesource.gerrit.plugins.replication.pull.api.HttpPayloadGsonProvider;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionsInput;
import java.io.BufferedReader;
@@ -38,7 +38,7 @@
public class PayloadSerDes {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private static final Gson gson = OutputFormat.JSON.newGsonBuilder().create();
+ private static final Gson gson = HttpPayloadGsonProvider.get();
public static RevisionInput parseRevisionInput(HttpServletRequest httpRequest)
throws BadRequestException, IOException {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java
index 28c74c9..b908435 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchApiClient.java
@@ -19,6 +19,7 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.Project.NameKey;
import com.googlesource.gerrit.plugins.replication.pull.Source;
+import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.RefInput;
import com.googlesource.gerrit.plugins.replication.pull.api.data.BatchApplyObjectData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
import java.io.IOException;
@@ -46,11 +47,11 @@
}
HttpResult callBatchFetch(
- Project.NameKey project, List<String> refsInBatch, URIish targetUri, long startTimeNanos)
+ Project.NameKey project, List<RefInput> refsInBatch, URIish targetUri, long startTimeNanos)
throws IOException;
default HttpResult callBatchFetch(
- Project.NameKey project, List<String> refsInBatch, URIish targetUri) throws IOException {
+ Project.NameKey project, List<RefInput> refsInBatch, URIish targetUri) throws IOException {
return callBatchFetch(
project, refsInBatch, targetUri, MILLISECONDS.toNanos(System.currentTimeMillis()));
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
index 614774d..cb03606 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
@@ -36,6 +36,7 @@
import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider;
import com.googlesource.gerrit.plugins.replication.pull.Source;
+import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.RefInput;
import com.googlesource.gerrit.plugins.replication.pull.api.PullReplicationApiRequestMetrics;
import com.googlesource.gerrit.plugins.replication.pull.api.data.BatchApplyObjectData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
@@ -151,19 +152,22 @@
return executeRequest(post, bearerTokenProvider.get(), targetUri);
}
- private Boolean containsSyncFetchRef(List<String> refsInBatch) {
- return refsInBatch.stream().anyMatch(syncRefsFilter::match);
+ private Boolean containsSyncFetchRef(List<RefInput> refsInBatch) {
+ return refsInBatch.stream().anyMatch(r -> syncRefsFilter.match(r.refName()));
}
@Override
public HttpResult callBatchFetch(
- NameKey project, List<String> refsInBatch, URIish targetUri, long startTimeNanos)
+ NameKey project, List<RefInput> refsInBatch, URIish targetUri, long startTimeNanos)
throws IOException {
boolean callAsync = !containsSyncFetchRef(refsInBatch);
- String refsNamesBody = refsInBatch.stream().collect(Collectors.joining("\",\"", "\"", "\""));
+ String refsNamesBody =
+ refsInBatch.stream()
+ .map(r -> "{\"ref_name\":\"" + r.refName() + "\", \"is_delete\":" + r.isDelete() + "}")
+ .collect(Collectors.joining(","));
String msgBody =
String.format(
- "{\"label\":\"%s\", \"refs_names\": [ %s ], \"async\":%s}",
+ "{\"label\":\"%s\", \"ref_inputs\": [ %s ], \"async\":%s}",
instanceId, refsNamesBody, callAsync);
String url = formatUrl(targetUri.toString(), project, "batch-fetch");
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index abc96ca..3b76be9 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -609,6 +609,10 @@
> *NOTE*: if any ref from a single batch matches `replication.syncRefs`
> filter, all refs in that batch are going to be fetched synchronously as
> a single git fetch operation.
+
+> *NOTE*: Should ref deletions over apply-object/HTTP, they will
+> be attempted over fetch/HTTP endpoint only when `enableBatchedRefs` is
+> enabled.
>
> By default, true.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
index 84fee55..f494c2f 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
@@ -51,6 +51,7 @@
import com.googlesource.gerrit.plugins.replication.MergedConfigResource;
import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
import com.googlesource.gerrit.plugins.replication.ReplicationConfigImpl;
+import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.RefInput;
import com.googlesource.gerrit.plugins.replication.pull.api.data.BatchApplyObjectData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
import com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient;
@@ -65,6 +66,7 @@
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -503,7 +505,7 @@
verify(fetchRestApiClient)
.callBatchFetch(
PROJECT,
- List.of("refs/changes/01/1/1", "refs/changes/02/1/1"),
+ Stream.of("refs/changes/01/1/1", "refs/changes/02/1/1").map(RefInput::create).toList(),
new URIish("http://localhost:18080"));
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BatchFetchActionTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BatchFetchActionTest.java
index 9dc736f..23b0fe1 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BatchFetchActionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/BatchFetchActionTest.java
@@ -22,11 +22,14 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.project.ProjectResource;
-import java.util.Set;
+import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.RefInput;
+import java.util.Arrays;
+import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -103,10 +106,12 @@
return input;
}
+ @VisibleForTesting
private FetchAction.BatchInput createBatchInput(String... refNames) {
FetchAction.BatchInput batchInput = new FetchAction.BatchInput();
batchInput.label = label;
- batchInput.refsNames = Set.of(refNames);
+ batchInput.refInputs =
+ Arrays.stream(refNames).map(RefInput::create).collect(Collectors.toSet());
return batchInput;
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
index 9653209..a4642ba 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchActionTest.java
@@ -19,7 +19,9 @@
import static org.apache.http.HttpStatus.SC_CREATED;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.gerrit.extensions.registration.DynamicItem;
@@ -32,6 +34,8 @@
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.git.WorkQueue.Task;
import com.google.gerrit.server.project.ProjectResource;
+import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.BatchInput;
+import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.RefInput;
import com.googlesource.gerrit.plugins.replication.pull.api.exception.RemoteConfigurationMissingException;
import java.util.Optional;
import java.util.Set;
@@ -57,8 +61,11 @@
int taskId = 1234;
@Mock FetchCommand fetchCommand;
+ @Mock DeleteRefCommand deleteRefCommand;
@Mock FetchJob fetchJob;
+ @Mock DeleteRefJob deleteRefJob;
@Mock FetchJob.Factory fetchJobFactory;
+ @Mock DeleteRefJob.Factory deleteRefJobFactory;
@Mock ProjectResource projectResource;
@Mock WorkQueue workQueue;
@Mock ScheduledExecutorService exceutorService;
@@ -70,6 +77,7 @@
@Before
public void setup() throws Exception {
when(fetchJobFactory.create(any(), any(), any())).thenReturn(fetchJob);
+ when(deleteRefJobFactory.create(any(), any())).thenReturn(deleteRefJob);
when(workQueue.getDefaultQueue()).thenReturn(exceutorService);
when(urlFormatter.getRestUrl(anyString())).thenReturn(Optional.of(location));
when(exceutorService.submit(any(Runnable.class)))
@@ -86,7 +94,13 @@
fetchAction =
new FetchAction(
- fetchCommand, workQueue, urlFormatterDynamicItem, preConditions, fetchJobFactory);
+ fetchCommand,
+ deleteRefCommand,
+ workQueue,
+ urlFormatterDynamicItem,
+ preConditions,
+ fetchJobFactory,
+ deleteRefJobFactory);
}
@Test
@@ -104,13 +118,38 @@
public void shouldReturnCreatedResponseCodeForBatchRefFetchAction() throws Exception {
FetchAction.BatchInput batchInputParams = new FetchAction.BatchInput();
batchInputParams.label = label;
- batchInputParams.refsNames = Set.of(refName, altRefName);
+ batchInputParams.refInputs = Set.of(RefInput.create(refName), RefInput.create(altRefName));
Response<?> response = fetchAction.apply(projectResource, batchInputParams);
assertThat(response.statusCode()).isEqualTo(SC_CREATED);
}
+ @Test
+ public void shouldDeleteRefSync() throws Exception {
+ FetchAction.BatchInput batchInputParams = new FetchAction.BatchInput();
+ batchInputParams.label = label;
+ batchInputParams.refInputs = Set.of(RefInput.create(refName, true));
+
+ Response<?> response = fetchAction.apply(projectResource, batchInputParams);
+ verify(deleteRefCommand).deleteRefsSync(any(), eq(Set.of(refName)), eq(label));
+
+ assertThat(response.statusCode()).isEqualTo(SC_CREATED);
+ }
+
+ @Test
+ public void shouldDeleteRefAsync() throws Exception {
+ FetchAction.BatchInput batchInputParams = new FetchAction.BatchInput();
+ batchInputParams.label = label;
+ batchInputParams.async = true;
+ batchInputParams.refInputs = Set.of(RefInput.create(refName, true));
+
+ Response<?> response = fetchAction.apply(projectResource, batchInputParams);
+ verify(deleteRefJobFactory).create(any(), eq(batchInputParams));
+
+ assertThat(response.statusCode()).isEqualTo(SC_ACCEPTED);
+ }
+
@SuppressWarnings("cast")
@Test
public void shouldReturnSourceUrlAndrefNameAsAResponseBody() throws Exception {
@@ -120,11 +159,11 @@
Response<?> response = fetchAction.apply(projectResource, inputParams);
- FetchAction.BatchInput responseBatchInput = (FetchAction.BatchInput) response.value();
+ BatchInput responseBatchInput = (BatchInput) response.value();
assertThat(responseBatchInput.label).isEqualTo(inputParams.label);
assertThat(responseBatchInput.async).isEqualTo(inputParams.async);
- assertThat(responseBatchInput.refsNames).containsExactly(inputParams.refName);
+ assertThat(responseBatchInput.refInputs).containsExactly(RefInput.create(inputParams.refName));
}
@Test(expected = BadRequestException.class)
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
index b193cd7..bc9e870 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
@@ -145,7 +145,7 @@
byte[] payloadBatchFetch =
("{"
+ "\"label\":\"Replication\", "
- + "\"refs_names\": [ \"refs/heads/master\" , \"refs/heads/test\" ], "
+ + "\"ref_inputs\": [ {\"ref_name\":\"refs/heads/master\", \"is_delete\":false}, {\"ref_name\":\"refs/heads/test\", \"is_delete\":false} ], "
+ "\"async\":false"
+ "}")
.getBytes(StandardCharsets.UTF_8);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java
index 3aa5b5a..1c7b553 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientBase.java
@@ -31,6 +31,7 @@
import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
import com.googlesource.gerrit.plugins.replication.pull.BearerTokenProvider;
import com.googlesource.gerrit.plugins.replication.pull.Source;
+import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.RefInput;
import com.googlesource.gerrit.plugins.replication.pull.api.data.BatchApplyObjectData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionObjectData;
@@ -42,6 +43,7 @@
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.HttpDelete;
@@ -185,7 +187,7 @@
objectUnderTest.callBatchFetch(
Project.nameKey("test_repo"),
- List.of(refName, RefNames.REFS_HEADS + "test"),
+ List.of(RefInput.create(refName), RefInput.create(RefNames.REFS_HEADS + "test")),
new URIish(api));
verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any());
@@ -253,19 +255,21 @@
source);
String testRef = RefNames.REFS_HEADS + "test";
- List<String> refs = List.of(refName, testRef);
+ List<RefInput> refs = refInputs(refName, testRef);
objectUnderTest.callBatchFetch(Project.nameKey("test_repo"), refs, new URIish(api));
verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any());
HttpPost httpPost = httpPostCaptor.getValue();
String expectedPayload =
- "{\"label\":\"Replication\", \"refs_names\": [ "
- + '"'
+ "{\"label\":\"Replication\", \"ref_inputs\": ["
+ + " {\"ref_name\":\""
+ refName
- + "\",\""
+ + "\", \"is_delete\":false}"
+ + ",{\"ref_name\":\""
+ testRef
- + "\" ]"
+ + "\", \"is_delete\":false}"
+ + " ]"
+ ", \"async\":true}";
assertThat(readPayload(httpPost)).isEqualTo(expectedPayload);
}
@@ -305,7 +309,10 @@
public void shouldCallSyncBatchFetchOnlyForMetaRef() throws Exception {
String metaRefName = "refs/changes/01/101/meta";
String expectedMetaRefPayload =
- "{\"label\":\"Replication\", \"refs_names\": [ \"" + metaRefName + "\" ], \"async\":false}";
+ "{\"label\":\"Replication\", \"ref_inputs\": [ "
+ + "{\"ref_name\":\""
+ + metaRefName
+ + "\", \"is_delete\":false} ], \"async\":false}";
when(config.getStringList("replication", null, "syncRefs"))
.thenReturn(new String[] {"^refs\\/changes\\/.*\\/meta"});
@@ -322,7 +329,7 @@
source);
objectUnderTest.callBatchFetch(
- Project.nameKey("test_repo"), List.of(metaRefName), new URIish(api));
+ Project.nameKey("test_repo"), List.of(RefInput.create(metaRefName)), new URIish(api));
verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any());
HttpPost httpPost = httpPostCaptor.getValue();
assertThat(readPayload(httpPost)).isEqualTo(expectedMetaRefPayload);
@@ -343,19 +350,23 @@
public void shouldCallBatchFetchEndpointWithPayload() throws Exception {
String testRef = RefNames.REFS_HEADS + "test";
- List<String> refs = List.of(refName, testRef);
+ List<RefInput> refs = refInputs(refName, testRef);
objectUnderTest.callBatchFetch(Project.nameKey("test_repo"), refs, new URIish(api));
verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any());
HttpPost httpPost = httpPostCaptor.getValue();
String expectedPayload =
- "{\"label\":\"Replication\", \"refs_names\": [ "
- + '"'
+ "{\"label\":\"Replication\", \"ref_inputs\": [ "
+ + "{\"ref_name\":\""
+ refName
- + "\",\""
- + refs.get(1)
- + "\" ], \"async\":false}";
+ + "\", \"is_delete\":false}"
+ + ",{\"ref_name\":\""
+ + refs.get(1).refName()
+ + "\", \"is_delete\":"
+ + refs.get(1).isDelete()
+ + "}"
+ + " ], \"async\":false}";
assertThat(readPayload(httpPost)).isEqualTo(expectedPayload);
}
@@ -366,7 +377,7 @@
.thenReturn(new String[] {"^refs\\/heads\\/test"});
syncRefsFilter = new SyncRefsFilter(replicationConfig);
String testRef = RefNames.REFS_HEADS + "test";
- List<String> refs = List.of(refName, testRef);
+ List<RefInput> refs = refInputs(refName, testRef);
objectUnderTest =
new FetchRestApiClient(
credentials,
@@ -383,8 +394,16 @@
HttpPost httpPosts = httpPostCaptor.getValue();
String expectedSyncPayload =
- "{\"label\":\"Replication\", \"refs_names\": [ "
- + refs.stream().map(r -> '"' + r + '"').collect(Collectors.joining(","))
+ "{\"label\":\"Replication\", \"ref_inputs\": [ "
+ + refs.stream()
+ .map(
+ r ->
+ "{\"ref_name\":\""
+ + r.refName()
+ + "\", \"is_delete\":"
+ + r.isDelete()
+ + "}")
+ .collect(Collectors.joining(","))
+ " ], \"async\":false}";
assertThat(readPayload(httpPosts)).isEqualTo(expectedSyncPayload);
@@ -405,7 +424,8 @@
@Test
public void shouldSetContentTypeHeaderInBatchFetch() throws Exception {
- objectUnderTest.callBatchFetch(Project.nameKey("test_repo"), List.of(refName), new URIish(api));
+ objectUnderTest.callBatchFetch(
+ Project.nameKey("test_repo"), refInputs(refName), new URIish(api));
verify(httpClient, times(1)).execute(httpPostCaptor.capture(), any());
@@ -733,4 +753,8 @@
return createSampleRevisionData(
commitObjectId, commitObject, treeObjectId, treeObject, blobObjectId, blobObject);
}
+
+ private List<RefInput> refInputs(String... refs) {
+ return Stream.of(refs).map(RefInput::create).collect(Collectors.toList());
+ }
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java
index 0f4bd33..c81820e 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/event/StreamEventListenerTest.java
@@ -186,7 +186,7 @@
FetchAction.BatchInput batchInput = batchInputCaptor.getValue();
assertThat(batchInput.label).isEqualTo(REMOTE_INSTANCE_ID);
- assertThat(batchInput.refsNames).contains(TEST_REF_NAME);
+ assertThat(batchInput.refInputs).contains(FetchAction.RefInput.create(TEST_REF_NAME));
verify(executor).submit(any(FetchJob.class));
}
@@ -259,7 +259,7 @@
FetchAction.BatchInput input = batchInputCaptor.getValue();
assertThat(input.label).isEqualTo(REMOTE_INSTANCE_ID);
- assertThat(input.refsNames).contains(FetchOne.ALL_REFS);
+ assertThat(input.refInputs).contains(FetchAction.RefInput.create(FetchOne.ALL_REFS));
verify(executor).submit(any(FetchJob.class));
}