Initial libraty creation

The problem is with DynamicItem - it doesn't get updated with the Gerrit
cache reference. It seems that it doesn't work across db and sys injector.

Change-Id: I05cf4766e4b74b380c9fbf7c6e25553dfa76d5d3
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..f8f49ad
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,44 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+load(
+    "//tools/bzl:plugin.bzl",
+    "PLUGIN_DEPS",
+    "PLUGIN_DEPS_NEVERLINK",
+    "PLUGIN_TEST_DEPS",
+    "gerrit_plugin",
+)
+
+gerrit_plugin(
+    name = "gerrit-cached-refdb",
+    srcs = glob(["src/main/java/**/*.java"]),
+    manifest_entries = [
+        "Gerrit-PluginName: gerrit-cached-refdb",
+        "Implementation-Title: gerrit-cached-refdb plugin",
+        "Implementation-URL: TODO",
+    ],
+    resources = glob(["src/main/resources/**/*"]),
+    deps = [],
+)
+
+junit_tests(
+    name = "gerrit-cached-refdb_tests",
+    srcs = glob(["src/test/java/**/*Test.java"]),
+    resources = glob(["src/test/resources/**/*"]),
+    tags = [
+        "local",
+        "gerrit-cached-refdb",
+    ],
+    deps = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+        ":gerrit-cached-refdb__plugin",
+    ],
+)
+
+acceptance_tests(
+    srcs = glob(["src/test/java/**/*IT.java"]),
+    group = "jgit_cache",
+    labels = ["server"],
+    vm_args = ["-Xmx2G"],
+    deps = [
+        ":gerrit-cached-refdb__plugin",
+    ],
+)
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/BatchRefUpdateWithCacheUpdate.java b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/BatchRefUpdateWithCacheUpdate.java
new file mode 100644
index 0000000..360dfa1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/BatchRefUpdateWithCacheUpdate.java
@@ -0,0 +1,184 @@
+// Copyright (C) 2021 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.gerritcachedrefdb;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.util.time.ProposedTimestamp;
+
+class BatchRefUpdateWithCacheUpdate extends BatchRefUpdate {
+  interface Factory {
+    BatchRefUpdateWithCacheUpdate create(Repository repo, BatchRefUpdate delegate);
+  }
+
+  private final Repository repo;
+  private final RefByNameCacheWrapper refsCache;
+  private final BatchRefUpdate delegate;
+
+  @Inject
+  BatchRefUpdateWithCacheUpdate(
+      RefByNameCacheWrapper refsCache,
+      @Assisted Repository repo,
+      @Assisted BatchRefUpdate delegate) {
+    super(repo.getRefDatabase());
+    this.refsCache = refsCache;
+    this.repo = repo;
+    this.delegate = delegate;
+  }
+
+  @Override
+  public boolean isAllowNonFastForwards() {
+    return delegate.isAllowNonFastForwards();
+  }
+
+  @Override
+  public BatchRefUpdate setAllowNonFastForwards(boolean allow) {
+    delegate.setAllowNonFastForwards(allow);
+    return this;
+  }
+
+  @Override
+  public PersonIdent getRefLogIdent() {
+    return delegate.getRefLogIdent();
+  }
+
+  @Override
+  public BatchRefUpdate setRefLogIdent(PersonIdent pi) {
+    delegate.setRefLogIdent(pi);
+    return this;
+  }
+
+  @Override
+  public String getRefLogMessage() {
+    return delegate.getRefLogMessage();
+  }
+
+  @Override
+  public boolean isRefLogIncludingResult() {
+    return delegate.isRefLogIncludingResult();
+  }
+
+  @Override
+  public BatchRefUpdate setRefLogMessage(String msg, boolean appendStatus) {
+    delegate.setRefLogMessage(msg, appendStatus);
+    return this;
+  }
+
+  @Override
+  public BatchRefUpdate disableRefLog() {
+    delegate.disableRefLog();
+    return this;
+  }
+
+  @Override
+  public BatchRefUpdate setForceRefLog(boolean force) {
+    delegate.setForceRefLog(force);
+    return this;
+  }
+
+  @Override
+  public boolean isRefLogDisabled() {
+    return delegate.isRefLogDisabled();
+  }
+
+  @Override
+  public BatchRefUpdate setAtomic(boolean atomic) {
+    delegate.setAtomic(atomic);
+    return this;
+  }
+
+  @Override
+  public boolean isAtomic() {
+    return delegate.isAtomic();
+  }
+
+  @Override
+  public void setPushCertificate(PushCertificate cert) {
+    delegate.setPushCertificate(cert);
+  }
+
+  @Override
+  public List<ReceiveCommand> getCommands() {
+    return delegate.getCommands();
+  }
+
+  @Override
+  public BatchRefUpdate addCommand(ReceiveCommand cmd) {
+    delegate.addCommand(cmd);
+    return this;
+  }
+
+  @Override
+  public BatchRefUpdate addCommand(ReceiveCommand... cmd) {
+    delegate.addCommand(cmd);
+    return this;
+  }
+
+  @Override
+  public BatchRefUpdate addCommand(Collection<ReceiveCommand> cmd) {
+    delegate.addCommand(cmd);
+    return this;
+  }
+
+  @Override
+  public List<String> getPushOptions() {
+    return delegate.getPushOptions();
+  }
+
+  @Override
+  public List<ProposedTimestamp> getProposedTimestamps() {
+    return delegate.getProposedTimestamps();
+  }
+
+  @Override
+  public BatchRefUpdate addProposedTimestamp(ProposedTimestamp ts) {
+    delegate.addProposedTimestamp(ts);
+    return this;
+  }
+
+  @Override
+  public void execute(RevWalk walk, ProgressMonitor monitor, List<String> options)
+      throws IOException {
+    delegate.execute(walk, monitor, options);
+    evictCache();
+  }
+
+  @Override
+  public void execute(RevWalk walk, ProgressMonitor monitor) throws IOException {
+    delegate.execute(walk, monitor);
+    evictCache();
+  }
+
+  private void evictCache() {
+    delegate
+        .getCommands()
+        .forEach(
+            cmd -> {
+              if (cmd.getResult() == ReceiveCommand.Result.OK) {
+                refsCache.evict(repo.getIdentifier(), cmd.getRefName());
+              }
+            });
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/CachedRefDatabase.java b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/CachedRefDatabase.java
new file mode 100644
index 0000000..79a619c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/CachedRefDatabase.java
@@ -0,0 +1,174 @@
+// Copyright (C) 2021 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.gerritcachedrefdb;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+
+class CachedRefDatabase extends RefDatabase {
+  interface Factory {
+    CachedRefDatabase create(CachedRefRepository repo);
+  }
+
+  private final RefByNameCacheWrapper refsCache;
+  private final BatchRefUpdateWithCacheUpdate.Factory batchUpdateFactory;
+  private final RefUpdateWithCacheUpdate.Factory updateFactory;
+  private final RefRenameWithCacheUpdate.Factory renameFactory;
+  private final RefDatabase delegate;
+  private final CachedRefRepository repo;
+
+  @Inject
+  CachedRefDatabase(
+      RefByNameCacheWrapper refsCache,
+      BatchRefUpdateWithCacheUpdate.Factory batchUpdateFactory,
+      RefUpdateWithCacheUpdate.Factory updateFactory,
+      RefRenameWithCacheUpdate.Factory renameFactory,
+      @Assisted CachedRefRepository repo) {
+    this.refsCache = refsCache;
+    this.batchUpdateFactory = batchUpdateFactory;
+    this.updateFactory = updateFactory;
+    this.renameFactory = renameFactory;
+    this.delegate = repo.getDelegate().getRefDatabase();
+    this.repo = repo;
+  }
+
+  @Override
+  public void create() throws IOException {
+    delegate.create();
+  }
+
+  @Override
+  public void close() {
+    delegate.close();
+  }
+
+  @Override
+  public boolean isNameConflicting(String name) throws IOException {
+    return delegate.isNameConflicting(name);
+  }
+
+  @Override
+  public RefUpdate newUpdate(String name, boolean detach) throws IOException {
+    return updateFactory.create(this, repo, delegate.newUpdate(name, detach));
+  }
+
+  @Override
+  public RefRename newRename(String fromName, String toName) throws IOException {
+    return renameFactory.create(
+        repo,
+        delegate.newRename(fromName, toName),
+        newUpdate(fromName, false),
+        newUpdate(toName, false));
+  }
+
+  @Override
+  public Ref exactRef(String name) throws IOException {
+    return refsCache.computeIfAbsent(
+        repo.getIdentifier(), name, () -> Optional.ofNullable(delegate.exactRef(name)));
+  }
+
+  @Deprecated
+  @Override
+  public Map<String, Ref> getRefs(String prefix) throws IOException {
+    return delegate.getRefs(prefix);
+  }
+
+  @Override
+  public List<Ref> getAdditionalRefs() throws IOException {
+    return delegate.getAdditionalRefs();
+  }
+
+  @Override
+  public Ref peel(Ref ref) throws IOException {
+    return delegate.peel(ref);
+  }
+
+  @Override
+  public boolean hasVersioning() {
+    return delegate.hasVersioning();
+  }
+
+  @Override
+  public Collection<String> getConflictingNames(String name) throws IOException {
+    return delegate.getConflictingNames(name);
+  }
+
+  @Override
+  public BatchRefUpdate newBatchUpdate() {
+    return batchUpdateFactory.create(repo, delegate.newBatchUpdate());
+  }
+
+  @Override
+  public boolean performsAtomicTransactions() {
+    return delegate.performsAtomicTransactions();
+  }
+
+  @Override
+  public Map<String, Ref> exactRef(String... refs) throws IOException {
+    return delegate.exactRef(refs);
+  }
+
+  @Override
+  public Ref firstExactRef(String... refs) throws IOException {
+    return delegate.firstExactRef(refs);
+  }
+
+  @Override
+  public List<Ref> getRefs() throws IOException {
+    return delegate.getRefs();
+  }
+
+  @Override
+  public List<Ref> getRefsByPrefix(String prefix) throws IOException {
+    return delegate.getRefsByPrefix(prefix);
+  }
+
+  @Override
+  public List<Ref> getRefsByPrefix(String... prefixes) throws IOException {
+    return delegate.getRefsByPrefix(prefixes);
+  }
+
+  @Override
+  public Set<Ref> getTipsWithSha1(ObjectId id) throws IOException {
+    return delegate.getTipsWithSha1(id);
+  }
+
+  @Override
+  public boolean hasFastTipsWithSha1() throws IOException {
+    return delegate.hasFastTipsWithSha1();
+  }
+
+  @Override
+  public boolean hasRefs() throws IOException {
+    return delegate.hasRefs();
+  }
+
+  @Override
+  public void refresh() {
+    delegate.refresh();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/CachedRefRepository.java b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/CachedRefRepository.java
new file mode 100644
index 0000000..e7960e3
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/CachedRefRepository.java
@@ -0,0 +1,350 @@
+// Copyright (C) 2021 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.gerritcachedrefdb;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.errors.AmbiguousObjectException;
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.NoWorkTreeException;
+import org.eclipse.jgit.errors.RevisionSyntaxException;
+import org.eclipse.jgit.events.ListenerList;
+import org.eclipse.jgit.events.RepositoryEvent;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.RebaseTodoLine;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryState;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.FS;
+
+class CachedRefRepository extends DelegateRepository {
+  interface Factory {
+    CachedRefRepository create(Repository repo);
+  }
+
+  private final CachedRefDatabase refDb;
+  private final RefUpdateWithCacheUpdate.Factory updateFactory;
+  private final RefRenameWithCacheUpdate.Factory renameFactory;
+
+  @Inject
+  CachedRefRepository(
+      CachedRefDatabase.Factory refDbFactory,
+      RefUpdateWithCacheUpdate.Factory updateFactory,
+      RefRenameWithCacheUpdate.Factory renameFactory,
+      @Assisted Repository repo) {
+    super(repo);
+    this.updateFactory = updateFactory;
+    this.renameFactory = renameFactory;
+    this.refDb = refDbFactory.create(this);
+  }
+
+  @Override
+  public RefDatabase getRefDatabase() {
+    return refDb;
+  }
+
+  @Override
+  public ListenerList getListenerList() {
+    return getDelegate().getListenerList();
+  }
+
+  @Override
+  public void fireEvent(RepositoryEvent<?> event) {
+    getDelegate().fireEvent(event);
+  }
+
+  @Override
+  public File getDirectory() {
+    return getDelegate().getDirectory();
+  }
+
+  @Override
+  public ObjectInserter newObjectInserter() {
+    return getDelegate().newObjectInserter();
+  }
+
+  @Override
+  public ObjectReader newObjectReader() {
+    return getDelegate().newObjectReader();
+  }
+
+  @Override
+  public FS getFS() {
+    return getDelegate().getFS();
+  }
+
+  @Deprecated
+  @Override
+  public boolean hasObject(AnyObjectId objectId) {
+    return getDelegate().hasObject(objectId);
+  }
+
+  @Override
+  public ObjectLoader open(AnyObjectId objectId) throws MissingObjectException, IOException {
+    return getDelegate().open(objectId);
+  }
+
+  @Override
+  public ObjectLoader open(AnyObjectId objectId, int typeHint)
+      throws MissingObjectException, IncorrectObjectTypeException, IOException {
+    return getDelegate().open(objectId, typeHint);
+  }
+
+  @Override
+  public RefUpdate updateRef(String ref) throws IOException {
+    return updateFactory.create(refDb, this, getDelegate().updateRef(ref));
+  }
+
+  @Override
+  public RefUpdate updateRef(String ref, boolean detach) throws IOException {
+    return updateFactory.create(refDb, this, getDelegate().updateRef(ref, detach));
+  }
+
+  @Override
+  public RefRename renameRef(String fromRef, String toRef) throws IOException {
+    return renameFactory.create(
+        this, getDelegate().renameRef(fromRef, toRef), updateRef(fromRef), updateRef(toRef));
+  }
+
+  @Override
+  public ObjectId resolve(String revstr)
+      throws AmbiguousObjectException, IncorrectObjectTypeException, RevisionSyntaxException,
+          IOException {
+    return getDelegate().resolve(revstr);
+  }
+
+  @Override
+  public String simplify(String revstr) throws AmbiguousObjectException, IOException {
+    return getDelegate().simplify(revstr);
+  }
+
+  @Override
+  public void incrementOpen() {
+    getDelegate().incrementOpen();
+  }
+
+  @Override
+  public void close() {
+    getDelegate().close();
+  }
+
+  @Override
+  public String getFullBranch() throws IOException {
+    return getDelegate().getFullBranch();
+  }
+
+  @Override
+  public String getBranch() throws IOException {
+    return getDelegate().getBranch();
+  }
+
+  @Override
+  public Set<ObjectId> getAdditionalHaves() {
+    return getDelegate().getAdditionalHaves();
+  }
+
+  @Deprecated
+  @Override
+  public Map<String, Ref> getAllRefs() {
+    return getDelegate().getAllRefs();
+  }
+
+  @Deprecated
+  @Override
+  public Map<String, Ref> getTags() {
+    return getDelegate().getTags();
+  }
+
+  @Deprecated
+  @Override
+  public Ref peel(Ref ref) {
+    return getDelegate().peel(ref);
+  }
+
+  @Override
+  public Map<AnyObjectId, Set<Ref>> getAllRefsByPeeledObjectId() {
+    return getDelegate().getAllRefsByPeeledObjectId();
+  }
+
+  @Override
+  public File getIndexFile() throws NoWorkTreeException {
+    return getDelegate().getIndexFile();
+  }
+
+  @Override
+  public RevCommit parseCommit(AnyObjectId id)
+      throws IncorrectObjectTypeException, IOException, MissingObjectException {
+    return getDelegate().parseCommit(id);
+  }
+
+  @Override
+  public DirCache readDirCache() throws NoWorkTreeException, CorruptObjectException, IOException {
+    return getDelegate().readDirCache();
+  }
+
+  @Override
+  public DirCache lockDirCache() throws NoWorkTreeException, CorruptObjectException, IOException {
+    return getDelegate().lockDirCache();
+  }
+
+  @Override
+  public RepositoryState getRepositoryState() {
+    return getDelegate().getRepositoryState();
+  }
+
+  @Override
+  public boolean isBare() {
+    return getDelegate().isBare();
+  }
+
+  @Override
+  public File getWorkTree() throws NoWorkTreeException {
+    return getDelegate().getWorkTree();
+  }
+
+  @Override
+  public String shortenRemoteBranchName(String refName) {
+    return getDelegate().shortenRemoteBranchName(refName);
+  }
+
+  @Override
+  public String getRemoteName(String refName) {
+    return getDelegate().getRemoteName(refName);
+  }
+
+  @Override
+  public String getGitwebDescription() throws IOException {
+    return getDelegate().getGitwebDescription();
+  }
+
+  @Override
+  public void setGitwebDescription(String description) throws IOException {
+    getDelegate().setGitwebDescription(description);
+  }
+
+  @Override
+  public String readMergeCommitMsg() throws IOException, NoWorkTreeException {
+    return getDelegate().readMergeCommitMsg();
+  }
+
+  @Override
+  public void writeMergeCommitMsg(String msg) throws IOException {
+    getDelegate().writeMergeCommitMsg(msg);
+  }
+
+  @Override
+  public String readCommitEditMsg() throws IOException, NoWorkTreeException {
+    return getDelegate().readCommitEditMsg();
+  }
+
+  @Override
+  public void writeCommitEditMsg(String msg) throws IOException {
+    getDelegate().writeCommitEditMsg(msg);
+  }
+
+  @Override
+  public List<ObjectId> readMergeHeads() throws IOException, NoWorkTreeException {
+    return getDelegate().readMergeHeads();
+  }
+
+  @Override
+  public void writeMergeHeads(List<? extends ObjectId> heads) throws IOException {
+    getDelegate().writeMergeHeads(heads);
+  }
+
+  @Override
+  public ObjectId readCherryPickHead() throws IOException, NoWorkTreeException {
+    return getDelegate().readCherryPickHead();
+  }
+
+  @Override
+  public ObjectId readRevertHead() throws IOException, NoWorkTreeException {
+    return getDelegate().readRevertHead();
+  }
+
+  @Override
+  public void writeCherryPickHead(ObjectId head) throws IOException {
+    getDelegate().writeCherryPickHead(head);
+  }
+
+  @Override
+  public void writeRevertHead(ObjectId head) throws IOException {
+    getDelegate().writeRevertHead(head);
+  }
+
+  @Override
+  public void writeOrigHead(ObjectId head) throws IOException {
+    getDelegate().writeOrigHead(head);
+  }
+
+  @Override
+  public ObjectId readOrigHead() throws IOException, NoWorkTreeException {
+    return getDelegate().readOrigHead();
+  }
+
+  @Override
+  public String readSquashCommitMsg() throws IOException {
+    return getDelegate().readSquashCommitMsg();
+  }
+
+  @Override
+  public void writeSquashCommitMsg(String msg) throws IOException {
+    getDelegate().writeSquashCommitMsg(msg);
+  }
+
+  @Override
+  public List<RebaseTodoLine> readRebaseTodo(String path, boolean includeComments)
+      throws IOException {
+    return getDelegate().readRebaseTodo(path, includeComments);
+  }
+
+  @Override
+  public void writeRebaseTodoFile(String path, List<RebaseTodoLine> steps, boolean append)
+      throws IOException {
+    getDelegate().writeRebaseTodoFile(path, steps, append);
+  }
+
+  @Override
+  public Set<String> getRemoteNames() {
+    return getDelegate().getRemoteNames();
+  }
+
+  @Override
+  public void autoGC(ProgressMonitor monitor) {
+    getDelegate().autoGC(monitor);
+  }
+
+  @Override
+  public void create() throws IOException {
+    getDelegate().create();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/CachedRefsModule.java b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/CachedRefsModule.java
new file mode 100644
index 0000000..9d6eb2d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/CachedRefsModule.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2021 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.gerritcachedrefdb;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+
+public class CachedRefsModule extends FactoryModule {
+  @Override
+  protected void configure() {
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/DelegateRepository.java b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/DelegateRepository.java
new file mode 100644
index 0000000..1323398
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/DelegateRepository.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2021 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.gerritcachedrefdb;
+
+import java.io.IOException;
+import org.eclipse.jgit.attributes.AttributesNodeProvider;
+import org.eclipse.jgit.lib.BaseRepositoryBuilder;
+import org.eclipse.jgit.lib.ObjectDatabase;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.ReflogReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+
+/**
+ * For the time being copied from com.google.gerrit.server.git.DelegateRepository as it is package
+ * protected in 'stable-3.2'. It can be dropped when change gets merged up to.
+ */
+class DelegateRepository extends Repository {
+
+  private final Repository delegate;
+
+  DelegateRepository(Repository delegate) {
+    super(toBuilder(delegate));
+    this.delegate = delegate;
+  }
+
+  @Override
+  public void create(boolean bare) throws IOException {
+    delegate.create(bare);
+  }
+
+  @Override
+  public String getIdentifier() {
+    return delegate.getIdentifier();
+  }
+
+  @Override
+  public ObjectDatabase getObjectDatabase() {
+    return delegate.getObjectDatabase();
+  }
+
+  @Override
+  public RefDatabase getRefDatabase() {
+    return delegate.getRefDatabase();
+  }
+
+  @Override
+  public StoredConfig getConfig() {
+    return delegate.getConfig();
+  }
+
+  @Override
+  public AttributesNodeProvider createAttributesNodeProvider() {
+    return delegate.createAttributesNodeProvider();
+  }
+
+  @Override
+  public void scanForRepoChanges() throws IOException {
+    delegate.scanForRepoChanges();
+  }
+
+  @Override
+  public void notifyIndexChanged(boolean internal) {
+    delegate.notifyIndexChanged(internal);
+  }
+
+  @Override
+  public ReflogReader getReflogReader(String refName) throws IOException {
+    return delegate.getReflogReader(refName);
+  }
+
+  public Repository getDelegate() {
+    return delegate;
+  }
+
+  @SuppressWarnings("rawtypes")
+  private static BaseRepositoryBuilder toBuilder(Repository repo) {
+    if (!repo.isBare()) {
+      throw new IllegalArgumentException(
+          "non-bare repository is not supported: " + repo.getIdentifier());
+    }
+
+    return new BaseRepositoryBuilder<>().setFS(repo.getFS()).setGitDir(repo.getDirectory());
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/GerritCachedGitRepositoryManager.java b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/GerritCachedGitRepositoryManager.java
new file mode 100644
index 0000000..4eb0cf1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/GerritCachedGitRepositoryManager.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2021 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.gerritcachedrefdb;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.SortedSet;
+import org.eclipse.jgit.lib.Repository;
+
+@Singleton
+class GerritCachedGitRepositoryManager implements GitRepositoryManager {
+  private final LocalDiskRepositoryManager repoManager;
+  private final CachedRefRepository.Factory repoWrapperFactory;
+
+  @Inject
+  GerritCachedGitRepositoryManager(
+      LocalDiskRepositoryManager repoManager, CachedRefRepository.Factory repoWrapperFactory) {
+    this.repoManager = repoManager;
+    this.repoWrapperFactory = repoWrapperFactory;
+  }
+
+  @Override
+  public Repository openRepository(Project.NameKey name) throws IOException {
+    return repoWrapperFactory.create(repoManager.openRepository(name));
+  }
+
+  @Override
+  public Repository createRepository(Project.NameKey name) throws IOException {
+    return repoWrapperFactory.create(repoManager.createRepository(name));
+  }
+
+  @Override
+  public SortedSet<Project.NameKey> list() {
+    return repoManager.list();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/LibDbModule.java b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/LibDbModule.java
new file mode 100644
index 0000000..5787abe
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/LibDbModule.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2021 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.gerritcachedrefdb;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.server.git.GitRepositoryManager;
+
+public class LibDbModule extends FactoryModule {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  @Override
+  protected void configure() {
+    DynamicItem.itemOf(binder(), RefByNameCache.class);
+
+    factory(RefUpdateWithCacheUpdate.Factory.class);
+    factory(RefRenameWithCacheUpdate.Factory.class);
+    factory(BatchRefUpdateWithCacheUpdate.Factory.class);
+    factory(CachedRefDatabase.Factory.class);
+    factory(CachedRefRepository.Factory.class);
+
+    bind(GitRepositoryManager.class).to(GerritCachedGitRepositoryManager.class);
+    logger.atInfo().log("DB library loaded");
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/LibSysModule.java b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/LibSysModule.java
new file mode 100644
index 0000000..e6dab8f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/LibSysModule.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2021 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.gerritcachedrefdb;
+
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.inject.AbstractModule;
+
+public class LibSysModule extends AbstractModule {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  @Override
+  protected void configure() {
+    install(RefByNameGerritCache.module());
+    DynamicItem.bind(binder(), RefByNameCache.class).to(RefByNameGerritCache.class).in(SINGLETON);
+    logger.atInfo().log("Sys library loaded");
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/NoOpRefByNameCache.java b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/NoOpRefByNameCache.java
new file mode 100644
index 0000000..44b6abb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/NoOpRefByNameCache.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2021 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.gerritcachedrefdb;
+
+import com.google.common.flogger.FluentLogger;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import org.eclipse.jgit.lib.Ref;
+
+class NoOpRefByNameCache implements RefByNameCache {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  @Override
+  public Ref computeIfAbsent(
+      String identifier, String ref, Callable<? extends Optional<Ref>> loader) {
+    try {
+      return loader.call().orElse(null);
+    } catch (Exception e) {
+      logger.atSevere().withCause(e).log(
+          "Repository '%s', getting ref '%s' failed", identifier, ref);
+    }
+    return null;
+  }
+
+  @Override
+  public void evict(String identifier, String ref) {
+    // do nothing as there is no cache to be evicted
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefByNameCache.java b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefByNameCache.java
new file mode 100644
index 0000000..69fabfc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefByNameCache.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2021 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.gerritcachedrefdb;
+
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import org.eclipse.jgit.lib.Ref;
+
+interface RefByNameCache {
+  Ref computeIfAbsent(String identifier, String ref, Callable<? extends Optional<Ref>> loader);
+
+  void evict(String identifier, String ref);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefByNameCacheWrapper.java b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefByNameCacheWrapper.java
new file mode 100644
index 0000000..c9744ad
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefByNameCacheWrapper.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2021 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.gerritcachedrefdb;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.inject.Inject;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import org.eclipse.jgit.lib.Ref;
+
+class RefByNameCacheWrapper implements RefByNameCache {
+  private static final RefByNameCache NOOP_CACHE = new NoOpRefByNameCache();
+
+  @Inject(optional = true)
+  private DynamicItem<RefByNameCache> refByNameCache;
+
+  @Override
+  public Ref computeIfAbsent(
+      String identifier, String ref, Callable<? extends Optional<Ref>> loader) {
+    return cache().computeIfAbsent(identifier, ref, loader);
+  }
+
+  @Override
+  public void evict(String identifier, String ref) {
+    cache().evict(identifier, ref);
+  }
+
+  @VisibleForTesting
+  RefByNameCache cache() {
+    return Optional.ofNullable(refByNameCache).map(DynamicItem::get).orElse(NOOP_CACHE);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefByNameGerritCache.java b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefByNameGerritCache.java
new file mode 100644
index 0000000..e9fc2b7
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefByNameGerritCache.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2021 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.gerritcachedrefdb;
+
+import com.google.common.cache.Cache;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import org.eclipse.jgit.lib.Ref;
+
+@Singleton
+class RefByNameGerritCache implements RefByNameCache {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final String REF_BY_NAME = "ref_by_name";
+
+  static com.google.inject.Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        cache(REF_BY_NAME, String.class, new TypeLiteral<Optional<Ref>>() {});
+      }
+    };
+  }
+
+  private final Cache<String, Optional<Ref>> refByName;
+
+  @Inject
+  RefByNameGerritCache(@Named(REF_BY_NAME) Cache<String, Optional<Ref>> refByName) {
+    this.refByName = refByName;
+  }
+
+  @Override
+  public Ref computeIfAbsent(
+      String identifier, String ref, Callable<? extends Optional<Ref>> loader) {
+    String uniqueRefName = getUniqueName(identifier, ref);
+    try {
+      return refByName.get(uniqueRefName, loader).orElse(null);
+    } catch (ExecutionException e) {
+      logger.atWarning().withCause(e).log("Getting ref for [%s] failed.", uniqueRefName);
+      return null;
+    }
+  }
+
+  @Override
+  public void evict(String identifier, String ref) {
+    refByName.invalidate(getUniqueName(identifier, ref));
+  }
+
+  private static String getUniqueName(String identifier, String ref) {
+    return String.format("%s$%s", identifier, ref);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefRenameWithCacheUpdate.java b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefRenameWithCacheUpdate.java
new file mode 100644
index 0000000..68de4ca
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefRenameWithCacheUpdate.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2021 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.gerritcachedrefdb;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.EnumSet;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+
+class RefRenameWithCacheUpdate extends RefRename {
+  interface Factory {
+    RefRenameWithCacheUpdate create(
+        Repository repo,
+        RefRename delegate,
+        @Assisted("src") RefUpdate src,
+        @Assisted("dst") RefUpdate dst);
+  }
+
+  private static final String NOT_SUPPORTED_MSG = "Should never be called";
+  private static final EnumSet<Result> SUCCESSFUL_RENAMES =
+      EnumSet.of(Result.NEW, Result.FORCED, Result.FAST_FORWARD, Result.RENAMED);
+
+  private final RefByNameCacheWrapper refsCache;
+  private final Repository repo;
+  private final RefRename delegate;
+  private final RefUpdate src;
+
+  @Inject
+  RefRenameWithCacheUpdate(
+      RefByNameCacheWrapper refsCache,
+      @Assisted Repository repo,
+      @Assisted RefRename delegate,
+      @Assisted("src") RefUpdate src,
+      @Assisted("dst") RefUpdate dst) {
+    super(src, dst);
+    this.refsCache = refsCache;
+    this.repo = repo;
+    this.delegate = delegate;
+    this.src = src;
+  }
+
+  @Override
+  public PersonIdent getRefLogIdent() {
+    return delegate.getRefLogIdent();
+  }
+
+  @Override
+  public void setRefLogIdent(PersonIdent pi) {
+    delegate.setRefLogIdent(pi);
+  }
+
+  @Override
+  public String getRefLogMessage() {
+    return delegate.getRefLogMessage();
+  }
+
+  @Override
+  public void setRefLogMessage(String msg) {
+    // Method is called from the super constructor and delegate is not available yet therefore
+    // 'null' has to be checked. It doesn't matter as wrapped instance is properly constructed and
+    // its ref log message is used.
+    if (delegate != null) {
+      delegate.setRefLogMessage(msg);
+    }
+  }
+
+  @Override
+  public void disableRefLog() {
+    delegate.disableRefLog();
+  }
+
+  @Override
+  public Result getResult() {
+    return delegate.getResult();
+  }
+
+  @Override
+  public Result rename() throws IOException {
+    Result r = delegate.rename();
+    if (SUCCESSFUL_RENAMES.contains(r)) {
+      refsCache.evict(repo.getIdentifier(), src.getName());
+    }
+    return r;
+  }
+
+  @Override
+  protected Result doRename() throws IOException {
+    throw new UnsupportedOperationException(NOT_SUPPORTED_MSG);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefUpdateWithCacheUpdate.java b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefUpdateWithCacheUpdate.java
new file mode 100644
index 0000000..a4bee39
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/RefUpdateWithCacheUpdate.java
@@ -0,0 +1,215 @@
+package com.googlesource.gerrit.plugins.gerritcachedrefdb;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.EnumSet;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
+
+class RefUpdateWithCacheUpdate extends RefUpdate {
+  interface Factory {
+    RefUpdateWithCacheUpdate create(RefDatabase refDb, Repository repo, RefUpdate delegate);
+  }
+
+  private static final String NOT_SUPPORTED_MSG = "Should never be called";
+  private static final EnumSet<Result> SUCCESSFUL_UPDATES =
+      EnumSet.of(Result.NEW, Result.FORCED, Result.FAST_FORWARD, Result.RENAMED);
+
+  private final RefByNameCacheWrapper refsCache;
+  private final RefDatabase refDb;
+  private final Repository repo;
+  private final RefUpdate delegate;
+
+  @Inject
+  RefUpdateWithCacheUpdate(
+      RefByNameCacheWrapper refsCache,
+      @Assisted RefDatabase refDb,
+      @Assisted Repository repo,
+      @Assisted RefUpdate delegate) {
+    super(delegate.getRef());
+    this.refsCache = refsCache;
+    this.refDb = refDb;
+    this.repo = repo;
+    this.delegate = delegate;
+  }
+
+  @Override
+  public String getName() {
+    return delegate.getName();
+  }
+
+  @Override
+  public Ref getRef() {
+    return delegate.getRef();
+  }
+
+  @Override
+  public ObjectId getNewObjectId() {
+    return delegate.getNewObjectId();
+  }
+
+  @Override
+  public void setDetachingSymbolicRef() {
+    delegate.setDetachingSymbolicRef();
+  }
+
+  @Override
+  public boolean isDetachingSymbolicRef() {
+    return delegate.isDetachingSymbolicRef();
+  }
+
+  @Override
+  public void setNewObjectId(AnyObjectId id) {
+    delegate.setNewObjectId(id);
+  }
+
+  @Override
+  public ObjectId getExpectedOldObjectId() {
+    return delegate.getExpectedOldObjectId();
+  }
+
+  @Override
+  public void setExpectedOldObjectId(AnyObjectId id) {
+    delegate.setExpectedOldObjectId(id);
+  }
+
+  @Override
+  public boolean isForceUpdate() {
+    return delegate.isForceUpdate();
+  }
+
+  @Override
+  public void setForceUpdate(boolean b) {
+    delegate.setForceUpdate(b);
+  }
+
+  @Override
+  public PersonIdent getRefLogIdent() {
+    return delegate.getRefLogIdent();
+  }
+
+  @Override
+  public void setRefLogIdent(PersonIdent pi) {
+    delegate.setRefLogIdent(pi);
+  }
+
+  @Override
+  public String getRefLogMessage() {
+    return delegate.getRefLogMessage();
+  }
+
+  @Override
+  public void setRefLogMessage(String msg, boolean appendStatus) {
+    delegate.setRefLogMessage(msg, appendStatus);
+  }
+
+  @Override
+  public void disableRefLog() {
+    delegate.disableRefLog();
+  }
+
+  @Override
+  public void setForceRefLog(boolean force) {
+    delegate.setForceRefLog(force);
+  }
+
+  @Override
+  public ObjectId getOldObjectId() {
+    return delegate.getOldObjectId();
+  }
+
+  @Override
+  public void setPushCertificate(PushCertificate cert) {
+    delegate.setPushCertificate(cert);
+  }
+
+  @Override
+  public Result getResult() {
+    return delegate.getResult();
+  }
+
+  @Override
+  public Result forceUpdate() throws IOException {
+    return evictCache(delegate.forceUpdate());
+  }
+
+  @Override
+  public Result update() throws IOException {
+    return evictCache(delegate.update());
+  }
+
+  @Override
+  public Result update(RevWalk walk) throws IOException {
+    return evictCache(delegate.update(walk));
+  }
+
+  @Override
+  public Result delete() throws IOException {
+    return evictCache(delegate.delete());
+  }
+
+  @Override
+  public Result delete(RevWalk walk) throws IOException {
+    return evictCache(delegate.delete(walk));
+  }
+
+  @Override
+  public Result link(String target) throws IOException {
+    return evictCache(delegate.link(target));
+  }
+
+  @Override
+  public void setCheckConflicting(boolean check) {
+    delegate.setCheckConflicting(check);
+  }
+
+  @Override
+  protected RefDatabase getRefDatabase() {
+    return refDb;
+  }
+
+  @Override
+  protected Repository getRepository() {
+    return repo;
+  }
+
+  @Override
+  protected boolean tryLock(boolean deref) throws IOException {
+    throw new UnsupportedOperationException(NOT_SUPPORTED_MSG);
+  }
+
+  @Override
+  protected void unlock() {
+    throw new UnsupportedOperationException(NOT_SUPPORTED_MSG);
+  }
+
+  @Override
+  protected Result doUpdate(Result desiredResult) throws IOException {
+    throw new UnsupportedOperationException(NOT_SUPPORTED_MSG);
+  }
+
+  @Override
+  protected Result doDelete(Result desiredResult) throws IOException {
+    throw new UnsupportedOperationException(NOT_SUPPORTED_MSG);
+  }
+
+  @Override
+  protected Result doLink(String target) throws IOException {
+    throw new UnsupportedOperationException(NOT_SUPPORTED_MSG);
+  }
+
+  private Result evictCache(Result r) {
+    if (SUCCESSFUL_UPDATES.contains(r)) {
+      refsCache.evict(repo.getIdentifier(), getName());
+    }
+    return r;
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/GerritCachedRefDbIT.java b/src/test/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/GerritCachedRefDbIT.java
new file mode 100644
index 0000000..bf2ca31
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/gerritcachedrefdb/GerritCachedRefDbIT.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2021 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.gerritcachedrefdb;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.UseLocalDisk;
+import com.google.gerrit.acceptance.config.GerritConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+@UseLocalDisk
+@NoHttpd
+public class GerritCachedRefDbIT extends AbstractDaemonTest {
+  @Inject private GitRepositoryManager gitRepoManager;
+
+  @Inject private RefByNameCacheWrapper refByNameCacheWrapper;
+
+  @Test
+  @GerritConfig(
+      name = "gerrit.installDbModule",
+      value = "com.googlesource.gerrit.plugins.gerritcachedrefdb.LibDbModule")
+  @GerritConfig(
+      name = "gerrit.installModule",
+      value = "com.googlesource.gerrit.plugins.gerritcachedrefdb.LibSysModule")
+  public void shouldBeAbleToInstallGerritCachedGitRepoManager() {
+    assertThat(gitRepoManager).isInstanceOf(GerritCachedGitRepositoryManager.class);
+    assertThat(refByNameCacheWrapper.cache()).isInstanceOf(RefByNameGerritCache.class);
+  }
+}