Extract UpdateHeadCommand

Move logic related to HEAD update from the action class to a command in
order to reuse it when HEAD modification event comes from stream events

Change-Id: Id9c9d297bb5e39d944386b9e514b5ac69ec0ea1a
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java
index 800f2b2..9a3b4fa 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java
@@ -15,56 +15,31 @@
 package com.googlesource.gerrit.plugins.replication.pull.api;
 
 import com.google.common.base.Strings;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.HeadInput;
-import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.server.events.EventDispatcher;
-import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import com.googlesource.gerrit.plugins.replication.LocalFS;
-import com.googlesource.gerrit.plugins.replication.pull.Context;
-import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent;
-import com.googlesource.gerrit.plugins.replication.pull.GerritConfigOps;
-import com.googlesource.gerrit.plugins.replication.pull.ReplicationState;
-import java.net.HttpURLConnection;
-import java.util.Optional;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.transport.URIish;
 
 @Singleton
 public class UpdateHeadAction implements RestModifyView<ProjectResource, HeadInput> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-  private final GerritConfigOps gerritConfigOps;
   private final FetchPreconditions preconditions;
-  private final DynamicItem<EventDispatcher> eventDispatcher;
-  private static final URIish EMPTY_URI = new URIish();
+  private final UpdateHeadCommand updateHeadCommand;
 
   @Inject
-  UpdateHeadAction(
-      GerritConfigOps gerritConfigOps,
-      FetchPreconditions preconditions,
-      DynamicItem<EventDispatcher> eventDispatcher) {
-    this.gerritConfigOps = gerritConfigOps;
+  UpdateHeadAction(FetchPreconditions preconditions, UpdateHeadCommand updateHeadCommand) {
     this.preconditions = preconditions;
-    this.eventDispatcher = eventDispatcher;
+    this.updateHeadCommand = updateHeadCommand;
   }
 
   @Override
   public Response<?> apply(ProjectResource projectResource, HeadInput input)
       throws AuthException, BadRequestException, ResourceConflictException, Exception {
-    Response<String> res = null;
-
     if (input == null || Strings.isNullOrEmpty(input.ref)) {
       throw new BadRequestException("ref required");
     }
@@ -74,50 +49,8 @@
       throw new AuthException("Update head not permitted");
     }
 
-    // TODO: the .git suffix should not be added here, but rather it should be
-    //  dealt with by the caller, honouring the naming style from the
-    //  replication.config (Issue 15221)
-    Optional<URIish> maybeRepo =
-        gerritConfigOps.getGitRepositoryURI(String.format("%s.git", projectResource.getName()));
+    updateHeadCommand.doUpdate(projectResource.getNameKey(), ref);
 
-    try {
-      if (maybeRepo.isPresent()) {
-        if (new LocalFS(maybeRepo.get()).updateHead(projectResource.getNameKey(), ref)) {
-          return res = Response.ok(ref);
-        }
-        throw new UnprocessableEntityException(
-            String.format(
-                "Could not update HEAD of repo %s to ref %s", projectResource.getName(), ref));
-      }
-    } finally {
-      fireEvent(
-          projectResource.getNameKey(),
-          res != null && res.statusCode() == HttpURLConnection.HTTP_OK);
-    }
-    throw new ResourceNotFoundException(
-        String.format("Could not compute URL for repo: %s", projectResource.getName()));
-  }
-
-  private void fireEvent(Project.NameKey projectName, boolean succeeded) {
-    try {
-      Context.setLocalEvent(true);
-      eventDispatcher
-          .get()
-          .postEvent(
-              new FetchRefReplicatedEvent(
-                  projectName.get(),
-                  RefNames.HEAD,
-                  EMPTY_URI, // TODO: the remote label is not passed as parameter, hence cannot be
-                  // propagated to the event
-                  succeeded
-                      ? ReplicationState.RefFetchResult.SUCCEEDED
-                      : ReplicationState.RefFetchResult.FAILED,
-                  RefUpdate.Result.FORCED));
-    } catch (PermissionBackendException e) {
-      logger.atSevere().withCause(e).log(
-          "Cannot post event for refs/meta/config on project %s", projectName);
-    } finally {
-      Context.unsetLocalEvent();
-    }
+    return Response.ok(ref);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadCommand.java
new file mode 100644
index 0000000..53bf5d9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadCommand.java
@@ -0,0 +1,99 @@
+// 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;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.events.EventDispatcher;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.replication.LocalFS;
+import com.googlesource.gerrit.plugins.replication.pull.Context;
+import com.googlesource.gerrit.plugins.replication.pull.FetchRefReplicatedEvent;
+import com.googlesource.gerrit.plugins.replication.pull.GerritConfigOps;
+import com.googlesource.gerrit.plugins.replication.pull.ReplicationState;
+import java.util.Optional;
+import javax.inject.Inject;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.transport.URIish;
+
+@Singleton
+public class UpdateHeadCommand {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final URIish EMPTY_URI = new URIish();
+
+  private final GerritConfigOps gerritConfigOps;
+  private final DynamicItem<EventDispatcher> eventDispatcher;
+
+  @Inject
+  UpdateHeadCommand(GerritConfigOps gerritConfigOps, DynamicItem<EventDispatcher> eventDispatcher) {
+    this.gerritConfigOps = gerritConfigOps;
+    this.eventDispatcher = eventDispatcher;
+  }
+
+  public void doUpdate(Project.NameKey project, String ref)
+      throws UnprocessableEntityException, ResourceNotFoundException {
+    boolean succeeded = false;
+    // TODO: the .git suffix should not be added here, but rather it should be
+    //  dealt with by the caller, honouring the naming style from the
+    //  replication.config (Issue 15221)
+    Optional<URIish> maybeRepo =
+        gerritConfigOps.getGitRepositoryURI(String.format("%s.git", project.get()));
+
+    logger.atInfo().log("do update: %s %s", project, ref);
+    try {
+      if (maybeRepo.isPresent()) {
+        if (new LocalFS(maybeRepo.get()).updateHead(project, ref)) {
+          succeeded = true;
+          return;
+        }
+
+        throw new UnprocessableEntityException(
+            String.format("Could not update HEAD of repo %s to ref %s", project, ref));
+      }
+    } finally {
+      fireEvent(project, succeeded);
+    }
+    throw new ResourceNotFoundException(
+        String.format("Could not compute URL for repo: %s", project.get()));
+  }
+
+  private void fireEvent(Project.NameKey projectName, boolean succeeded) {
+    try {
+      Context.setLocalEvent(true);
+      eventDispatcher
+          .get()
+          .postEvent(
+              new FetchRefReplicatedEvent(
+                  projectName.get(),
+                  RefNames.HEAD,
+                  EMPTY_URI, // TODO: the remote label is not passed as parameter, hence cannot be
+                  // propagated to the event
+                  succeeded
+                      ? ReplicationState.RefFetchResult.SUCCEEDED
+                      : ReplicationState.RefFetchResult.FAILED,
+                  RefUpdate.Result.FORCED));
+    } catch (PermissionBackendException e) {
+      logger.atSevere().withCause(e).log(
+          "Cannot post event for refs/meta/config on project %s", projectName);
+    } finally {
+      Context.unsetLocalEvent();
+    }
+  }
+}