Introduce FetchRefSpec over the whole replication queue processing

Do not rely on simple String ref renames for tracking
replication tasks and keep the full RefSpec all throughout
the processing, expanding it further using the configured
RefSpec. This allows to represent further fetch operations like
refs deletion, which is leveraged as a follow-up of this
change.

Introduce FetchRefSpec as a subclass of RefSpec with additional
utility methods for translating from/to a refname and a plain
RefSpec.

Change-Id: I9141eade50058c128c0f14e33fec9d755fea16c7
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchAll.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchAll.java
index 5a7362a..ff0459f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchAll.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchAll.java
@@ -85,9 +85,11 @@
       if (cfg.wouldFetchProject(project)) {
         for (URIish uri : cfg.getURIs(project, urlMatch)) {
           if (now) {
-            cfg.scheduleNow(project, FetchOne.ALL_REFS, uri, state, Optional.empty());
+            cfg.scheduleNow(
+                project, FetchRefSpec.fromRef(FetchOne.ALL_REFS), uri, state, Optional.empty());
           } else {
-            cfg.schedule(project, FetchOne.ALL_REFS, uri, state, Optional.empty());
+            cfg.schedule(
+                project, FetchRefSpec.fromRef(FetchOne.ALL_REFS), uri, state, Optional.empty());
           }
         }
       }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java
index 042ae0f..84be78a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchOne.java
@@ -86,7 +86,7 @@
   private final FetchRefsDatabase fetchRefsDatabase;
   private final Project.NameKey projectName;
   private final URIish uri;
-  private final Set<String> delta = Sets.newHashSetWithExpectedSize(4);
+  private final Set<FetchRefSpec> delta = Sets.newHashSetWithExpectedSize(4);
   private final Set<TransportException> fetchFailures = Sets.newHashSetWithExpectedSize(4);
   private boolean fetchAllRefs;
   private Repository git;
@@ -94,7 +94,7 @@
   private int retryCount;
   private final int maxRetries;
   private boolean canceled;
-  private final ListMultimap<String, ReplicationState> stateMap = LinkedListMultimap.create();
+  private final ListMultimap<FetchRefSpec, ReplicationState> stateMap = LinkedListMultimap.create();
   private final int maxLockRetries;
   private int lockRetryCount;
   private final int id;
@@ -182,7 +182,7 @@
 
   @Override
   public String toString() {
-    String print = "[" + taskIdHex + "] fetch " + uri + " [" + String.join(",", delta) + "]";
+    String print = "[" + taskIdHex + "] fetch " + uri + " " + delta;
 
     if (retryCount > 0) {
       print = "(retry " + retryCount + ") " + print;
@@ -212,8 +212,8 @@
     return uri;
   }
 
-  void addRef(String ref) {
-    if (ALL_REFS.equals(ref)) {
+  void addRef(FetchRefSpec ref) {
+    if (ref.equalsToRef(ALL_REFS)) {
       delta.clear();
       fetchAllRefs = true;
       repLog.trace("[{}] Added all refs for replication from {}", taskIdHex, uri);
@@ -223,23 +223,29 @@
     }
   }
 
-  Set<String> getRefs() {
-    return fetchAllRefs ? Sets.newHashSet(ALL_REFS) : delta;
+  Set<FetchRefSpec> getRefSpecs() {
+    return fetchAllRefs ? Set.of(FetchRefSpec.fromRef(ALL_REFS)) : delta;
   }
 
-  void addRefs(Set<String> refs) {
+  Set<String> getRefs() {
+    return getRefSpecs().stream()
+        .map(FetchRefSpec::refName)
+        .collect(Collectors.toUnmodifiableSet());
+  }
+
+  void addRefs(Set<FetchRefSpec> refs) {
     if (!fetchAllRefs) {
-      for (String ref : refs) {
+      for (FetchRefSpec ref : refs) {
         addRef(ref);
       }
     }
   }
 
-  void addState(String ref, ReplicationState state) {
+  void addState(FetchRefSpec ref, ReplicationState state) {
     stateMap.put(ref, state);
   }
 
-  ListMultimap<String, ReplicationState> getStates() {
+  ListMultimap<FetchRefSpec, ReplicationState> getStates() {
     return stateMap;
   }
 
@@ -249,12 +255,12 @@
     return statesSet.toArray(new ReplicationState[statesSet.size()]);
   }
 
-  ReplicationState[] getStatesByRef(String ref) {
-    Collection<ReplicationState> states = stateMap.get(ref);
+  ReplicationState[] getStatesByRef(FetchRefSpec refSpec) {
+    Collection<ReplicationState> states = stateMap.get(refSpec);
     return states.toArray(new ReplicationState[states.size()]);
   }
 
-  void addStates(ListMultimap<String, ReplicationState> states) {
+  void addStates(ListMultimap<FetchRefSpec, ReplicationState> states) {
     stateMap.putAll(states);
   }
 
@@ -264,12 +270,12 @@
 
   private void statesCleanUp() {
     if (!stateMap.isEmpty() && !isRetrying()) {
-      for (Map.Entry<String, ReplicationState> entry : stateMap.entries()) {
+      for (Map.Entry<FetchRefSpec, ReplicationState> entry : stateMap.entries()) {
         entry
             .getValue()
             .notifyRefReplicated(
                 projectName.get(),
-                entry.getKey(),
+                entry.getKey().refName(),
                 uri,
                 ReplicationState.RefFetchResult.FAILED,
                 null);
@@ -342,7 +348,7 @@
       long startedAt = context.getStartTime();
       long delay = NANOSECONDS.toMillis(startedAt - createdAt);
       git = gitManager.openRepository(projectName);
-      List<RefSpec> fetchRefSpecs = runImpl();
+      List<FetchRefSpec> fetchRefSpecs = runImpl();
 
       if (fetchRefSpecs.isEmpty()) {
         repLog.info(
@@ -443,9 +449,9 @@
     repLog.info("[{}] Cannot replicate from {}. It was canceled while running", taskIdHex, uri, e);
   }
 
-  private List<RefSpec> runImpl() throws IOException {
+  private List<FetchRefSpec> runImpl() throws IOException {
     Fetch fetch = fetchFactory.create(taskIdHex, uri, git);
-    List<RefSpec> fetchRefSpecs = getFetchRefSpecs();
+    List<FetchRefSpec> fetchRefSpecs = getFetchRefSpecs();
 
     try {
       updateStates(fetch.fetch(fetchRefSpecs));
@@ -457,7 +463,7 @@
           uri,
           inexistentRef);
       fetchFailures.add(e);
-      delta.remove(inexistentRef);
+      delta.remove(FetchRefSpec.fromRef(inexistentRef));
       if (delta.isEmpty()) {
         repLog.warn("[{}] Empty replication task, skipping.", taskIdHex);
         return Collections.emptyList();
@@ -486,8 +492,9 @@
    *
    * @return The list of refSpecs to fetch
    */
-  public List<RefSpec> getFetchRefSpecs() throws IOException {
-    List<RefSpec> configRefSpecs = config.getFetchRefSpecs();
+  public List<FetchRefSpec> getFetchRefSpecs() throws IOException {
+    List<FetchRefSpec> configRefSpecs =
+        config.getFetchRefSpecs().stream().map(FetchRefSpec::fromRefSpec).toList();
 
     if (delta.isEmpty() && replicationFetchFilter().isEmpty()) {
       return configRefSpecs;
@@ -500,7 +507,7 @@
         .collect(Collectors.toList());
   }
 
-  public List<RefSpec> safeGetFetchRefSpecs() {
+  public List<FetchRefSpec> safeGetFetchRefSpecs() {
     try {
       return getFetchRefSpecs();
     } catch (IOException e) {
@@ -509,14 +516,14 @@
     }
   }
 
-  private Set<String> computeDeltaIfNeeded() throws IOException {
+  private Set<FetchRefSpec> computeDeltaIfNeeded() throws IOException {
     if (!delta.isEmpty()) {
       return delta;
     }
     return staleOrMissingLocalRefs();
   }
 
-  private Set<String> staleOrMissingLocalRefs() throws IOException {
+  private Set<FetchRefSpec> staleOrMissingLocalRefs() throws IOException {
     Map<String, Ref> localRefsMap = fetchRefsDatabase.getLocalRefsMap(git);
     Map<String, Ref> remoteRefsMap = fetchRefsDatabase.getRemoteRefsMap(git, uri);
 
@@ -524,7 +531,7 @@
         .filter(
             srcRef -> {
               // that match our configured refSpecs
-              return refToFetchRefSpec(srcRef, config.getFetchRefSpecs())
+              return refToFetchRefSpec(FetchRefSpec.fromRef(srcRef), config.getFetchRefSpecs())
                   .flatMap(
                       spec ->
                           shouldBeFetched(srcRef, localRefsMap, remoteRefsMap)
@@ -532,6 +539,7 @@
                               : Optional.empty())
                   .isPresent();
             })
+        .map(FetchRefSpec::fromRef)
         .collect(Collectors.toSet());
   }
 
@@ -548,21 +556,31 @@
         .flatMap(filter -> Optional.ofNullable(filter.get()));
   }
 
-  private Set<String> runRefsFilter(Set<String> refs) {
-    return replicationFetchFilter().map(f -> f.filter(this.projectName.get(), refs)).orElse(refs);
+  private Set<FetchRefSpec> runRefsFilter(Set<FetchRefSpec> refs) {
+    Set<String> refsNames =
+        refs.stream().map(FetchRefSpec::refName).collect(Collectors.toUnmodifiableSet());
+    Set<String> filteredRefNames =
+        replicationFetchFilter()
+            .map(f -> f.filter(this.projectName.get(), refsNames))
+            .orElse(refsNames);
+    return refs.stream()
+        .filter(refSpec -> filteredRefNames.contains(refSpec.refName()))
+        .collect(Collectors.toUnmodifiableSet());
   }
 
-  private Optional<RefSpec> refToFetchRefSpec(String ref, List<RefSpec> configRefSpecs) {
-    for (RefSpec refSpec : configRefSpecs) {
-      if (refSpec.matchSource(ref)) {
-        return Optional.of(refSpec.expandFromSource(ref));
+  private Optional<FetchRefSpec> refToFetchRefSpec(
+      FetchRefSpec refSpec, List<? extends RefSpec> configRefSpecs) {
+    for (RefSpec configRefSpec : configRefSpecs) {
+      if (configRefSpec.matchSource(refSpec.refName())) {
+        return Optional.of(
+            FetchRefSpec.fromRefSpec(configRefSpec.expandFromSource(refSpec.refName())));
       }
     }
     return Optional.empty();
   }
 
   private void updateStates(List<RefUpdateState> refUpdates) throws IOException {
-    Set<String> doneRefs = new HashSet<>();
+    Set<RefSpec> doneRefSpecs = new HashSet<>();
     boolean anyRefFailed = false;
     RefUpdate.Result lastRefUpdateResult = RefUpdate.Result.NO_CHANGE;
 
@@ -571,11 +589,11 @@
       Set<ReplicationState> logStates = new HashSet<>();
       lastRefUpdateResult = u.getResult();
 
-      logStates.addAll(stateMap.get(u.getRemoteName()));
-      logStates.addAll(stateMap.get(ALL_REFS));
+      logStates.addAll(stateMap.get(FetchRefSpec.fromRef(u.getRemoteName())));
+      logStates.addAll(stateMap.get(FetchRefSpec.fromRef(ALL_REFS)));
       ReplicationState[] logStatesArray = logStates.toArray(new ReplicationState[logStates.size()]);
 
-      doneRefs.add(u.getRemoteName());
+      doneRefSpecs.add(FetchRefSpec.fromRef(u.getRemoteName()));
       switch (u.getResult()) {
         case NO_CHANGE:
         case NEW:
@@ -614,14 +632,14 @@
           break;
       }
 
-      for (ReplicationState rs : getStatesByRef(u.getRemoteName())) {
+      for (ReplicationState rs : getStatesByRef(FetchRefSpec.fromRef(u.getRemoteName()))) {
         rs.notifyRefReplicated(
             projectName.get(), u.getRemoteName(), uri, fetchStatus, u.getResult());
       }
     }
 
-    doneRefs.add(ALL_REFS);
-    for (ReplicationState rs : getStatesByRef(ALL_REFS)) {
+    doneRefSpecs.add(FetchRefSpec.fromRef(ALL_REFS));
+    for (ReplicationState rs : getStatesByRef(FetchRefSpec.fromRef(ALL_REFS))) {
       rs.notifyRefReplicated(
           projectName.get(),
           ALL_REFS,
@@ -631,31 +649,31 @@
               : ReplicationState.RefFetchResult.SUCCEEDED,
           lastRefUpdateResult);
     }
-    for (Map.Entry<String, ReplicationState> entry : stateMap.entries()) {
-      if (!doneRefs.contains(entry.getKey())) {
+    for (Map.Entry<FetchRefSpec, ReplicationState> entry : stateMap.entries()) {
+      if (!doneRefSpecs.contains(entry.getKey())) {
         entry
             .getValue()
             .notifyRefReplicated(
                 projectName.get(),
-                entry.getKey(),
+                entry.getKey().refName(),
                 uri,
                 ReplicationState.RefFetchResult.NOT_ATTEMPTED,
                 null);
       }
     }
 
-    for (String doneRef : doneRefs) {
+    for (RefSpec doneRef : doneRefSpecs) {
       stateMap.removeAll(doneRef);
     }
   }
 
   private void notifyRefReplicatedIOException() {
-    for (Map.Entry<String, ReplicationState> entry : stateMap.entries()) {
+    for (Map.Entry<FetchRefSpec, ReplicationState> entry : stateMap.entries()) {
       entry
           .getValue()
           .notifyRefReplicated(
               projectName.get(),
-              entry.getKey(),
+              entry.getKey().refName(),
               uri,
               ReplicationState.RefFetchResult.FAILED,
               RefUpdate.Result.IO_FAILURE);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchRefSpec.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchRefSpec.java
new file mode 100644
index 0000000..613b0cf
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/FetchRefSpec.java
@@ -0,0 +1,46 @@
+// 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.common.base.MoreObjects;
+import java.util.List;
+import org.eclipse.jgit.transport.RefSpec;
+
+public class FetchRefSpec extends RefSpec {
+
+  public static FetchRefSpec fromRefSpec(RefSpec refSpec) {
+    return new FetchRefSpec(refSpec.toString());
+  }
+
+  public static FetchRefSpec fromRef(String refName) {
+    return new FetchRefSpec(refName);
+  }
+
+  public static List<RefSpec> toListOfRefSpec(List<FetchRefSpec> fetchRefSpecsList) {
+    return List.copyOf(fetchRefSpecsList);
+  }
+
+  private FetchRefSpec(String refSpecString) {
+    super(refSpecString);
+  }
+
+  public boolean equalsToRef(String cmpRefName) {
+    return cmpRefName.equals(refName());
+  }
+
+  public String refName() {
+    return MoreObjects.firstNonNull(getSource(), getDestination());
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
index 922ce32..9168c29 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/Source.java
@@ -350,7 +350,7 @@
   }
 
   private boolean shouldReplicate(
-      final Project.NameKey project, String ref, ReplicationState... states) {
+      final Project.NameKey project, FetchRefSpec refSpec, ReplicationState... states) {
     try {
       return threadScoper
           .scope(
@@ -364,7 +364,7 @@
                     repLog.warn(
                         "NOT scheduling replication {}:{} because could not open source project",
                         project,
-                        ref,
+                        refSpec,
                         e);
                     return false;
                   }
@@ -372,28 +372,28 @@
                     repLog.warn(
                         "NOT scheduling replication {}:{} because project does not exist",
                         project,
-                        ref);
+                        refSpec);
                     throw new NoSuchProjectException(project);
                   }
                   if (!shouldReplicate(projectState.get(), userProvider.get())) {
                     return false;
                   }
-                  if (FetchOne.ALL_REFS.equals(ref)) {
+                  if (refSpec.equalsToRef(FetchOne.ALL_REFS)) {
                     return true;
                   }
                   try {
-                    if (!ref.startsWith(RefNames.REFS_CHANGES)) {
+                    if (!refSpec.refName().startsWith(RefNames.REFS_CHANGES)) {
                       permissionBackend
                           .user(userProvider.get())
                           .project(project)
-                          .ref(ref)
+                          .ref(refSpec.refName())
                           .check(RefPermission.READ);
                     }
                   } catch (AuthException e) {
                     repLog.warn(
                         "NOT scheduling replication {}:{} because lack of permissions to access project/ref",
                         project,
-                        ref);
+                        refSpec);
                     return false;
                   }
                   return true;
@@ -406,7 +406,7 @@
       Throwables.throwIfUnchecked(e);
       throw new RuntimeException(e);
     }
-    repLog.warn("NOT scheduling replication {}:{}", project, ref);
+    repLog.warn("NOT scheduling replication {}:{}", project, refSpec);
     return false;
   }
 
@@ -441,16 +441,16 @@
 
   public Future<?> schedule(
       Project.NameKey project,
-      String ref,
+      FetchRefSpec refSpec,
       ReplicationState state,
       Optional<PullReplicationApiRequestMetrics> apiRequestMetrics) {
     URIish uri = getURI(project);
-    return schedule(project, ref, uri, state, apiRequestMetrics, false);
+    return schedule(project, refSpec, uri, state, apiRequestMetrics, false);
   }
 
   public Future<?> scheduleNow(
       Project.NameKey project,
-      String ref,
+      FetchRefSpec ref,
       ReplicationState state,
       Optional<PullReplicationApiRequestMetrics> apiRequestMetrics) {
     URIish uri = getURI(project);
@@ -459,7 +459,7 @@
 
   public Future<?> schedule(
       Project.NameKey project,
-      String ref,
+      FetchRefSpec ref,
       URIish uri,
       ReplicationState state,
       Optional<PullReplicationApiRequestMetrics> apiRequestMetrics) {
@@ -468,7 +468,7 @@
 
   public Future<?> scheduleNow(
       Project.NameKey project,
-      String ref,
+      FetchRefSpec ref,
       URIish uri,
       ReplicationState state,
       Optional<PullReplicationApiRequestMetrics> apiRequestMetrics) {
@@ -477,14 +477,14 @@
 
   private Future<?> schedule(
       Project.NameKey project,
-      String ref,
+      FetchRefSpec refSpec,
       URIish uri,
       ReplicationState state,
       Optional<PullReplicationApiRequestMetrics> apiRequestMetrics,
       boolean now) {
 
-    repLog.info("scheduling replication {}:{} => {}", uri, ref, project);
-    if (!shouldReplicate(project, ref, state)) {
+    repLog.info("scheduling replication {}:{} => {}", uri, refSpec, project);
+    if (!shouldReplicate(project, refSpec, state)) {
       queueMetrics.incrementTaskNotScheduled(this);
       return CompletableFuture.completedFuture(null);
     }
@@ -522,8 +522,8 @@
       Future<?> f = CompletableFuture.completedFuture(null);
       if (e == null || e.isRetrying()) {
         e = opFactory.create(project, uri, apiRequestMetrics);
-        addRef(e, ref);
-        e.addState(ref, state);
+        addRef(e, refSpec);
+        e.addState(refSpec, state);
         pending.put(uri, e);
         f =
             pool.schedule(
@@ -531,25 +531,25 @@
                 now ? 0 : config.getDelay(),
                 TimeUnit.SECONDS);
         queueMetrics.incrementTaskScheduled(this);
-      } else if (!e.getRefs().contains(ref)) {
-        addRef(e, ref);
-        e.addState(ref, state);
+      } else if (!e.getRefSpecs().contains(refSpec)) {
+        addRef(e, refSpec);
+        e.addState(refSpec, state);
         queueMetrics.incrementTaskMerged(this);
       } else {
         queueMetrics.incrementTaskNotScheduled(this);
       }
-      state.increaseFetchTaskCount(project.get(), ref);
-      repLog.info("scheduled {}:{} => {} to run after {}s", e, ref, project, config.getDelay());
+      state.increaseFetchTaskCount(project.get(), refSpec.refName());
+      repLog.info("scheduled {}:{} => {} to run after {}s", e, refSpec, project, config.getDelay());
       return f;
     }
   }
 
   public Optional<FetchOne> fetchSync(
       Project.NameKey project,
-      Set<String> refs,
+      Set<FetchRefSpec> refs,
       URIish uri,
       Optional<PullReplicationApiRequestMetrics> apiRequestMetrics) {
-    Set<String> refsToReplicate =
+    Set<FetchRefSpec> refsToReplicate =
         refs.stream()
             .filter(ref -> shouldReplicate(project, ref))
             .filter(ref -> config.replicatePermissions() || !ref.equals(RefNames.REFS_CONFIG))
@@ -583,9 +583,9 @@
     }
   }
 
-  private void addRef(FetchOne e, String ref) {
+  private void addRef(FetchOne e, FetchRefSpec ref) {
     e.addRef(ref);
-    postReplicationScheduledEvent(e, ref);
+    postReplicationScheduledEvent(e, ref.refName());
   }
 
   /**
@@ -629,7 +629,7 @@
           // second one fails, it will also be rescheduled and then,
           // here, find out replication to its URI is already pending
           // for retry (blocking).
-          pendingFetchOp.addRefs(fetchOp.getRefs());
+          pendingFetchOp.addRefs(fetchOp.getRefSpecs());
           pendingFetchOp.addStates(fetchOp.getStates());
           fetchOp.removeStates();
 
@@ -656,7 +656,7 @@
           pendingFetchOp.canceledByReplication();
           pending.remove(uri);
 
-          fetchOp.addRefs(pendingFetchOp.getRefs());
+          fetchOp.addRefs(pendingFetchOp.getRefSpecs());
           fetchOp.addStates(pendingFetchOp.getStates());
           pendingFetchOp.removeStates();
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceFetchPeriodically.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceFetchPeriodically.java
index 264c880..c7b5c28 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceFetchPeriodically.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/SourceFetchPeriodically.java
@@ -73,7 +73,7 @@
                 projectToFetch ->
                     source.scheduleNow(
                         projectToFetch,
-                        FetchOne.ALL_REFS,
+                        FetchRefSpec.fromRef(FetchOne.ALL_REFS),
                         fetchReplicationFactory.create(
                             new FetchResultProcessing.GitUpdateProcessing(eventDispatcher.get())),
                         metrics))
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 9cb9285..2da66af 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
@@ -37,6 +37,7 @@
 import com.google.gson.annotations.SerializedName;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.replication.pull.FetchRefSpec;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchAction.Input;
 import com.googlesource.gerrit.plugins.replication.pull.api.FetchJob.Factory;
 import com.googlesource.gerrit.plugins.replication.pull.api.exception.RemoteConfigurationMissingException;
@@ -124,19 +125,24 @@
       return batchInput;
     }
 
-    private Set<String> getFilteredRefNames(Predicate<RefInput> filterFunc) {
-      return refInputs.stream()
-          .filter(filterFunc)
-          .map(RefInput::refName)
-          .collect(Collectors.toSet());
+    private Stream<String> getFilteredRefNames(Predicate<RefInput> filterFunc) {
+      return refInputs.stream().filter(filterFunc).map(RefInput::refName);
     }
 
-    public Set<String> getNonDeletedRefNames() {
-      return getFilteredRefNames(RefInput.IS_DELETE.negate());
+    private Stream<FetchRefSpec> getFilteredRefSpecs(Predicate<RefInput> filterFunc) {
+      return getFilteredRefNames(filterFunc).map(FetchRefSpec::fromRef);
+    }
+
+    public Set<FetchRefSpec> getNonDeletedRefSpecs() {
+      return getFilteredRefSpecs(RefInput.IS_DELETE.negate()).collect(Collectors.toSet());
+    }
+
+    public boolean hasDeletedRefSpecs() {
+      return refInputs.stream().anyMatch(RefInput.IS_DELETE);
     }
 
     public Set<String> getDeletedRefNames() {
-      return getFilteredRefNames(RefInput.IS_DELETE);
+      return getFilteredRefNames(RefInput.IS_DELETE).collect(Collectors.toSet());
     }
   }
 
@@ -184,14 +190,14 @@
   private Response<?> applySync(Project.NameKey project, BatchInput input)
       throws InterruptedException, ExecutionException, RemoteConfigurationMissingException,
           TimeoutException, TransportException {
-    command.fetchSync(project, input.label, input.getNonDeletedRefNames());
+    command.fetchSync(project, input.label, input.getNonDeletedRefSpecs());
 
     /* 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()) {
+    if (input.hasDeletedRefSpecs()) {
       deleteRefCommand.deleteRefsSync(project, input.getDeletedRefNames(), input.label);
     }
     return Response.created(input);
@@ -211,7 +217,7 @@
             .get()
             .getRestUrl("a/config/server/tasks/" + HexFormat.fromInt(task.getTaskId()));
 
-    if (!batchInput.getDeletedRefNames().isEmpty()) {
+    if (batchInput.hasDeletedRefSpecs()) {
       workQueue.getDefaultQueue().submit(deleteJobFactory.create(project, batchInput));
     }
     // We're in a HTTP handler, so must be present.
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java
index 3227698..7984873 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommand.java
@@ -23,6 +23,7 @@
 import com.google.inject.Inject;
 import com.googlesource.gerrit.plugins.replication.pull.Command;
 import com.googlesource.gerrit.plugins.replication.pull.FetchOne;
+import com.googlesource.gerrit.plugins.replication.pull.FetchRefSpec;
 import com.googlesource.gerrit.plugins.replication.pull.FetchResultProcessing;
 import com.googlesource.gerrit.plugins.replication.pull.PullReplicationStateLogger;
 import com.googlesource.gerrit.plugins.replication.pull.ReplicationState;
@@ -40,7 +41,6 @@
 import java.util.concurrent.TimeoutException;
 import java.util.stream.Collectors;
 import org.eclipse.jgit.errors.TransportException;
-import org.eclipse.jgit.transport.RefSpec;
 
 public class FetchCommand implements Command {
 
@@ -64,23 +64,23 @@
   public void fetchAsync(
       Project.NameKey name,
       String label,
-      Set<String> refsNames,
+      Set<FetchRefSpec> refsSpecs,
       PullReplicationApiRequestMetrics apiRequestMetrics)
       throws InterruptedException, ExecutionException, RemoteConfigurationMissingException,
           TimeoutException, TransportException {
-    fetch(name, label, refsNames, ASYNC, Optional.of(apiRequestMetrics));
+    fetch(name, label, refsSpecs, ASYNC, Optional.of(apiRequestMetrics));
   }
 
-  public void fetchSync(Project.NameKey name, String label, Set<String> refsNames)
+  public void fetchSync(Project.NameKey name, String label, Set<FetchRefSpec> refsSpecs)
       throws InterruptedException, ExecutionException, RemoteConfigurationMissingException,
           TimeoutException, TransportException {
-    fetch(name, label, refsNames, SYNC, Optional.empty());
+    fetch(name, label, refsSpecs, SYNC, Optional.empty());
   }
 
   private void fetch(
       Project.NameKey name,
       String label,
-      Set<String> refsNames,
+      Set<FetchRefSpec> refSpecs,
       ReplicationType fetchType,
       Optional<PullReplicationApiRequestMetrics> apiRequestMetrics)
       throws InterruptedException, ExecutionException, RemoteConfigurationMissingException,
@@ -99,8 +99,8 @@
       if (fetchType == ReplicationType.ASYNC) {
         state.markAllFetchTasksScheduled();
         List<Future<?>> futures = new ArrayList<>();
-        for (String refName : refsNames) {
-          futures.add(source.get().schedule(name, refName, state, apiRequestMetrics));
+        for (FetchRefSpec refSpec : refSpecs) {
+          futures.add(source.get().schedule(name, refSpec, state, apiRequestMetrics));
         }
         int timeout = source.get().getTimeout();
         for (Future future : futures) {
@@ -112,7 +112,7 @@
         }
       } else {
         Optional<FetchOne> maybeFetch =
-            source.get().fetchSync(name, refsNames, source.get().getURI(name), apiRequestMetrics);
+            source.get().fetchSync(name, refSpecs, source.get().getURI(name), apiRequestMetrics);
         if (maybeFetch.map(FetchOne::safeGetFetchRefSpecs).filter(List::isEmpty).isPresent()) {
           fetchStateLog.warn(
               String.format(
@@ -140,7 +140,6 @@
   }
 
   private TransportException newTransportException(FetchOne fetchOne) {
-    List<RefSpec> fetchRefSpecs = fetchOne.safeGetFetchRefSpecs();
     String combinedErrorMessage =
         fetchOne.getFetchFailures().stream()
             .map(TransportException::getMessage)
@@ -148,7 +147,7 @@
     return new TransportException(
         String.format(
             "[%s] %s trying to fetch %s",
-            fetchOne.getTaskIdHex(), combinedErrorMessage, fetchRefSpecs));
+            fetchOne.getTaskIdHex(), combinedErrorMessage, fetchOne.safeGetFetchRefSpecs()));
   }
 
   @Override
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 d30ba74..3b0ba93 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.getNonDeletedRefNames(), metrics);
+      command.fetchAsync(project, batchInput.label, batchInput.getNonDeletedRefSpecs(), 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.getNonDeletedRefNames());
+          project.get(), batchInput.label, batchInput.getNonDeletedRefSpecs());
     }
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/BatchFetchClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/BatchFetchClient.java
index 188f69b..b4128da 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/BatchFetchClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/BatchFetchClient.java
@@ -17,11 +17,11 @@
 import com.google.common.collect.Lists;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.replication.pull.FetchRefSpec;
 import com.googlesource.gerrit.plugins.replication.pull.SourceConfiguration;
 import java.io.IOException;
 import java.util.List;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.URIish;
 
 public class BatchFetchClient implements Fetch {
@@ -40,9 +40,9 @@
   }
 
   @Override
-  public List<RefUpdateState> fetch(List<RefSpec> refs) throws IOException {
+  public List<RefUpdateState> fetch(List<FetchRefSpec> refs) throws IOException {
     List<RefUpdateState> results = Lists.newArrayList();
-    for (List<RefSpec> refsBatch : Lists.partition(refs, batchSize)) {
+    for (List<FetchRefSpec> refsBatch : Lists.partition(refs, batchSize)) {
       results.addAll(fetchClient.fetch(refsBatch));
     }
     return results;
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 43ec9bf..e3d64cd 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
@@ -20,6 +20,7 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import com.googlesource.gerrit.plugins.replication.CredentialsFactory;
+import com.googlesource.gerrit.plugins.replication.pull.FetchRefSpec;
 import com.googlesource.gerrit.plugins.replication.pull.SourceConfiguration;
 import java.io.BufferedReader;
 import java.io.File;
@@ -35,7 +36,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.CredentialItem;
 import org.eclipse.jgit.transport.CredentialsProvider;
-import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.URIish;
 
 public class CGitFetch implements Fetch {
@@ -61,7 +61,7 @@
   }
 
   @Override
-  public List<RefUpdateState> fetch(List<RefSpec> refsSpec) throws IOException {
+  public List<RefUpdateState> fetch(List<FetchRefSpec> refsSpec) throws IOException {
     List<String> refs = refsSpec.stream().map(s -> s.toString()).collect(Collectors.toList());
     List<String> command = Lists.newArrayList("git", "fetch");
     if (isMirror) {
@@ -91,7 +91,7 @@
       return refsSpec.stream()
           .map(
               value -> {
-                return new RefUpdateState(value.getSource(), RefUpdate.Result.NEW);
+                return new RefUpdateState(value.refName(), RefUpdate.Result.NEW);
               })
           .collect(Collectors.toList());
     } catch (TransportException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/Fetch.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/Fetch.java
index 1e99a94..866d28d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/Fetch.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/Fetch.java
@@ -14,11 +14,11 @@
 
 package com.googlesource.gerrit.plugins.replication.pull.fetch;
 
+import com.googlesource.gerrit.plugins.replication.pull.FetchRefSpec;
 import java.io.IOException;
 import java.util.List;
-import org.eclipse.jgit.transport.RefSpec;
 
 @FunctionalInterface
 public interface Fetch {
-  public List<RefUpdateState> fetch(List<RefSpec> refs) throws IOException;
+  public List<RefUpdateState> fetch(List<FetchRefSpec> refs) throws IOException;
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java
index 039c395..09dbd31 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/fetch/JGitFetch.java
@@ -18,6 +18,7 @@
 
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.replication.pull.FetchRefSpec;
 import com.googlesource.gerrit.plugins.replication.pull.transport.TransportProvider;
 import java.io.IOException;
 import java.util.List;
@@ -49,10 +50,10 @@
   }
 
   @Override
-  public List<RefUpdateState> fetch(List<RefSpec> refs) throws IOException {
+  public List<RefUpdateState> fetch(List<FetchRefSpec> refs) throws IOException {
     FetchResult res;
     try (Transport tn = transportProvider.open(git, uri)) {
-      res = fetchVia(tn, refs);
+      res = fetchVia(tn, FetchRefSpec.toListOfRefSpec(refs));
     }
     return res.getTrackingRefUpdates().stream()
         .map(value -> new RefUpdateState(value.getRemoteName(), value.getResult()))
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 3ca937c..c097c7e 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
@@ -48,7 +48,6 @@
 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;
@@ -86,7 +85,7 @@
       Fetch objectUnderTest =
           fetchFactory.create(TEST_TASK_ID, new URIish(testRepoPath.toString()), repo);
 
-      objectUnderTest.fetch(Lists.newArrayList(new RefSpec(sourceRef + ":" + sourceRef)));
+      objectUnderTest.fetch(Lists.newArrayList(FetchRefSpec.fromRef(sourceRef + ":" + sourceRef)));
 
       waitUntil(() -> checkedGetRef(repo, sourceRef) != null);
 
@@ -107,7 +106,7 @@
       Fetch objectUnderTest =
           fetchFactory.create(TEST_TASK_ID, new URIish(testRepoPath.toString()), repo);
 
-      objectUnderTest.fetch(Lists.newArrayList(new RefSpec(nonExistingRef)));
+      objectUnderTest.fetch(Lists.newArrayList(FetchRefSpec.fromRef(nonExistingRef)));
     }
   }
 
@@ -122,7 +121,7 @@
       Fetch objectUnderTest =
           fetchFactory.create(TEST_TASK_ID, new URIish("/not_existing_path/"), repo);
 
-      objectUnderTest.fetch(Lists.newArrayList(new RefSpec(sourceRef + ":" + sourceRef)));
+      objectUnderTest.fetch(Lists.newArrayList(FetchRefSpec.fromRef(sourceRef + ":" + sourceRef)));
     }
   }
 
@@ -142,8 +141,8 @@
 
       objectUnderTest.fetch(
           Lists.newArrayList(
-              new RefSpec(sourceRefOne + ":" + sourceRefOne),
-              new RefSpec(sourceRefTwo + ":" + sourceRefTwo)));
+              FetchRefSpec.fromRef(sourceRefOne + ":" + sourceRefOne),
+              FetchRefSpec.fromRef(sourceRefTwo + ":" + sourceRefTwo)));
 
       waitUntil(
           () ->
@@ -183,9 +182,9 @@
 
     objectUnderTest.fetch(
         Lists.newArrayList(
-            new RefSpec("refs/changes/01/1/1:refs/changes/01/1/1"),
-            new RefSpec("refs/changes/02/2/1:refs/changes/02/2/1"),
-            new RefSpec("refs/changes/03/3/1:refs/changes/03/3/1")));
+            FetchRefSpec.fromRef("refs/changes/01/1/1:refs/changes/01/1/1"),
+            FetchRefSpec.fromRef("refs/changes/02/2/1:refs/changes/02/2/1"),
+            FetchRefSpec.fromRef("refs/changes/03/3/1:refs/changes/03/3/1")));
     verify(fetchClient, times(2)).fetch(any());
   }
 
@@ -205,7 +204,7 @@
       Fetch objectUnderTest =
           fetchFactory.create(TEST_TASK_ID, new URIish(testRepoPath.toString()), repo);
 
-      objectUnderTest.fetch(Lists.newArrayList(new RefSpec(newBranch + ":" + newBranch)));
+      objectUnderTest.fetch(Lists.newArrayList(FetchRefSpec.fromRef(newBranch + ":" + newBranch)));
 
       waitUntil(() -> checkedGetRef(repo, newBranch) != null);
 
@@ -231,7 +230,8 @@
           fetchFactory.create(TEST_TASK_ID, new URIish(testRepoPath.toString()), repo);
 
       objectUnderTest.fetch(
-          Lists.newArrayList(new RefSpec("non_existing_branch" + ":" + "non_existing_branch")));
+          Lists.newArrayList(
+              FetchRefSpec.fromRef("non_existing_branch" + ":" + "non_existing_branch")));
     }
   }
 
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 34e9ae3..423e96d 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
@@ -47,7 +47,6 @@
 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;
@@ -58,7 +57,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/*");
+  private static final FetchRefSpec ALL_REFS = FetchRefSpec.fromRef("+refs/*:refs/*");
 
   @Inject private SitePaths sitePaths;
   @Inject private ProjectOperations projectOperations;
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
index 86b0792..8903b2b 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/FetchOneTest.java
@@ -68,10 +68,11 @@
   private final String TEST_PROJECT_NAME = "FetchOneTest";
   private final Project.NameKey PROJECT_NAME = Project.NameKey.parse(TEST_PROJECT_NAME);
   private final String TEST_REF = "refs/heads/refForReplicationTask";
+  private final FetchRefSpec TEST_REF_SPEC = FetchRefSpec.fromRef(TEST_REF);
   private final String URI_PATTERN = "http://test.com/" + TEST_PROJECT_NAME + ".git";
   private final TestMetricMaker testMetricMaker = new TestMetricMaker();
 
-  private final RefSpec ALL_REFS_SPEC = new RefSpec("refs/*:refs/*");
+  private final RefSpec ALL_REFS_SPEC = FetchRefSpec.fromRef("refs/*:refs/*");
 
   @Mock private GitRepositoryManager grm;
   @Mock private Repository repository;
@@ -132,63 +133,64 @@
 
   @Test
   public void shouldIncludeTheTaskIndexInItsStringRepresentation() {
-    objectUnderTest.addRefs(Set.of("refs/heads/foo", "refs/heads/bar"));
+    objectUnderTest.addRefs(refSpecsSetOf("refs/heads/foo", "refs/heads/bar"));
     String expected =
         "["
             + objectUnderTest.getTaskIdHex()
             + "] fetch "
             + URI_PATTERN
-            + " [refs/heads/bar,refs/heads/foo]";
+            + " [refs/heads/bar, refs/heads/foo]";
 
     assertThat(objectUnderTest.toString()).isEqualTo(expected);
   }
 
   @Test
   public void shouldIncludeTheRetryCountInItsStringRepresentationWhenATaskIsRetried() {
-    objectUnderTest.addRefs(Set.of("refs/heads/bar", "refs/heads/foo"));
+    objectUnderTest.addRefs(refSpecsSetOf("refs/heads/bar", "refs/heads/foo"));
     objectUnderTest.setToRetry();
     String expected =
         "(retry 1) ["
             + objectUnderTest.getTaskIdHex()
             + "] fetch "
             + URI_PATTERN
-            + " [refs/heads/bar,refs/heads/foo]";
+            + " [refs/heads/bar, refs/heads/foo]";
 
     assertThat(objectUnderTest.toString()).isEqualTo(expected);
   }
 
   @Test
   public void shouldAddARefToTheDeltaIfItsNotTheAllRefs() {
-    Set<String> refs = Set.of(TEST_REF);
+    Set<FetchRefSpec> refs = Set.of(TEST_REF_SPEC);
     objectUnderTest.addRefs(refs);
 
-    assertThat(refs).isEqualTo(objectUnderTest.getRefs());
+    assertThat(refs).isEqualTo(objectUnderTest.getRefSpecs());
   }
 
   @Test
   public void shouldIgnoreEveryRefButTheAllRefsWhenAddingARef() {
-    objectUnderTest.addRefs(Set.of(TEST_REF, FetchOne.ALL_REFS));
+    objectUnderTest.addRefs(refSpecsSetOf(TEST_REF, FetchOne.ALL_REFS));
 
     assertThat(Set.of(FetchOne.ALL_REFS)).isEqualTo(objectUnderTest.getRefs());
   }
 
   @Test
   public void shouldReturnExistingStates() {
-    assertThat(createTestStates(TEST_REF, 1)).isEqualTo(objectUnderTest.getStates().get(TEST_REF));
+    assertThat(createTestStates(TEST_REF_SPEC, 1))
+        .isEqualTo(objectUnderTest.getStates().get(TEST_REF_SPEC));
   }
 
   @Test
   public void shouldKeepMultipleStatesInInsertionOrderForARef() {
-    List<ReplicationState> states = createTestStates(TEST_REF, 2);
+    List<ReplicationState> states = createTestStates(TEST_REF_SPEC, 2);
 
-    List<ReplicationState> actualStates = objectUnderTest.getStates().get(TEST_REF);
+    List<ReplicationState> actualStates = objectUnderTest.getStates().get(TEST_REF_SPEC);
 
     assertThat(actualStates).containsExactlyElementsIn(states).inOrder();
   }
 
   @Test
   public void shouldReturnStatesInAnArray() {
-    List<ReplicationState> states = createTestStates(TEST_REF, 2);
+    List<ReplicationState> states = createTestStates(TEST_REF_SPEC, 2);
 
     ReplicationState[] actualStates = objectUnderTest.getStatesAsArray();
 
@@ -197,7 +199,7 @@
 
   @Test
   public void shouldClearTheStates() {
-    createTestStates(TEST_REF, 2);
+    createTestStates(TEST_REF_SPEC, 2);
 
     objectUnderTest.removeStates();
 
@@ -215,7 +217,7 @@
   @Test
   public void shouldRunAReplicationTaskForAllRefsIfDeltaIsEmpty() throws Exception {
     setupMocks(true);
-    List<ReplicationState> states = createTestStates(FetchOne.ALL_REFS, 1);
+    List<ReplicationState> states = createTestStates(FetchRefSpec.fromRef(FetchOne.ALL_REFS), 1);
     setupFetchFactoryMock(Collections.emptyList());
 
     objectUnderTest.run();
@@ -264,11 +266,11 @@
                     .withRemoteName("testRemote")
                     .withResult(RefUpdate.Result.NEW)
                     .build()));
-    objectUnderTest.addRefs(Set.of(TEST_REF));
+    objectUnderTest.addRefs(Set.of(TEST_REF_SPEC));
 
     objectUnderTest.run();
 
-    verify(mockFetch).fetch(List.of(new RefSpec(TEST_REF)));
+    verify(mockFetch).fetch(List.of(TEST_REF_SPEC));
   }
 
   @Test
@@ -277,10 +279,10 @@
           throws Exception {
     setupMocks(true);
     String someRef = "refs/heads/someRef";
-    List<ReplicationState> states = createTestStates(someRef, 1);
+    List<ReplicationState> states = createTestStates(FetchRefSpec.fromRef(someRef), 1);
     setupFetchFactoryMock(
         List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()));
-    objectUnderTest.addRefs(Set.of(TEST_REF));
+    objectUnderTest.addRefs(Set.of(TEST_REF_SPEC));
 
     objectUnderTest.run();
 
@@ -307,11 +309,13 @@
   @Test
   public void shouldFilterOutRefsFromFetchReplicationDelta() throws Exception {
     setupMocks(true);
-    String filteredRef = "refs/heads/filteredRef";
-    Set<String> refSpecs = Set.of(TEST_REF, filteredRef);
+    FetchRefSpec filteredRef = FetchRefSpec.fromRef("refs/heads/filteredRef");
+    Set<FetchRefSpec> refSpecs = Set.of(TEST_REF_SPEC, filteredRef);
+    Set<String> refs = Set.of(TEST_REF, filteredRef.refName());
     List<ReplicationState> states =
         Stream.concat(
-                createTestStates(TEST_REF, 1).stream(), createTestStates(filteredRef, 1).stream())
+                createTestStates(TEST_REF_SPEC, 1).stream(),
+                createTestStates(filteredRef, 1).stream())
             .collect(Collectors.toList());
     setupFetchFactoryMock(
         List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()),
@@ -320,7 +324,7 @@
     objectUnderTest.setReplicationFetchFilter(replicationFilter);
     ReplicationFetchFilter mockFilter = mock(ReplicationFetchFilter.class);
     when(replicationFilter.get()).thenReturn(mockFilter);
-    when(mockFilter.filter(TEST_PROJECT_NAME, refSpecs)).thenReturn(Set.of(TEST_REF));
+    when(mockFilter.filter(TEST_PROJECT_NAME, refs)).thenReturn(Set.of(TEST_REF));
 
     objectUnderTest.run();
 
@@ -334,7 +338,11 @@
             RefUpdate.Result.NEW);
     verify(states.get(1))
         .notifyRefReplicated(
-            TEST_PROJECT_NAME, filteredRef, urIish, ReplicationState.RefFetchResult.FAILED, null);
+            TEST_PROJECT_NAME,
+            filteredRef.refName(),
+            urIish,
+            ReplicationState.RefFetchResult.FAILED,
+            null);
   }
 
   @Test
@@ -393,7 +401,7 @@
     setupMocks(true);
     String REF = "refs/non-dev/someRef";
     Set<String> remoteRefs = Set.of(REF);
-    RefSpec DEV_REFS_SPEC = new RefSpec("refs/dev/*:refs/dev/*");
+    RefSpec DEV_REFS_SPEC = FetchRefSpec.fromRef("refs/dev/*:refs/dev/*");
 
     setupRemoteConfigMock(List.of(DEV_REFS_SPEC));
     setupFetchRefsDatabaseMock(Map.of(), Map.of(REF, mock(Ref.class)));
@@ -408,10 +416,10 @@
   public void shouldMarkTheReplicationStatusAsSucceededOnSuccessfulReplicationOfARef()
       throws Exception {
     setupMocks(true);
-    List<ReplicationState> states = createTestStates(TEST_REF, 1);
+    List<ReplicationState> states = createTestStates(TEST_REF_SPEC, 1);
     setupFetchFactoryMock(
         List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()));
-    objectUnderTest.addRefs(Set.of(TEST_REF));
+    objectUnderTest.addRefs(refSpecsSetOf(TEST_REF));
 
     objectUnderTest.run();
     assertFinishedWithEmptyStateAndNoFailures();
@@ -428,10 +436,10 @@
   public void shouldMarkAllTheStatesOfARefAsReplicatedSuccessfullyOnASuccessfulReplication()
       throws Exception {
     setupMocks(true);
-    List<ReplicationState> states = createTestStates(TEST_REF, 2);
+    List<ReplicationState> states = createTestStates(TEST_REF_SPEC, 2);
     setupFetchFactoryMock(
         List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()));
-    objectUnderTest.addRefs(Set.of(TEST_REF));
+    objectUnderTest.addRefs(refSpecsSetOf(TEST_REF));
 
     objectUnderTest.run();
 
@@ -457,12 +465,12 @@
     setupMocks(true);
     List<ReplicationState> states =
         Stream.concat(
-                createTestStates(TEST_REF, 1).stream(),
-                createTestStates(FetchOne.ALL_REFS, 1).stream())
+                createTestStates(TEST_REF_SPEC, 1).stream(),
+                createTestStates(FetchRefSpec.fromRef(FetchOne.ALL_REFS), 1).stream())
             .collect(Collectors.toList());
     setupFetchFactoryMock(
         List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()));
-    objectUnderTest.addRefs(Set.of(TEST_REF));
+    objectUnderTest.addRefs(refSpecsSetOf(TEST_REF));
 
     objectUnderTest.run();
 
@@ -487,14 +495,14 @@
   public void shouldMarkReplicationStateAsRejectedWhenTheObjectIsNotInRepository()
       throws Exception {
     setupMocks(true);
-    List<ReplicationState> states = createTestStates(TEST_REF, 2);
+    List<ReplicationState> states = createTestStates(TEST_REF_SPEC, 2);
     setupFetchFactoryMock(
         List.of(
             new FetchFactoryEntry.Builder()
                 .withRefNames(TEST_REF)
                 .withResult(RefUpdate.Result.REJECTED_MISSING_OBJECT)
                 .build()));
-    objectUnderTest.addRefs(Set.of(TEST_REF));
+    objectUnderTest.addRefs(refSpecsSetOf(TEST_REF));
 
     objectUnderTest.run();
 
@@ -511,14 +519,14 @@
   @Test
   public void shouldMarkReplicationStateAsRejectedWhenFailedForUnknownReason() throws Exception {
     setupMocks(true);
-    List<ReplicationState> states = createTestStates(TEST_REF, 1);
+    List<ReplicationState> states = createTestStates(TEST_REF_SPEC, 1);
     setupFetchFactoryMock(
         List.of(
             new FetchFactoryEntry.Builder()
                 .withRefNames(TEST_REF)
                 .withResult(RefUpdate.Result.REJECTED_OTHER_REASON)
                 .build()));
-    objectUnderTest.addRefs(Set.of(TEST_REF));
+    objectUnderTest.addRefs(refSpecsSetOf(TEST_REF));
 
     objectUnderTest.run();
 
@@ -539,10 +547,10 @@
     String forcedRef = "refs/heads/forcedRef";
     List<ReplicationState> states =
         Stream.of(
-                createTestStates(TEST_REF, 1),
-                createTestStates(failingRef, 1),
-                createTestStates(forcedRef, 1),
-                createTestStates(FetchOne.ALL_REFS, 1))
+                createTestStates(TEST_REF_SPEC, 1),
+                createTestStates(FetchRefSpec.fromRef(failingRef), 1),
+                createTestStates(FetchRefSpec.fromRef(forcedRef), 1),
+                createTestStates(FetchRefSpec.fromRef(FetchOne.ALL_REFS), 1))
             .flatMap(Collection::stream)
             .collect(Collectors.toList());
     setupFetchFactoryMock(
@@ -559,7 +567,7 @@
                 .withRefNames(forcedRef)
                 .withResult(RefUpdate.Result.FORCED)
                 .build()));
-    objectUnderTest.addRefs(Set.of(TEST_REF, failingRef, forcedRef));
+    objectUnderTest.addRefs(refSpecsSetOf(TEST_REF, failingRef, forcedRef));
 
     objectUnderTest.run();
 
@@ -597,14 +605,14 @@
   @Test
   public void shouldRetryOnLockingFailures() throws Exception {
     setupMocks(true);
-    List<ReplicationState> states = createTestStates(TEST_REF, 1);
+    List<ReplicationState> states = createTestStates(TEST_REF_SPEC, 1);
     setupFetchFactoryMock(
         List.of(
             new FetchFactoryEntry.Builder()
                 .withRefNames(TEST_REF)
                 .withResult(RefUpdate.Result.LOCK_FAILURE)
                 .build()));
-    objectUnderTest.addRefs(Set.of(TEST_REF));
+    objectUnderTest.addRefs(refSpecsSetOf(TEST_REF));
 
     objectUnderTest.run();
 
@@ -618,14 +626,14 @@
   @Test
   public void shouldNotRetryWhenMaxLockRetriesLimitIsReached() throws Exception {
     setupMocks(true);
-    List<ReplicationState> states = createTestStates(TEST_REF, 1);
+    List<ReplicationState> states = createTestStates(TEST_REF_SPEC, 1);
     setupFetchFactoryMock(
         List.of(
             new FetchFactoryEntry.Builder()
                 .withRefNames(TEST_REF)
                 .withResult(RefUpdate.Result.LOCK_FAILURE)
                 .build()));
-    objectUnderTest.addRefs(Set.of(TEST_REF));
+    objectUnderTest.addRefs(refSpecsSetOf(TEST_REF));
 
     Stream.of(1, 1).forEach(e -> objectUnderTest.run());
 
@@ -639,14 +647,14 @@
   @Test
   public void shouldNotRetryOnLockingFailuresIfTheTaskWasCancelledWhileRunning() throws Exception {
     setupMocks(true);
-    List<ReplicationState> states = createTestStates(TEST_REF, 1);
+    List<ReplicationState> states = createTestStates(TEST_REF_SPEC, 1);
     setupFetchFactoryMock(
         List.of(
             new FetchFactoryEntry.Builder()
                 .withRefNames(TEST_REF)
                 .withResult(RefUpdate.Result.LOCK_FAILURE)
                 .build()));
-    objectUnderTest.addRefs(Set.of(TEST_REF));
+    objectUnderTest.addRefs(refSpecsSetOf(TEST_REF));
     objectUnderTest.setCanceledWhileRunning();
 
     objectUnderTest.run();
@@ -661,14 +669,14 @@
   @Test
   public void shouldNotRetryForUnexpectedIOErrors() throws Exception {
     setupMocks(true);
-    List<ReplicationState> states = createTestStates(TEST_REF, 1);
+    List<ReplicationState> states = createTestStates(TEST_REF_SPEC, 1);
     setupFetchFactoryMock(
         List.of(
             new FetchFactoryEntry.Builder()
                 .withRefNames(TEST_REF)
                 .withResult(RefUpdate.Result.IO_FAILURE)
                 .build()));
-    objectUnderTest.addRefs(Set.of(TEST_REF));
+    objectUnderTest.addRefs(refSpecsSetOf(TEST_REF));
 
     objectUnderTest.run();
 
@@ -682,7 +690,7 @@
   @Test
   public void shouldTreatInexistentRefsAsFailures() throws Exception {
     setupMocks(true);
-    List<ReplicationState> states = createTestStates(TEST_REF, 1);
+    List<ReplicationState> states = createTestStates(TEST_REF_SPEC, 1);
     Fetch fetch =
         setupFetchFactoryMock(
             List.of(
@@ -692,7 +700,7 @@
                     .build()));
     when(fetch.fetch(anyList()))
         .thenThrow(new InexistentRefTransportException(TEST_REF, new Throwable("boom")));
-    objectUnderTest.addRefs(Set.of(TEST_REF));
+    objectUnderTest.addRefs(refSpecsSetOf(TEST_REF));
 
     objectUnderTest.run();
 
@@ -707,7 +715,9 @@
     setupMocks(true);
     String inexistentRef = "refs/heads/inexistentRef";
     List<ReplicationState> states =
-        Stream.of(createTestStates(inexistentRef, 1), createTestStates(TEST_REF, 1))
+        Stream.of(
+                createTestStates(FetchRefSpec.fromRef(inexistentRef), 1),
+                createTestStates(TEST_REF_SPEC, 1))
             .flatMap(Collection::stream)
             .collect(Collectors.toList());
     Fetch fetch =
@@ -724,7 +734,7 @@
     when(fetch.fetch(anyList()))
         .thenThrow(new InexistentRefTransportException(TEST_REF, new Throwable("boom")))
         .thenReturn(List.of(new RefUpdateState(TEST_REF, RefUpdate.Result.NEW)));
-    objectUnderTest.addRefs(Set.of(inexistentRef, TEST_REF));
+    objectUnderTest.addRefs(refSpecsSetOf(inexistentRef, TEST_REF));
 
     objectUnderTest.run();
 
@@ -751,7 +761,7 @@
   @Test
   public void shouldRescheduleCertainTypesOfTransportException() throws Exception {
     setupMocks(true);
-    List<ReplicationState> states = createTestStates(TEST_REF, 1);
+    List<ReplicationState> states = createTestStates(TEST_REF_SPEC, 1);
     Fetch fetch =
         setupFetchFactoryMock(
             List.of(
@@ -760,7 +770,7 @@
                     .withResult(RefUpdate.Result.NEW)
                     .build()));
     when(fetch.fetch(anyList())).thenThrow(new PackProtocolException(urIish, "boom"));
-    objectUnderTest.addRefs(Set.of(TEST_REF));
+    objectUnderTest.addRefs(refSpecsSetOf(TEST_REF));
 
     objectUnderTest.run();
 
@@ -774,7 +784,7 @@
   @Test
   public void shouldNotMarkReplicationTaskAsFailedIfItIsBeingRetried() throws Exception {
     setupMocks(true);
-    List<ReplicationState> states = createTestStates(TEST_REF, 1);
+    List<ReplicationState> states = createTestStates(TEST_REF_SPEC, 1);
     Fetch fetch =
         setupFetchFactoryMock(
             List.of(
@@ -783,7 +793,7 @@
                     .withResult(RefUpdate.Result.NEW)
                     .build()));
     when(fetch.fetch(anyList())).thenThrow(new PackProtocolException(urIish, "boom"));
-    objectUnderTest.addRefs(Set.of(TEST_REF));
+    objectUnderTest.addRefs(refSpecsSetOf(TEST_REF));
     objectUnderTest.setToRetry();
 
     objectUnderTest.run();
@@ -798,9 +808,9 @@
   public void shouldNotRecordReplicationLatencyMetricIfAllRefsAreExcluded() throws Exception {
     setupMocks(true);
     String filteredRef = "refs/heads/filteredRef";
-    Set<String> refSpecs = Set.of(TEST_REF, filteredRef);
-    createTestStates(TEST_REF, 1);
-    createTestStates(filteredRef, 1);
+    Set<FetchRefSpec> refSpecs = refSpecsSetOf(TEST_REF, filteredRef);
+    createTestStates(TEST_REF_SPEC, 1);
+    createTestStates(FetchRefSpec.fromRef(filteredRef), 1);
     setupFetchFactoryMock(
         List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()),
         Optional.of(List.of(TEST_REF)));
@@ -818,9 +828,10 @@
       throws Exception {
     setupMocks(true);
     String filteredRef = "refs/heads/filteredRef";
-    Set<String> refSpecs = Set.of(TEST_REF, filteredRef);
-    createTestStates(TEST_REF, 1);
-    createTestStates(filteredRef, 1);
+    Set<FetchRefSpec> refSpecs = refSpecsSetOf(TEST_REF, filteredRef);
+    Set<String> refs = Set.of(TEST_REF, filteredRef);
+    createTestStates(TEST_REF_SPEC, 1);
+    createTestStates(FetchRefSpec.fromRef(filteredRef), 1);
     setupFetchFactoryMock(
         List.of(new FetchFactoryEntry.Builder().refSpecNameWithDefaults(TEST_REF).build()),
         Optional.of(List.of(TEST_REF)));
@@ -828,7 +839,7 @@
     objectUnderTest.setReplicationFetchFilter(replicationFilter);
     ReplicationFetchFilter mockFilter = mock(ReplicationFetchFilter.class);
     when(replicationFilter.get()).thenReturn(mockFilter);
-    when(mockFilter.filter(TEST_PROJECT_NAME, refSpecs)).thenReturn(Set.of(TEST_REF));
+    when(mockFilter.filter(TEST_PROJECT_NAME, refs)).thenReturn(Set.of(TEST_REF));
 
     objectUnderTest.run();
 
@@ -884,12 +895,12 @@
     return mockFilter;
   }
 
-  private List<ReplicationState> createTestStates(String ref, int numberOfStates) {
+  private List<ReplicationState> createTestStates(FetchRefSpec refSpec, int numberOfStates) {
     List<ReplicationState> states =
         IntStream.rangeClosed(1, numberOfStates)
             .mapToObj(i -> Mockito.mock(ReplicationState.class))
             .collect(Collectors.toList());
-    states.forEach(rs -> objectUnderTest.addState(ref, rs));
+    states.forEach(rs -> objectUnderTest.addState(refSpec, rs));
 
     return states;
   }
@@ -909,7 +920,7 @@
       throws Exception {
     List<RefSpec> refSpecs =
         fetchFactoryEntries.stream()
-            .map(ffe -> new RefSpec(ffe.getRefSpecName()))
+            .map(ffe -> FetchRefSpec.fromRef(ffe.getRefSpecName()))
             .collect(Collectors.toList());
     List<RefUpdateState> refUpdateStates =
         fetchFactoryEntries.stream()
@@ -946,6 +957,10 @@
     verify(source).notifyFinished(objectUnderTest);
     assertThat(objectUnderTest.getFetchFailures().isEmpty()).isEqualTo(noFailures);
   }
+
+  private Set<FetchRefSpec> refSpecsSetOf(String... refs) {
+    return Stream.of(refs).map(FetchRefSpec::fromRef).collect(Collectors.toUnmodifiableSet());
+  }
 }
 
 class FetchFactoryEntry {
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 b900d8a..5b624a5 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
@@ -34,7 +34,6 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.PushResult;
-import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.URIish;
 import org.junit.Before;
 import org.junit.Test;
@@ -66,7 +65,7 @@
     try (Repository repo = repoManager.openRepository(project)) {
       Fetch objectUnderTest =
           fetchFactory.create(TEST_TASK_ID, new URIish(testRepoPath.toString()), repo);
-      objectUnderTest.fetch(Lists.newArrayList(new RefSpec(nonExistingRef)));
+      objectUnderTest.fetch(Lists.newArrayList(FetchRefSpec.fromRef(nonExistingRef)));
     }
   }
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParserTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParserTest.java
index c7102ae..09915aa 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParserTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/SourceConfigParserTest.java
@@ -23,7 +23,6 @@
 import java.util.List;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.transport.RefSpec;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -84,6 +83,6 @@
 
     assertThat(remoteConfig.getUrls()).containsExactly(TEST_REMOTE_URL);
     assertThat(remoteConfig.getRemoteConfig().getFetchRefSpecs())
-        .containsExactly(new RefSpec(TEST_REMOTE_FETCH_REFSPEC));
+        .containsExactly(FetchRefSpec.fromRef(TEST_REMOTE_FETCH_REFSPEC));
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommandTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommandTest.java
index ebf0076..a83b642 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommandTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/FetchCommandTest.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.server.events.EventDispatcher;
+import com.googlesource.gerrit.plugins.replication.pull.FetchRefSpec;
 import com.googlesource.gerrit.plugins.replication.pull.PullReplicationStateLogger;
 import com.googlesource.gerrit.plugins.replication.pull.ReplicationState;
 import com.googlesource.gerrit.plugins.replication.pull.Source;
@@ -43,9 +44,9 @@
 
 @RunWith(MockitoJUnitRunner.class)
 public class FetchCommandTest {
-  private static final String REF_NAME_TO_FETCH = "refs/heads/master";
-  private static final String ALT_REF_NAME_TO_FETCH = "refs/heads/alt";
-  private static final Set<String> REFS_NAMES_TO_FETCH =
+  private static final FetchRefSpec REF_NAME_TO_FETCH = FetchRefSpec.fromRef("refs/heads/master");
+  private static final FetchRefSpec ALT_REF_NAME_TO_FETCH = FetchRefSpec.fromRef("refs/heads/alt");
+  private static final Set<FetchRefSpec> REFS_NAMES_TO_FETCH =
       Set.of(REF_NAME_TO_FETCH, ALT_REF_NAME_TO_FETCH);
   @Mock ReplicationState state;
   @Mock ReplicationState.Factory fetchReplicationStateFactory;
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObjectIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObjectIT.java
index 60c8459..1192063 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObjectIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/fetch/ApplyObjectIT.java
@@ -44,6 +44,7 @@
 import com.googlesource.gerrit.plugins.replication.api.ConfigResource;
 import com.googlesource.gerrit.plugins.replication.api.ReplicationConfig;
 import com.googlesource.gerrit.plugins.replication.api.ReplicationConfigOverrides;
+import com.googlesource.gerrit.plugins.replication.pull.FetchRefSpec;
 import com.googlesource.gerrit.plugins.replication.pull.RevisionReader;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionData;
 import com.googlesource.gerrit.plugins.replication.pull.api.data.RevisionObjectData;
@@ -54,7 +55,6 @@
 import java.util.stream.Collectors;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.RefSpec;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -85,13 +85,13 @@
     String refName = RefNames.changeMetaRef(changeId);
     String patchSetRefName = RefNames.patchSetRef(PatchSet.id(changeId, 1));
 
-    RefSpec refSpec = new RefSpec(refName);
+    FetchRefSpec refSpec = FetchRefSpec.fromRef(refName);
     Optional<RevisionData> revisionData;
     NameKey testRepoKey = Project.nameKey(testRepoProjectName);
 
     try (Repository repo = repoManager.openRepository(testRepoKey)) {
       revisionData = reader.read(testRepoKey, repo.exactRef(refName).getObjectId(), refName, 0);
-      objectUnderTest.apply(project, new RefSpec(patchSetRefName), toArray(revisionData));
+      objectUnderTest.apply(project, FetchRefSpec.fromRef(patchSetRefName), toArray(revisionData));
       objectUnderTest.apply(project, refSpec, toArray(revisionData));
     }
 
@@ -114,7 +114,7 @@
 
     Optional<RevisionData> revisionData = reader.read(allProjects, seqChangesRef, 0);
 
-    RefSpec refSpec = new RefSpec(seqChangesRef);
+    FetchRefSpec refSpec = FetchRefSpec.fromRef(seqChangesRef);
     objectUnderTest.apply(project, refSpec, toArray(revisionData));
     try (Repository repo = repoManager.openRepository(project);
         TestRepository<Repository> testRepo = new TestRepository<>(repo); ) {
@@ -135,13 +135,13 @@
     Change.Id changeId = pushResult.getChange().getId();
     String patchSetRefname = RefNames.patchSetRef(PatchSet.id(changeId, 1));
     String refName = RefNames.changeMetaRef(changeId);
-    RefSpec refSpec = new RefSpec(refName);
+    FetchRefSpec refSpec = FetchRefSpec.fromRef(refName);
 
     NameKey testRepoKey = Project.nameKey(testRepoProjectName);
     try (Repository repo = repoManager.openRepository(testRepoKey)) {
       Optional<RevisionData> revisionData =
           reader.read(testRepoKey, repo.exactRef(refName).getObjectId(), refName, 0);
-      objectUnderTest.apply(project, new RefSpec(patchSetRefname), toArray(revisionData));
+      objectUnderTest.apply(project, FetchRefSpec.fromRef(patchSetRefname), toArray(revisionData));
       objectUnderTest.apply(project, refSpec, toArray(revisionData));
     }
 
@@ -185,7 +185,7 @@
       Optional<RevisionData> revisionData =
           reader.read(createTestProject, repo.exactRef(refName).getObjectId(), refName, 0);
 
-      RefSpec refSpec = new RefSpec(refName);
+      FetchRefSpec refSpec = FetchRefSpec.fromRef(refName);
       assertThrows(
           MissingParentObjectException.class,
           () -> objectUnderTest.apply(project, refSpec, toArray(revisionData)));
@@ -206,7 +206,7 @@
       Optional<RevisionData> revisionData =
           reader.read(createTestProject, repo.exactRef(refName).getObjectId(), refName, 0);
 
-      RefSpec refSpec = new RefSpec(refName);
+      FetchRefSpec refSpec = FetchRefSpec.fromRef(refName);
       assertThrows(
           MissingLatestPatchSetException.class,
           () -> objectUnderTest.apply(project, refSpec, toArray(revisionData)));