Merge "Remove events from beforeStartupEventsQueue in-memory queue" into stable-3.6
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 fe6a17c..ef95596 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
@@ -521,7 +521,20 @@
private HttpResult initProject(
Project.NameKey project, URIish uri, FetchApiClient fetchClient, HttpResult result)
throws IOException, ClientProtocolException {
- HttpResult initProjectResult = fetchClient.initProject(project, uri);
+ RevisionData refsMetaConfigRevisionData =
+ revReaderProvider
+ .get()
+ .read(project, null, RefNames.REFS_CONFIG, 0)
+ .orElseThrow(
+ () ->
+ new IllegalStateException(
+ String.format(
+ "Project %s does not have %s", project, RefNames.REFS_CONFIG)));
+
+ List<RevisionData> refsMetaConfigDataList =
+ fetchWholeMetaHistory(project, RefNames.REFS_CONFIG, refsMetaConfigRevisionData);
+ HttpResult initProjectResult =
+ fetchClient.initProject(project, uri, System.currentTimeMillis(), refsMetaConfigDataList);
if (initProjectResult.isSuccessful()) {
result = fetchClient.callFetch(project, FetchOne.ALL_REFS, uri);
} else {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java
index 8711379..e7289ac 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectInitializationAction.java
@@ -14,25 +14,39 @@
package com.googlesource.gerrit.plugins.replication.pull.api;
+import static com.googlesource.gerrit.plugins.replication.pull.PullReplicationLogger.repLog;
import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.checkAcceptHeader;
import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.setResponse;
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
+import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
+import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
+import com.google.common.net.MediaType;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.index.project.ProjectIndexer;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.googlesource.gerrit.plugins.replication.LocalFS;
import com.googlesource.gerrit.plugins.replication.pull.GerritConfigOps;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionsInput;
+import com.googlesource.gerrit.plugins.replication.pull.api.exception.MissingParentObjectException;
+import com.googlesource.gerrit.plugins.replication.pull.api.exception.RefUpdateException;
+import com.googlesource.gerrit.plugins.replication.pull.api.util.PayloadSerDes;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
import java.util.Optional;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
@@ -51,17 +65,23 @@
private final Provider<CurrentUser> userProvider;
private final PermissionBackend permissionBackend;
private final ProjectIndexer projectIndexer;
+ private final ApplyObjectCommand applyObjectCommand;
+ private final ProjectCache projectCache;
@Inject
ProjectInitializationAction(
GerritConfigOps gerritConfigOps,
Provider<CurrentUser> userProvider,
PermissionBackend permissionBackend,
- ProjectIndexer projectIndexer) {
+ ProjectIndexer projectIndexer,
+ ApplyObjectCommand applyObjectCommand,
+ ProjectCache projectCache) {
this.gerritConfigOps = gerritConfigOps;
this.userProvider = userProvider;
this.permissionBackend = permissionBackend;
this.projectIndexer = projectIndexer;
+ this.applyObjectCommand = applyObjectCommand;
+ this.projectCache = projectCache;
}
@Override
@@ -73,47 +93,140 @@
return;
}
- String path = httpServletRequest.getRequestURI();
- String projectName = Url.decode(path.substring(path.lastIndexOf('/') + 1));
+ String gitRepositoryName = getGitRepositoryName(httpServletRequest);
try {
- if (initProject(projectName)) {
+ boolean initProjectStatus;
+ String contentType = httpServletRequest.getContentType();
+ if (checkContentType(contentType, MediaType.JSON_UTF_8)) {
+ // init project request includes project configuration in JSON format.
+ initProjectStatus = initProjectWithConfiguration(httpServletRequest, gitRepositoryName);
+ } else if (checkContentType(contentType, MediaType.PLAIN_TEXT_UTF_8)) {
+ // init project request does not include project configuration.
+ initProjectStatus = initProject(gitRepositoryName);
+ } else {
+ setResponse(
+ httpServletResponse,
+ SC_BAD_REQUEST,
+ String.format(
+ "Invalid Content Type. Only %s or %s is supported.",
+ MediaType.JSON_UTF_8.toString(), MediaType.PLAIN_TEXT_UTF_8.toString()));
+ return;
+ }
+
+ if (initProjectStatus) {
setResponse(
httpServletResponse,
HttpServletResponse.SC_CREATED,
- "Project " + projectName + " initialized");
+ "Project " + gitRepositoryName + " initialized");
return;
}
- } catch (AuthException | PermissionBackendException e) {
+
setResponse(
httpServletResponse,
- HttpServletResponse.SC_FORBIDDEN,
- "User not authorized to create project " + projectName);
- return;
+ HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+ "Cannot initialize project " + gitRepositoryName);
+ } catch (BadRequestException | IllegalArgumentException e) {
+ logExceptionAndUpdateResponse(httpServletResponse, e, SC_BAD_REQUEST, gitRepositoryName);
+ } catch (RefUpdateException | MissingParentObjectException e) {
+ logExceptionAndUpdateResponse(httpServletResponse, e, SC_CONFLICT, gitRepositoryName);
+ } catch (AuthException | PermissionBackendException e) {
+ logExceptionAndUpdateResponse(httpServletResponse, e, SC_FORBIDDEN, gitRepositoryName);
}
-
- setResponse(
- httpServletResponse,
- HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
- "Cannot initialize project " + projectName);
}
- public boolean initProject(String projectName) throws AuthException, PermissionBackendException {
+ public boolean initProject(String gitRepositoryName)
+ throws AuthException, PermissionBackendException {
+ if (initProject(gitRepositoryName, true)) {
+ repLog.info("Init project API from {}", gitRepositoryName);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean initProjectWithConfiguration(
+ HttpServletRequest httpServletRequest, String gitRepositoryName)
+ throws AuthException, PermissionBackendException, IOException, BadRequestException,
+ MissingParentObjectException, RefUpdateException {
+
+ RevisionsInput input = PayloadSerDes.parseRevisionsInput(httpServletRequest);
+ validateInput(input);
+ if (!initProject(gitRepositoryName, false)) {
+ return false;
+ }
+
+ String projectName = gitRepositoryName.replace(".git", "");
+ applyObjectCommand.applyObjects(
+ Project.nameKey(projectName),
+ input.getRefName(),
+ input.getRevisionsData(),
+ input.getLabel(),
+ input.getEventCreatedOn());
+ projectCache.onCreateProject(Project.nameKey(projectName));
+ repLog.info(
+ "Init project API from {} for {}:{} - {}",
+ input.getLabel(),
+ projectName,
+ input.getRefName(),
+ Arrays.toString(input.getRevisionsData()));
+ return true;
+ }
+
+ private boolean initProject(String gitRepositoryName, boolean needsProjectReindexing)
+ throws AuthException, PermissionBackendException {
// When triggered internally(for example by consuming stream events) user is not provided
// and internal user is returned. Project creation should be always allowed for internal user.
if (!userProvider.get().isInternalUser()) {
permissionBackend.user(userProvider.get()).check(GlobalPermission.CREATE_PROJECT);
}
- Optional<URIish> maybeUri = gerritConfigOps.getGitRepositoryURI(projectName);
+ Optional<URIish> maybeUri = gerritConfigOps.getGitRepositoryURI(gitRepositoryName);
if (!maybeUri.isPresent()) {
- logger.atSevere().log("Cannot initialize project '%s'", projectName);
+ logger.atSevere().log("Cannot initialize project '%s'", gitRepositoryName);
return false;
}
LocalFS localFS = new LocalFS(maybeUri.get());
- Project.NameKey projectNameKey = Project.NameKey.parse(projectName);
+ Project.NameKey projectNameKey = Project.NameKey.parse(gitRepositoryName);
if (localFS.createProject(projectNameKey, RefNames.HEAD)) {
- projectIndexer.index(projectNameKey);
+ if (needsProjectReindexing) {
+ projectIndexer.index(projectNameKey);
+ }
return true;
}
return false;
}
+
+ private void validateInput(RevisionsInput input) {
+
+ if (Strings.isNullOrEmpty(input.getLabel())) {
+ throw new IllegalArgumentException("Source label cannot be null or empty");
+ }
+
+ if (!Objects.equals(input.getRefName(), RefNames.REFS_CONFIG)) {
+ throw new IllegalArgumentException(
+ String.format("Ref-update refname should be %s", RefNames.REFS_CONFIG));
+ }
+ input.validate();
+ }
+
+ private String getGitRepositoryName(HttpServletRequest httpServletRequest) {
+ String path = httpServletRequest.getRequestURI();
+ return Url.decode(path.substring(path.lastIndexOf('/') + 1));
+ }
+
+ private void logExceptionAndUpdateResponse(
+ HttpServletResponse httpServletResponse,
+ Exception e,
+ int statusCode,
+ String gitRepositoryName)
+ throws IOException {
+ repLog.error("Init Project API FAILED for {}", gitRepositoryName, e);
+ setResponse(httpServletResponse, statusCode, e.getMessage());
+ }
+
+ private boolean checkContentType(String contentType, MediaType mediaType) {
+ try {
+ return MediaType.parse(contentType).is(mediaType);
+ } catch (Exception e) {
+ return false;
+ }
+ }
}
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 5a4393c..d0e5c10 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
@@ -18,10 +18,8 @@
import static com.googlesource.gerrit.plugins.replication.pull.api.HttpServletOps.checkAcceptHeader;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
-import static javax.servlet.http.HttpServletResponse.SC_CREATED;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
-import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import com.google.common.flogger.FluentLogger;
@@ -38,28 +36,22 @@
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;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
-import com.google.gson.Gson;
import com.google.gson.JsonParseException;
-import com.google.gson.stream.JsonReader;
import com.google.gson.stream.MalformedJsonException;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import com.google.inject.TypeLiteral;
import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.Input;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionInput;
import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionsInput;
import com.googlesource.gerrit.plugins.replication.pull.api.exception.UnauthorizedAuthException;
-import java.io.BufferedReader;
-import java.io.EOFException;
+import com.googlesource.gerrit.plugins.replication.pull.api.util.PayloadSerDes;
import java.io.IOException;
-import java.io.PrintWriter;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
@@ -86,7 +78,6 @@
private UpdateHeadAction updateHEADAction;
private ProjectDeletionAction projectDeletionAction;
private ProjectCache projectCache;
- private Gson gson;
private String pluginName;
private final Provider<CurrentUser> currentUserProvider;
@@ -109,7 +100,6 @@
this.projectDeletionAction = projectDeletionAction;
this.projectCache = projectCache;
this.pluginName = pluginName;
- this.gson = OutputFormat.JSON.newGsonBuilder().create();
this.currentUserProvider = currentUserProvider;
}
@@ -126,13 +116,13 @@
try {
if (isFetchAction(httpRequest)) {
failIfcurrentUserIsAnonymous();
- writeResponse(httpResponse, doFetch(httpRequest));
+ PayloadSerDes.writeResponse(httpResponse, doFetch(httpRequest));
} else if (isApplyObjectAction(httpRequest)) {
failIfcurrentUserIsAnonymous();
- writeResponse(httpResponse, doApplyObject(httpRequest));
+ PayloadSerDes.writeResponse(httpResponse, doApplyObject(httpRequest));
} else if (isApplyObjectsAction(httpRequest)) {
failIfcurrentUserIsAnonymous();
- writeResponse(httpResponse, doApplyObjects(httpRequest));
+ PayloadSerDes.writeResponse(httpResponse, doApplyObjects(httpRequest));
} else if (isInitProjectAction(httpRequest)) {
failIfcurrentUserIsAnonymous();
if (!checkAcceptHeader(httpRequest, httpResponse)) {
@@ -141,10 +131,10 @@
doInitProject(httpRequest, httpResponse);
} else if (isUpdateHEADAction(httpRequest)) {
failIfcurrentUserIsAnonymous();
- writeResponse(httpResponse, doUpdateHEAD(httpRequest));
+ PayloadSerDes.writeResponse(httpResponse, doUpdateHEAD(httpRequest));
} else if (isDeleteProjectAction(httpRequest)) {
failIfcurrentUserIsAnonymous();
- writeResponse(httpResponse, doDeleteProject(httpRequest));
+ PayloadSerDes.writeResponse(httpResponse, doDeleteProject(httpRequest));
} else {
chain.doFilter(request, response);
}
@@ -199,7 +189,7 @@
@SuppressWarnings("unchecked")
private Response<String> doApplyObject(HttpServletRequest httpRequest)
throws RestApiException, IOException, PermissionBackendException {
- RevisionInput input = readJson(httpRequest, TypeLiteral.get(RevisionInput.class));
+ RevisionInput input = PayloadSerDes.parseRevisionInput(httpRequest);
IdString id = getProjectName(httpRequest).get();
return (Response<String>) applyObjectAction.apply(parseProjectResource(id), input);
@@ -208,7 +198,7 @@
@SuppressWarnings("unchecked")
private Response<String> doApplyObjects(HttpServletRequest httpRequest)
throws RestApiException, IOException, PermissionBackendException {
- RevisionsInput input = readJson(httpRequest, TypeLiteral.get(RevisionsInput.class));
+ RevisionsInput input = PayloadSerDes.parseRevisionsInput(httpRequest);
IdString id = getProjectName(httpRequest).get();
return (Response<String>) applyObjectsAction.apply(parseProjectResource(id), input);
@@ -216,7 +206,7 @@
@SuppressWarnings("unchecked")
private Response<String> doUpdateHEAD(HttpServletRequest httpRequest) throws Exception {
- HeadInput input = readJson(httpRequest, TypeLiteral.get(HeadInput.class));
+ HeadInput input = PayloadSerDes.parseHeadInput(httpRequest);
IdString id = getProjectName(httpRequest).get();
return (Response<String>) updateHEADAction.apply(parseProjectResource(id), input);
@@ -233,7 +223,7 @@
@SuppressWarnings("unchecked")
private Response<Map<String, Object>> doFetch(HttpServletRequest httpRequest)
throws IOException, RestApiException, PermissionBackendException {
- Input input = readJson(httpRequest, TypeLiteral.get(Input.class));
+ Input input = PayloadSerDes.parseInput(httpRequest);
IdString id = getProjectName(httpRequest).get();
return (Response<Map<String, Object>>) fetchAction.apply(parseProjectResource(id), input);
@@ -247,50 +237,6 @@
return new ProjectResource(project.get(), currentUserProvider.get());
}
- private <T> void writeResponse(HttpServletResponse httpResponse, Response<T> response)
- throws IOException {
- String responseJson = gson.toJson(response);
- if (response.statusCode() == SC_OK || response.statusCode() == SC_CREATED) {
-
- httpResponse.setContentType("application/json");
- httpResponse.setStatus(response.statusCode());
- PrintWriter writer = httpResponse.getWriter();
- writer.print(new String(RestApiServlet.JSON_MAGIC));
- writer.print(responseJson);
- } else {
- httpResponse.sendError(response.statusCode(), responseJson);
- }
- }
-
- private <T> T readJson(HttpServletRequest httpRequest, TypeLiteral<T> typeLiteral)
- throws IOException, BadRequestException {
-
- try (BufferedReader br = httpRequest.getReader();
- JsonReader json = new JsonReader(br)) {
- try {
- json.setLenient(true);
-
- try {
- json.peek();
- } catch (EOFException e) {
- throw new BadRequestException("Expected JSON object", e);
- }
-
- return gson.fromJson(json, typeLiteral.getType());
- } finally {
- try {
- // Reader.close won't consume the rest of the input. Explicitly consume the request
- // body.
- br.skip(Long.MAX_VALUE);
- } catch (Exception e) {
- // ignore, e.g. trying to consume the rest of the input may fail if the request was
- // cancelled
- logger.atFine().withCause(e).log("Exception during the parsing of the request json");
- }
- }
- }
- }
-
/**
* Return project name from request URI. Request URI format:
* /a/projects/<project_name>/pull-replication~apply-object
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
new file mode 100644
index 0000000..414af37
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/util/PayloadSerDes.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2023 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.util;
+
+import static javax.servlet.http.HttpServletResponse.SC_CREATED;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.api.projects.HeadInput;
+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.data.RevisionInput;
+import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionsInput;
+import java.io.BufferedReader;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class PayloadSerDes {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final Gson gson = OutputFormat.JSON.newGsonBuilder().create();
+
+ public static RevisionInput parseRevisionInput(HttpServletRequest httpRequest)
+ throws BadRequestException, IOException {
+ return parse(httpRequest, TypeLiteral.get(RevisionInput.class));
+ }
+
+ public static RevisionsInput parseRevisionsInput(HttpServletRequest httpRequest)
+ throws BadRequestException, IOException {
+ return parse(httpRequest, TypeLiteral.get(RevisionsInput.class));
+ }
+
+ public static HeadInput parseHeadInput(HttpServletRequest httpRequest)
+ throws BadRequestException, IOException {
+ return parse(httpRequest, TypeLiteral.get(HeadInput.class));
+ }
+
+ public static FetchAction.Input parseInput(HttpServletRequest httpRequest)
+ throws BadRequestException, IOException {
+ return parse(httpRequest, TypeLiteral.get(FetchAction.Input.class));
+ }
+
+ public static <T> void writeResponse(HttpServletResponse httpResponse, Response<T> response)
+ throws IOException {
+ String responseJson = gson.toJson(response);
+ if (response.statusCode() == SC_OK || response.statusCode() == SC_CREATED) {
+
+ httpResponse.setContentType("application/json");
+ httpResponse.setStatus(response.statusCode());
+ PrintWriter writer = httpResponse.getWriter();
+ writer.print(new String(RestApiServlet.JSON_MAGIC));
+ writer.print(responseJson);
+ } else {
+ httpResponse.sendError(response.statusCode(), responseJson);
+ }
+ }
+
+ private static <T> T parse(HttpServletRequest httpRequest, TypeLiteral<T> typeLiteral)
+ throws IOException, BadRequestException {
+
+ try (BufferedReader br = httpRequest.getReader();
+ JsonReader json = new JsonReader(br)) {
+ try {
+ json.setLenient(true);
+
+ try {
+ json.peek();
+ } catch (EOFException e) {
+ throw new BadRequestException("Expected JSON object", e);
+ }
+
+ return gson.fromJson(json, typeLiteral.getType());
+ } finally {
+ try {
+ // Reader.close won't consume the rest of the input. Explicitly consume the request
+ // body.
+ br.skip(Long.MAX_VALUE);
+ } catch (Exception e) {
+ // ignore, e.g. trying to consume the rest of the input may fail if the request was
+ // cancelled
+ logger.atFine().withCause(e).log("Exception during the parsing of the request json");
+ }
+ }
+ }
+ }
+}
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 1991260..f7ed4cb 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
@@ -40,7 +40,22 @@
return callFetch(project, refName, targetUri, MILLISECONDS.toNanos(System.currentTimeMillis()));
}
- HttpResult initProject(Project.NameKey project, URIish uri) throws IOException;
+ /**
+ * Replicates the creation of a project, including the configuration stored in refs/meta/config.
+ *
+ * @param project The unique name of the project.
+ * @param uri The destination URI where the project and its configuration should be replicated to.
+ * @param eventCreatedOn The timestamp indicating when the init project event occurred.
+ * @param refsMetaConfigRevisionData A history of revisions for the refs/meta/config ref.
+ * @return An HTTP result object providing information about the replication process.
+ * @throws IOException If an I/O error occurs during the replication.
+ */
+ HttpResult initProject(
+ Project.NameKey project,
+ URIish uri,
+ long eventCreatedOn,
+ List<RevisionData> refsMetaConfigRevisionData)
+ throws IOException;
HttpResult deleteProject(Project.NameKey project, URIish apiUri) throws IOException;
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 b606ba8..7607e4b 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
@@ -23,6 +23,7 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.Project.NameKey;
+import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.server.config.GerritInstanceId;
@@ -130,14 +131,29 @@
}
/* (non-Javadoc)
- * @see com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient#initProject(com.google.gerrit.entities.Project.NameKey, org.eclipse.jgit.transport.URIish)
+ * @see com.googlesource.gerrit.plugins.replication.pull.client.FetchApiClient#initProject(com.google.gerrit.entities.Project.NameKey, org.eclipse.jgit.transport.URIish, long, java.util.List<com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData>)
*/
@Override
- public HttpResult initProject(Project.NameKey project, URIish uri) throws IOException {
+ public HttpResult initProject(
+ NameKey project,
+ URIish uri,
+ long eventCreatedOn,
+ List<RevisionData> refsMetaConfigRevisionData)
+ throws IOException {
String url = formatInitProjectUrl(uri.toString(), project);
+
+ RevisionData[] inputData = new RevisionData[refsMetaConfigRevisionData.size()];
+ RevisionsInput input =
+ new RevisionsInput(
+ instanceId,
+ RefNames.REFS_CONFIG,
+ eventCreatedOn,
+ refsMetaConfigRevisionData.toArray(inputData));
+
HttpPut put = new HttpPut(url);
+ put.setEntity(new StringEntity(GSON.toJson(input)));
put.addHeader(new BasicHeader("Accept", MediaType.ANY_TEXT_TYPE.toString()));
- put.addHeader(new BasicHeader("Content-Type", MediaType.PLAIN_TEXT_UTF_8.toString()));
+ put.addHeader(new BasicHeader("Content-Type", MediaType.JSON_UTF_8.toString()));
return executeRequest(put, bearerTokenProvider.get(), uri);
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java
index d3e45da..43ec9bf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/CGitFetch.java
@@ -44,6 +44,7 @@
private URIish uri;
private int timeout;
private final String taskIdHex;
+ private final boolean isMirror;
@Inject
public CGitFetch(
@@ -56,12 +57,17 @@
this.taskIdHex = taskIdHex;
this.uri = appendCredentials(uri, cpFactory.create(config.getRemoteConfig().getName()));
this.timeout = config.getRemoteConfig().getTimeout();
+ this.isMirror = config.getRemoteConfig().isMirror();
}
@Override
public List<RefUpdateState> fetch(List<RefSpec> refsSpec) throws IOException {
List<String> refs = refsSpec.stream().map(s -> s.toString()).collect(Collectors.toList());
- List<String> command = Lists.newArrayList("git", "fetch", uri.toPrivateASCIIString());
+ List<String> command = Lists.newArrayList("git", "fetch");
+ if (isMirror) {
+ command.add("--prune");
+ }
+ command.add(uri.toPrivateASCIIString());
command.addAll(refs);
ProcessBuilder pb = new ProcessBuilder().command(command).directory(localProjectDirectory);
repLog.info("[{}] Fetch references {} from {}", taskIdHex, refs, uri);
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index b7db2c0..61d62b4 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -526,8 +526,7 @@
remote.NAME.mirror
: If true, replication will remove local branches and tags that are
absent remotely or invisible to the replication (for example read access
-denied via `authGroup` option). Note that this option is currently
-implemented for the JGit client only.
+denied via `authGroup` option).
By default, false, do not remove remote branches or tags.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/CGitFetchIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/CGitFetchIT.java
index baacf20..3ca937c 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/CGitFetchIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/CGitFetchIT.java
@@ -15,6 +15,9 @@
package com.googlesource.gerrit.plugins.replication.pull;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.deleteRef;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
@@ -22,35 +25,33 @@
import static org.mockito.Mockito.when;
import com.google.common.collect.Lists;
+import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.SkipProjectClone;
import com.google.gerrit.acceptance.TestPlugin;
import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Permission;
import com.google.gerrit.extensions.api.projects.BranchInput;
-import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.inject.Scopes;
-import com.google.inject.assistedinject.FactoryModuleBuilder;
-import com.googlesource.gerrit.plugins.replication.AutoReloadSecureCredentialsFactoryDecorator;
-import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
-import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
-import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig;
+import com.google.inject.Inject;
import com.googlesource.gerrit.plugins.replication.pull.fetch.BatchFetchClient;
import com.googlesource.gerrit.plugins.replication.pull.fetch.CGitFetch;
import com.googlesource.gerrit.plugins.replication.pull.fetch.Fetch;
-import com.googlesource.gerrit.plugins.replication.pull.fetch.FetchClientImplementation;
import com.googlesource.gerrit.plugins.replication.pull.fetch.FetchFactory;
import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState;
-import java.net.URISyntaxException;
import java.util.List;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.URIish;
+import org.junit.Before;
import org.junit.Test;
@SkipProjectClone
@@ -62,6 +63,16 @@
private static final String TEST_REPLICATION_SUFFIX = "suffix1";
private static final String TEST_TASK_ID = "taskid";
+ @Inject private ProjectOperations projectOperations;
+
+ @Before
+ public void allowRefDeletion() {
+ projectOperations
+ .allProjectsForUpdate()
+ .add(allow(Permission.DELETE).ref("refs/*").group(adminGroupUuid()))
+ .update();
+ }
+
@Test
public void shouldFetchRef() throws Exception {
testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
@@ -224,29 +235,55 @@
}
}
+ @Test
+ public void shouldNotPruneRefsWhenMirrorIsUnset() throws Exception {
+ testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
+ String BRANCH_REF = Constants.R_HEADS + "anyBranch";
+ String TAG_REF = Constants.R_TAGS + "anyTag";
+
+ PushOneCommit.Result branchPush = pushFactory.create(user.newIdent(), testRepo).to(BRANCH_REF);
+ branchPush.assertOkStatus();
+
+ PushResult tagPush = pushHead(testRepo, TAG_REF, false, false);
+ assertOkStatus(tagPush, TAG_REF);
+
+ try (Repository localRepo = repoManager.openRepository(project)) {
+ fetchAllRefs(TEST_TASK_ID, testRepoPath, localRepo);
+ waitUntil(
+ () ->
+ checkedGetRef(localRepo, BRANCH_REF) != null
+ && checkedGetRef(localRepo, TAG_REF) != null);
+ assertThat(getRef(localRepo, BRANCH_REF)).isNotNull();
+ assertThat(getRef(localRepo, TAG_REF)).isNotNull();
+
+ PushResult deleteBranchResult = deleteRef(testRepo, BRANCH_REF);
+ assertOkStatus(deleteBranchResult, BRANCH_REF);
+
+ PushResult deleteTagResult = deleteRef(testRepo, TAG_REF);
+ assertOkStatus(deleteTagResult, TAG_REF);
+
+ fetchAllRefs(TEST_TASK_ID, testRepoPath, localRepo);
+ waitUntil(
+ () ->
+ checkedGetRef(localRepo, BRANCH_REF) != null
+ && checkedGetRef(localRepo, TAG_REF) != null);
+ assertThat(getRef(localRepo, BRANCH_REF)).isNotNull();
+ assertThat(getRef(localRepo, TAG_REF)).isNotNull();
+ }
+ }
+
@SuppressWarnings("unused")
- private static class TestModule extends FactoryModule {
+ private static class TestModule extends FetchModule<CGitFetch> {
@Override
- protected void configure() {
+ Class<CGitFetch> clientClass() {
+ return CGitFetch.class;
+ }
+
+ @Override
+ Config cf() {
Config cf = new Config();
cf.setInt("remote", "test_config", "timeout", 0);
- try {
- RemoteConfig remoteConfig = new RemoteConfig(cf, "test_config");
- SourceConfiguration sourceConfig = new SourceConfiguration(remoteConfig, cf);
- bind(ReplicationConfig.class).to(ReplicationFileBasedConfig.class);
- bind(CredentialsFactory.class)
- .to(AutoReloadSecureCredentialsFactoryDecorator.class)
- .in(Scopes.SINGLETON);
-
- bind(SourceConfiguration.class).toInstance(sourceConfig);
- install(
- new FactoryModuleBuilder()
- .implement(Fetch.class, CGitFetch.class)
- .implement(Fetch.class, FetchClientImplementation.class, CGitFetch.class)
- .build(FetchFactory.class));
- } catch (URISyntaxException e) {
- throw new RuntimeException(e);
- }
+ return cf;
}
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/CGitFetchWithMirrorIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/CGitFetchWithMirrorIT.java
new file mode 100644
index 0000000..8a3bae9
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/CGitFetchWithMirrorIT.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2023 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 static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.deleteRef;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+
+import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.acceptance.SkipProjectClone;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Permission;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.replication.pull.fetch.CGitFetch;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushResult;
+import org.junit.Before;
+import org.junit.Test;
+
+@SkipProjectClone
+@UseLocalDisk
+@TestPlugin(
+ name = "pull-replication",
+ sysModule = "com.googlesource.gerrit.plugins.replication.pull.CGitFetchWithMirrorIT$TestModule")
+public class CGitFetchWithMirrorIT extends FetchITBase {
+ private static final String TEST_REPLICATION_SUFFIX = "suffix1";
+ private static final String TEST_TASK_ID = "taskid";
+
+ @Inject private ProjectOperations projectOperations;
+
+ @Before
+ public void allowRefDeletion() {
+ projectOperations
+ .allProjectsForUpdate()
+ .add(allow(Permission.DELETE).ref("refs/*").group(adminGroupUuid()))
+ .update();
+ }
+
+ @Test
+ public void shouldPruneRefsWhenMirrorIsTrue() throws Exception {
+ testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
+ String BRANCH_REF = Constants.R_HEADS + "anyBranch";
+ String TAG_REF = Constants.R_TAGS + "anyTag";
+
+ Result branchPush = pushFactory.create(user.newIdent(), testRepo).to(BRANCH_REF);
+ branchPush.assertOkStatus();
+
+ PushResult tagPush = pushHead(testRepo, TAG_REF, false, false);
+ assertOkStatus(tagPush, TAG_REF);
+
+ try (Repository localRepo = repoManager.openRepository(project)) {
+ fetchAllRefs(TEST_TASK_ID, testRepoPath, localRepo);
+ waitUntil(
+ () ->
+ checkedGetRef(localRepo, BRANCH_REF) != null
+ && checkedGetRef(localRepo, TAG_REF) != null);
+ assertThat(getRef(localRepo, BRANCH_REF)).isNotNull();
+ assertThat(getRef(localRepo, TAG_REF)).isNotNull();
+
+ PushResult deleteBranchResult = deleteRef(testRepo, BRANCH_REF);
+ assertOkStatus(deleteBranchResult, BRANCH_REF);
+
+ PushResult deleteTagResult = deleteRef(testRepo, TAG_REF);
+ assertOkStatus(deleteTagResult, TAG_REF);
+
+ fetchAllRefs(TEST_TASK_ID, testRepoPath, localRepo);
+ waitUntil(
+ () ->
+ checkedGetRef(localRepo, BRANCH_REF) == null
+ && checkedGetRef(localRepo, TAG_REF) == null);
+ assertThat(getRef(localRepo, BRANCH_REF)).isNull();
+ assertThat(getRef(localRepo, TAG_REF)).isNull();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static class TestModule extends FetchModule<CGitFetch> {
+ @Override
+ Class<CGitFetch> clientClass() {
+ return CGitFetch.class;
+ }
+
+ @Override
+ Config cf() {
+ Config cf = new Config();
+ cf.setInt("remote", "test_config", "timeout", 0);
+ cf.setBoolean("remote", "test_config", "mirror", true);
+ return cf;
+ }
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchITBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchITBase.java
index be9f902..3429983 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchITBase.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchITBase.java
@@ -14,18 +14,39 @@
package com.googlesource.gerrit.plugins.replication.pull;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.server.config.SitePaths;
import com.google.inject.Inject;
+import com.google.inject.Scopes;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import com.googlesource.gerrit.plugins.replication.AutoReloadSecureCredentialsFactoryDecorator;
+import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
+import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
+import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig;
+import com.googlesource.gerrit.plugins.replication.pull.fetch.Fetch;
+import com.googlesource.gerrit.plugins.replication.pull.fetch.FetchClientImplementation;
import com.googlesource.gerrit.plugins.replication.pull.fetch.FetchFactory;
+import java.io.IOException;
+import java.net.URISyntaxException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.function.Supplier;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.URIish;
public abstract class FetchITBase extends LightweightPluginDaemonTest {
private static final String TEST_REPLICATION_SUFFIX = "suffix1";
@@ -33,6 +54,7 @@
private static final int TEST_REPLICATION_DELAY = 60;
private static final Duration TEST_TIMEOUT = Duration.ofSeconds(TEST_REPLICATION_DELAY * 2);
+ private static final RefSpec ALL_REFS = new RefSpec("+refs/*:refs/*");
@Inject private SitePaths sitePaths;
@Inject private ProjectOperations projectOperations;
@@ -66,7 +88,49 @@
}
}
+ protected void fetchAllRefs(String taskId, Path remotePath, Repository localRepo)
+ throws URISyntaxException, IOException {
+ fetchFactory
+ .create(taskId, new URIish(remotePath.toString()), localRepo)
+ .fetch(Lists.newArrayList(ALL_REFS));
+ }
+
+ protected static void assertOkStatus(PushResult result, String ref) {
+ RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
+ assertThat(refUpdate).isNotNull();
+ assertWithMessage(refUpdate.getMessage())
+ .that(refUpdate.getStatus())
+ .isEqualTo(RemoteRefUpdate.Status.OK);
+ }
+
Project.NameKey createTestProject(String name) {
return projectOperations.newProject().name(name).create();
}
+
+ protected abstract static class FetchModule<T extends Fetch> extends FactoryModule {
+ abstract Config cf();
+
+ abstract Class<T> clientClass();
+
+ @Override
+ protected void configure() {
+ try {
+ RemoteConfig remoteConfig = new RemoteConfig(cf(), "test_config");
+ SourceConfiguration sourceConfig = new SourceConfiguration(remoteConfig, cf());
+ bind(ReplicationConfig.class).to(ReplicationFileBasedConfig.class);
+ bind(CredentialsFactory.class)
+ .to(AutoReloadSecureCredentialsFactoryDecorator.class)
+ .in(Scopes.SINGLETON);
+
+ bind(SourceConfiguration.class).toInstance(sourceConfig);
+ install(
+ new FactoryModuleBuilder()
+ .implement(Fetch.class, clientClass())
+ .implement(Fetch.class, FetchClientImplementation.class, clientClass())
+ .build(FetchFactory.class));
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/JGitFetchIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/JGitFetchIT.java
index 77cf80d..b900d8a 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/JGitFetchIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/JGitFetchIT.java
@@ -15,7 +15,6 @@
package com.googlesource.gerrit.plugins.replication.pull;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.gerrit.acceptance.GitUtil.deleteRef;
import static com.google.gerrit.acceptance.GitUtil.pushHead;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
@@ -27,31 +26,15 @@
import com.google.gerrit.acceptance.UseLocalDisk;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.Permission;
-import com.google.gerrit.extensions.config.FactoryModule;
import com.google.inject.Inject;
-import com.google.inject.Scopes;
-import com.google.inject.assistedinject.FactoryModuleBuilder;
-import com.googlesource.gerrit.plugins.replication.AutoReloadSecureCredentialsFactoryDecorator;
-import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
-import com.googlesource.gerrit.plugins.replication.ReplicationConfig;
-import com.googlesource.gerrit.plugins.replication.ReplicationFileBasedConfig;
import com.googlesource.gerrit.plugins.replication.pull.fetch.Fetch;
-import com.googlesource.gerrit.plugins.replication.pull.fetch.FetchClientImplementation;
-import com.googlesource.gerrit.plugins.replication.pull.fetch.FetchFactory;
import com.googlesource.gerrit.plugins.replication.pull.fetch.JGitFetch;
import com.googlesource.gerrit.plugins.replication.pull.fetch.PermanentTransportException;
-import com.googlesource.gerrit.plugins.replication.pull.fetch.RefUpdateState;
-import java.io.IOException;
-import java.net.URISyntaxException;
-import java.util.List;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.URIish;
import org.junit.Before;
import org.junit.Test;
@@ -64,7 +47,6 @@
public class JGitFetchIT extends FetchITBase {
private static final String TEST_REPLICATION_SUFFIX = "suffix1";
private static final String TEST_TASK_ID = "taskid";
- private static final RefSpec ALL_REFS = new RefSpec("+refs/*:refs/*");
@Inject private ProjectOperations projectOperations;
@@ -89,12 +71,10 @@
}
@Test
- public void shouldPruneRefsWhenMirrorIsTrue() throws Exception {
+ public void shouldPruneRefsWhenMirrorIsUnset() throws Exception {
testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
- String branchName = "anyBranch";
- String branchRef = Constants.R_HEADS + branchName;
- String tagName = "anyTag";
- String tagRef = Constants.R_TAGS + tagName;
+ String branchRef = Constants.R_HEADS + "anyBranch";
+ String tagRef = Constants.R_TAGS + "anyTag";
PushOneCommit.Result branchPush = pushFactory.create(user.newIdent(), testRepo).to(branchRef);
branchPush.assertOkStatus();
@@ -103,13 +83,8 @@
assertOkStatus(tagPush, tagRef);
try (Repository localRepo = repoManager.openRepository(project)) {
- List<RefUpdateState> fetchCreated = fetchAllRefs(localRepo);
- assertThat(fetchCreated.toString())
- .contains(new RefUpdateState(branchRef, RefUpdate.Result.NEW).toString());
+ fetchAllRefs(TEST_TASK_ID, testRepoPath, localRepo);
assertThat(getRef(localRepo, branchRef)).isNotNull();
-
- assertThat(fetchCreated.toString())
- .contains(new RefUpdateState(tagRef, RefUpdate.Result.NEW).toString());
assertThat(getRef(localRepo, tagRef)).isNotNull();
PushResult deleteBranchResult = deleteRef(testRepo, branchRef);
@@ -118,55 +93,24 @@
PushResult deleteTagResult = deleteRef(testRepo, tagRef);
assertOkStatus(deleteTagResult, tagRef);
- List<RefUpdateState> fetchDeleted = fetchAllRefs(localRepo);
- assertThat(fetchDeleted.toString())
- .contains(new RefUpdateState(branchRef, RefUpdate.Result.FORCED).toString());
- assertThat(getRef(localRepo, branchRef)).isNull();
-
- assertThat(fetchDeleted.toString())
- .contains(new RefUpdateState(tagRef, RefUpdate.Result.FORCED).toString());
- assertThat(getRef(localRepo, tagRef)).isNull();
+ fetchAllRefs(TEST_TASK_ID, testRepoPath, localRepo);
+ assertThat(getRef(localRepo, branchRef)).isNotNull();
+ assertThat(getRef(localRepo, tagRef)).isNotNull();
}
}
- private List<RefUpdateState> fetchAllRefs(Repository localRepo)
- throws URISyntaxException, IOException {
- Fetch fetch = fetchFactory.create(TEST_TASK_ID, new URIish(testRepoPath.toString()), localRepo);
- return fetch.fetch(Lists.newArrayList(ALL_REFS));
- }
-
- private static void assertOkStatus(PushResult result, String ref) {
- RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
- assertThat(refUpdate).isNotNull();
- assertWithMessage(refUpdate.getMessage())
- .that(refUpdate.getStatus())
- .isEqualTo(RemoteRefUpdate.Status.OK);
- }
-
@SuppressWarnings("unused")
- private static class TestModule extends FactoryModule {
+ private static class TestModule extends FetchModule<JGitFetch> {
@Override
- protected void configure() {
+ Class<JGitFetch> clientClass() {
+ return JGitFetch.class;
+ }
+
+ @Override
+ Config cf() {
Config cf = new Config();
cf.setInt("remote", "test_config", "timeout", 0);
- cf.setBoolean("remote", "test_config", "mirror", true);
- try {
- RemoteConfig remoteConfig = new RemoteConfig(cf, "test_config");
- SourceConfiguration sourceConfig = new SourceConfiguration(remoteConfig, cf);
- bind(ReplicationConfig.class).to(ReplicationFileBasedConfig.class);
- bind(CredentialsFactory.class)
- .to(AutoReloadSecureCredentialsFactoryDecorator.class)
- .in(Scopes.SINGLETON);
-
- bind(SourceConfiguration.class).toInstance(sourceConfig);
- install(
- new FactoryModuleBuilder()
- .implement(Fetch.class, JGitFetch.class)
- .implement(Fetch.class, FetchClientImplementation.class, JGitFetch.class)
- .build(FetchFactory.class));
- } catch (URISyntaxException e) {
- throw new RuntimeException(e);
- }
+ return cf;
}
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/JGitFetchWithMirrorIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/JGitFetchWithMirrorIT.java
new file mode 100644
index 0000000..2df700f
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/JGitFetchWithMirrorIT.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2023 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 static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.GitUtil.deleteRef;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.SkipProjectClone;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.entities.Permission;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.replication.pull.fetch.JGitFetch;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushResult;
+import org.junit.Before;
+import org.junit.Test;
+
+@SkipProjectClone
+@UseLocalDisk
+@TestPlugin(
+ name = "pull-replication",
+ sysModule = "com.googlesource.gerrit.plugins.replication.pull.JGitFetchWithMirrorIT$TestModule")
+public class JGitFetchWithMirrorIT extends FetchITBase {
+ private static final String TEST_REPLICATION_SUFFIX = "suffix1";
+ private static final String TEST_TASK_ID = "taskid";
+
+ @Inject private ProjectOperations projectOperations;
+
+ @Before
+ public void allowRefDeletion() {
+ projectOperations
+ .allProjectsForUpdate()
+ .add(allow(Permission.DELETE).ref("refs/*").group(adminGroupUuid()))
+ .update();
+ }
+
+ @Test
+ public void shouldPruneRefsWhenMirrorIsTrue() throws Exception {
+ testRepo = cloneProject(createTestProject(project + TEST_REPLICATION_SUFFIX));
+ String branchRef = Constants.R_HEADS + "anyBranch";
+ String tagRef = Constants.R_TAGS + "anyTag";
+
+ PushOneCommit.Result branchPush = pushFactory.create(user.newIdent(), testRepo).to(branchRef);
+ branchPush.assertOkStatus();
+
+ PushResult tagPush = pushHead(testRepo, tagRef, false, false);
+ assertOkStatus(tagPush, tagRef);
+
+ try (Repository localRepo = repoManager.openRepository(project)) {
+ fetchAllRefs(TEST_TASK_ID, testRepoPath, localRepo);
+ assertThat(getRef(localRepo, branchRef)).isNotNull();
+ assertThat(getRef(localRepo, tagRef)).isNotNull();
+
+ PushResult deleteBranchResult = deleteRef(testRepo, branchRef);
+ assertOkStatus(deleteBranchResult, branchRef);
+
+ PushResult deleteTagResult = deleteRef(testRepo, tagRef);
+ assertOkStatus(deleteTagResult, tagRef);
+
+ fetchAllRefs(TEST_TASK_ID, testRepoPath, localRepo);
+ assertThat(getRef(localRepo, branchRef)).isNull();
+ assertThat(getRef(localRepo, tagRef)).isNull();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static class TestModule extends FetchModule<JGitFetch> {
+ @Override
+ Class<JGitFetch> clientClass() {
+ return JGitFetch.class;
+ }
+
+ @Override
+ Config cf() {
+ Config cf = new Config();
+ cf.setInt("remote", "test_config", "timeout", 0);
+ cf.setBoolean("remote", "test_config", "mirror", true);
+ return cf;
+ }
+ }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
index 29bf7e4..5717a8b 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
@@ -309,7 +309,11 @@
getInstance(SourcesCollection.class).getByRemoteName(TEST_REPLICATION_REMOTE).get();
FetchApiClient client = getInstance(FetchApiClient.Factory.class).create(source);
- client.initProject(projectToCreate, new URIish(source.getApis().get(0)));
+ client.initProject(
+ projectToCreate,
+ new URIish(source.getApis().get(0)),
+ System.currentTimeMillis(),
+ Collections.emptyList());
waitUntil(() -> repoManager.list().contains(projectToCreate));
}
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 603528d..a07aa55 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
@@ -33,6 +33,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.events.ProjectDeletedListener;
@@ -147,7 +148,8 @@
.when(fetchRestApiClient.callSendObjects(any(), anyString(), anyLong(), any(), any()))
.thenReturn(httpResult);
when(fetchRestApiClient.callFetch(any(), anyString(), any())).thenReturn(fetchHttpResult);
- when(fetchRestApiClient.initProject(any(), any())).thenReturn(successfulHttpResult);
+ when(fetchRestApiClient.initProject(any(), any(), anyLong(), any()))
+ .thenReturn(successfulHttpResult);
when(successfulHttpResult.isSuccessful()).thenReturn(true);
when(httpResult.isSuccessful()).thenReturn(true);
when(fetchHttpResult.isSuccessful()).thenReturn(true);
@@ -206,7 +208,7 @@
objectUnderTest.start();
objectUnderTest.onEvent(event);
- verify(fetchRestApiClient).initProject(any(), any());
+ verify(fetchRestApiClient).initProject(any(), any(), anyLong(), any());
}
@Test
@@ -219,7 +221,22 @@
objectUnderTest.start();
objectUnderTest.onEvent(event);
- verify(fetchRestApiClient, never()).initProject(any(), any());
+ verify(fetchRestApiClient, never()).initProject(any(), any(), anyLong(), any());
+ }
+
+ @Test
+ public void shouldNotCallInitProjectWhenProjectWithoutConfiguration() throws Exception {
+ Event event = new TestEvent("refs/changes/01/1/meta");
+ when(httpResult.isSuccessful()).thenReturn(false);
+ when(httpResult.isProjectMissing(any())).thenReturn(true);
+ when(source.isCreateMissingRepositories()).thenReturn(true);
+ when(revReader.read(any(), any(), eq(RefNames.REFS_CONFIG), anyInt()))
+ .thenReturn(Optional.empty());
+
+ objectUnderTest.start();
+ objectUnderTest.onEvent(event);
+
+ verify(fetchRestApiClient, never()).initProject(any(), any(), anyLong(), any());
}
@Test
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 25aa2a7..65ccdfb 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
@@ -24,6 +24,7 @@
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import com.google.common.io.CharStreams;
+import com.google.common.net.MediaType;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
@@ -37,6 +38,7 @@
import java.nio.ByteBuffer;
import java.util.Collections;
import org.apache.http.Header;
+import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
@@ -94,6 +96,17 @@
+ "\",\"type\":2,\"content\":\"MTAwNjQ0IGJsb2IgYmIzODNmNTI0OWM2OGE0Y2M4YzgyYmRkMTIyOGI0YTg4ODNmZjZlOCAgICBmNzVhNjkwMDRhOTNiNGNjYzhjZTIxNWMxMjgwODYzNmMyYjc1Njc1\"},\"blobs\":[{\"sha1\":\""
+ blobObjectId
+ "\",\"type\":3,\"content\":\"ewogICJjb21tZW50cyI6IFsKICAgIHsKICAgICAgImtleSI6IHsKICAgICAgICAidXVpZCI6ICI5MGI1YWJmZl80ZjY3NTI2YSIsCiAgICAgICAgImZpbGVuYW1lIjogIi9DT01NSVRfTVNHIiwKICAgICAgICAicGF0Y2hTZXRJZCI6IDEKICAgICAgfSwKICAgICAgImxpbmVOYnIiOiA5LAogICAgICAiYXV0aG9yIjogewogICAgICAgICJpZCI6IDEwMDAwMDAKICAgICAgfSwKICAgICAgIndyaXR0ZW5PbiI6ICIyMDIxLTAxLTEzVDIyOjU3OjI4WiIsCiAgICAgICJzaWRlIjogMSwKICAgICAgIm1lc3NhZ2UiOiAidGVzdCBjb21tZW50IiwKICAgICAgInJhbmdlIjogewogICAgICAgICJzdGFydExpbmUiOiA5LAogICAgICAgICJzdGFydENoYXIiOiAyMSwKICAgICAgICAiZW5kTGluZSI6IDksCiAgICAgICAgImVuZENoYXIiOiAzNAogICAgICB9LAogICAgICAicmV2SWQiOiAiZjc1YTY5MDA0YTkzYjRjY2M4Y2UyMTVjMTI4MDg2MzZjMmI3NTY3NSIsCiAgICAgICJzZXJ2ZXJJZCI6ICI2OWVjMzhmMC0zNTBlLTRkOWMtOTZkNC1iYzk1NmYyZmFhYWMiLAogICAgICAidW5yZXNvbHZlZCI6IHRydWUKICAgIH0KICBdCn0\\u003d\"}]}}";
+
+ String expectedInitProjectWithConfigPayload =
+ "{\"label\":\"Replication\",\"ref_name\":\"refs/meta/config\",\"event_created_on\":"
+ + eventCreatedOn
+ + ",\"revisions_data\":[{\"commit_object\":{\"sha1\":\""
+ + commitObjectId
+ + "\",\"type\":1,\"content\":\"dHJlZSA3NzgxNGQyMTZhNmNhYjJkZGI5ZjI4NzdmYmJkMGZlYmRjMGZhNjA4CnBhcmVudCA5ODNmZjFhM2NmNzQ3MjVhNTNhNWRlYzhkMGMwNjEyMjEyOGY1YThkCmF1dGhvciBHZXJyaXQgVXNlciAxMDAwMDAwIDwxMDAwMDAwQDY5ZWMzOGYwLTM1MGUtNGQ5Yy05NmQ0LWJjOTU2ZjJmYWFhYz4gMTYxMDU3ODY0OCArMDEwMApjb21taXR0ZXIgR2Vycml0IENvZGUgUmV2aWV3IDxyb290QG1hY3plY2gtWFBTLTE1PiAxNjEwNTc4NjQ4ICswMTAwCgpVcGRhdGUgcGF0Y2ggc2V0IDEKClBhdGNoIFNldCAxOgoKKDEgY29tbWVudCkKClBhdGNoLXNldDogMQo\\u003d\"},\"tree_object\":{\"sha1\":\""
+ + treeObjectId
+ + "\",\"type\":2,\"content\":\"MTAwNjQ0IGJsb2IgYmIzODNmNTI0OWM2OGE0Y2M4YzgyYmRkMTIyOGI0YTg4ODNmZjZlOCAgICBmNzVhNjkwMDRhOTNiNGNjYzhjZTIxNWMxMjgwODYzNmMyYjc1Njc1\"},\"blobs\":[{\"sha1\":\""
+ + blobObjectId
+ + "\",\"type\":3,\"content\":\"ewogICJjb21tZW50cyI6IFsKICAgIHsKICAgICAgImtleSI6IHsKICAgICAgICAidXVpZCI6ICI5MGI1YWJmZl80ZjY3NTI2YSIsCiAgICAgICAgImZpbGVuYW1lIjogIi9DT01NSVRfTVNHIiwKICAgICAgICAicGF0Y2hTZXRJZCI6IDEKICAgICAgfSwKICAgICAgImxpbmVOYnIiOiA5LAogICAgICAiYXV0aG9yIjogewogICAgICAgICJpZCI6IDEwMDAwMDAKICAgICAgfSwKICAgICAgIndyaXR0ZW5PbiI6ICIyMDIxLTAxLTEzVDIyOjU3OjI4WiIsCiAgICAgICJzaWRlIjogMSwKICAgICAgIm1lc3NhZ2UiOiAidGVzdCBjb21tZW50IiwKICAgICAgInJhbmdlIjogewogICAgICAgICJzdGFydExpbmUiOiA5LAogICAgICAgICJzdGFydENoYXIiOiAyMSwKICAgICAgICAiZW5kTGluZSI6IDksCiAgICAgICAgImVuZENoYXIiOiAzNAogICAgICB9LAogICAgICAicmV2SWQiOiAiZjc1YTY5MDA0YTkzYjRjY2M4Y2UyMTVjMTI4MDg2MzZjMmI3NTY3NSIsCiAgICAgICJzZXJ2ZXJJZCI6ICI2OWVjMzhmMC0zNTBlLTRkOWMtOTZkNC1iYzk1NmYyZmFhYWMiLAogICAgICAidW5yZXNvbHZlZCI6IHRydWUKICAgIH0KICBdCn0\\u003d\"}]}]}";
String commitObject =
"tree "
+ treeObjectId
@@ -374,18 +387,26 @@
@Test
public void shouldCallInitProjectEndpoint() throws Exception {
-
- objectUnderTest.initProject(Project.nameKey("test_repo"), new URIish(api));
+ objectUnderTest.initProject(
+ Project.nameKey("test_repo"),
+ new URIish(api),
+ eventCreatedOn,
+ Collections.singletonList(createSampleRevisionData()));
verify(httpClient, times(1)).execute(httpPutCaptor.capture(), any());
-
HttpPut httpPut = httpPutCaptor.getValue();
+ String payload =
+ CharStreams.toString(
+ new InputStreamReader(httpPut.getEntity().getContent(), Charsets.UTF_8));
assertThat(httpPut.getURI().getHost()).isEqualTo("gerrit-host");
+ assertThat(httpPut.getHeaders(HttpHeaders.CONTENT_TYPE)[0].getValue())
+ .isEqualTo(MediaType.JSON_UTF_8.toString());
assertThat(httpPut.getURI().getPath())
.isEqualTo(
String.format(
"%s/plugins/pull-replication/init-project/test_repo.git",
urlAuthenticationPrefix()));
+ assertThat(payload).isEqualTo(expectedInitProjectWithConfigPayload);
assertAuthentication(httpPut);
}