Add pack-refs command to the CLI

This command can be used to optimize storage of references.

For a RefDirectory database, it packs non-symbolic, loose refs into
packed-refs. By default, only the references under '$GIT_DIR/refs/tags'
are packed. The '--all' option can be used to pack all the references
under '$GIT_DIR/refs'.

For Reftable, all refs are compacted into a single table.

Change-Id: I92e786403f8638d35ae3037844a7ad89e8959e02
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/PackRefsTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/PackRefsTest.java
new file mode 100644
index 0000000..b4d4ea9
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/PackRefsTest.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2024 Qualcomm Innovation Center, Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.pgm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.CLIRepositoryTestCase;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PackRefsTest extends CLIRepositoryTestCase {
+	private Git git;
+
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+		git = new Git(db);
+		git.commit().setMessage("initial commit").call();
+	}
+
+	@Test
+	public void tagPacked() throws Exception {
+		git.tag().setName("test").call();
+		git.packRefs().call();
+		assertEquals(Ref.Storage.PACKED,
+				git.getRepository().exactRef("refs/tags/test").getStorage());
+	}
+
+	@Test
+	public void nonTagRefNotPackedWithoutAll() throws Exception {
+		git.branchCreate().setName("test").call();
+		git.packRefs().call();
+		assertEquals(Ref.Storage.LOOSE,
+				git.getRepository().exactRef("refs/heads/test").getStorage());
+	}
+
+	@Test
+	public void nonTagRefPackedWithAll() throws Exception {
+		git.branchCreate().setName("test").call();
+		git.packRefs().setAll(true).call();
+		assertEquals(Ref.Storage.PACKED,
+				git.getRepository().exactRef("refs/heads/test").getStorage());
+	}
+
+	@Test
+	public void refTableCompacted() throws Exception {
+		((FileRepository) git.getRepository()).convertRefStorage(
+				ConfigConstants.CONFIG_REF_STORAGE_REFTABLE, false, false);
+
+		git.commit().setMessage("test commit").call();
+		File tableDir = new File(db.getDirectory(), Constants.REFTABLE);
+		File[] reftables = tableDir.listFiles();
+		assertNotNull(reftables);
+		assertTrue(reftables.length > 2);
+
+		git.packRefs().call();
+
+		reftables = tableDir.listFiles();
+		assertNotNull(reftables);
+		assertEquals(2, reftables.length);
+	}
+}
diff --git a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
index 08d3727..41b0091 100644
--- a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
+++ b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
@@ -26,6 +26,7 @@
 org.eclipse.jgit.pgm.Merge
 org.eclipse.jgit.pgm.MergeBase
 org.eclipse.jgit.pgm.MergeTool
+org.eclipse.jgit.pgm.PackRefs
 org.eclipse.jgit.pgm.Push
 org.eclipse.jgit.pgm.ReceivePack
 org.eclipse.jgit.pgm.Reflog
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
index 50ee809..d24b639 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
@@ -257,6 +257,7 @@
 usage_Abbrev=Instead of using the default number of hexadecimal digits (which will vary according to the number of objects in the repository with a default of 7) of the abbreviated object name, use <n> digits, or as many digits as needed to form a unique object name. An <n> of 0 will suppress long format, only showing the closest tag.
 usage_addRenormalize=Apply the "clean" process freshly to tracked files to forcibly add them again to the index. This implies -u.
 usage_Aggressive=This option will cause gc to more aggressively optimize the repository at the expense of taking much more time
+usage_All=Pack all refs, except hidden refs, broken refs, and symbolic refs.
 usage_AlwaysFallback=Show uniquely abbreviated commit object as fallback
 usage_bareClone=Make a bare Git repository. That is, instead of creating [DIRECTORY] and placing the administrative files in [DIRECTORY]/.git, make the [DIRECTORY] itself the $GIT_DIR.
 usage_extraArgument=Pass an extra argument to a merge driver. Currently supported are "-X ours" and "-X theirs".
@@ -300,6 +301,7 @@
 usage_MergeBase=Find as good common ancestors as possible for a merge
 usage_MergesTwoDevelopmentHistories=Merges two development histories
 usage_PackKeptObjects=Include objects in packs locked by a ".keep" file when repacking
+usage_PackRefs=Pack heads and tags for efficient repository access
 usage_PreserveOldPacks=Preserve old pack files by moving them into the preserved subdirectory instead of deleting them after repacking
 usage_PrunePreserved=Remove the preserved subdirectory containing previously preserved old pack files before repacking, and before preserving more old pack files
 usage_ReadDirCache= Read the DirCache 100 times
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/PackRefs.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/PackRefs.java
new file mode 100644
index 0000000..ee05f5c
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/PackRefs.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2024 Qualcomm Innovation Center, Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.pgm;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.kohsuke.args4j.Option;
+
+@Command(common = true, usage = "usage_PackRefs")
+class PackRefs extends TextBuiltin {
+	@Option(name = "--all", usage = "usage_All")
+	private boolean all;
+
+	@Override
+	protected void run() {
+		Git git = Git.wrap(db);
+		try {
+			git.packRefs().setProgressMonitor(new TextProgressMonitor(errw))
+					.setAll(all).call();
+		} catch (GitAPIException e) {
+			throw die(e.getMessage(), e);
+		}
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
index c572955..f84be21 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
@@ -19,7 +19,6 @@
 import static org.junit.Assert.assertSame;
 
 import java.io.File;
-import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.concurrent.BrokenBarrierException;
@@ -31,6 +30,8 @@
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.PackRefsCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
@@ -49,7 +50,7 @@ public void looseRefPacked() throws Exception {
 		RevBlob a = tr.blob("a");
 		tr.lightweightTag("t", a);
 
-		gc.packRefs();
+		packRefs(false);
 		assertSame(repo.exactRef("refs/tags/t").getStorage(), Storage.PACKED);
 	}
 
@@ -60,7 +61,7 @@ public void emptyRefDirectoryDeleted() throws Exception {
 		String name = repo.findRef(ref).getName();
 		Path dir = repo.getCommonDirectory().toPath().resolve(name).getParent();
 		assertNotNull(dir);
-		gc.packRefs();
+		packRefs(true);
 		assertFalse(Files.exists(dir));
 	}
 
@@ -75,9 +76,9 @@ public void concurrentOnlyOneWritesPackedRefs() throws Exception {
 		Callable<Integer> packRefs = () -> {
 			syncPoint.await();
 			try {
-				gc.packRefs();
+				packRefs(false);
 				return 0;
-			} catch (IOException e) {
+			} catch (GitAPIException e) {
 				return 1;
 			}
 		};
@@ -102,7 +103,7 @@ public void whileRefLockedRefNotPackedNoError()
 				"refs/tags/t1"));
 		try {
 			refLock.lock();
-			gc.packRefs();
+			packRefs(false);
 		} finally {
 			refLock.unlock();
 		}
@@ -145,7 +146,7 @@ public boolean isForceUpdate() {
 
 			Future<Result> result2 = pool.submit(() -> {
 				refUpdateLockedRef.await();
-				gc.packRefs();
+				packRefs(false);
 				packRefsDone.await();
 				return null;
 			});
@@ -173,19 +174,20 @@ public void dontPackHEAD_nonBare() throws Exception {
 		assertEquals(repo.exactRef("HEAD").getTarget().getName(),
 				"refs/heads/master");
 		assertNull(repo.exactRef("HEAD").getTarget().getObjectId());
-		gc.packRefs();
+		PackRefsCommand packRefsCommand = git.packRefs().setAll(true);
+		packRefsCommand.call();
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 		assertEquals(repo.exactRef("HEAD").getTarget().getName(),
 				"refs/heads/master");
 		assertNull(repo.exactRef("HEAD").getTarget().getObjectId());
 
 		git.checkout().setName("refs/heads/side").call();
-		gc.packRefs();
+		packRefsCommand.call();
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 
 		// check for detached HEAD
 		git.checkout().setName(first.getName()).call();
-		gc.packRefs();
+		packRefsCommand.call();
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 	}
 
@@ -208,7 +210,7 @@ public void dontPackHEAD_bare() throws Exception {
 		assertEquals(repo.exactRef("HEAD").getTarget().getName(),
 				"refs/heads/master");
 		assertNull(repo.exactRef("HEAD").getTarget().getObjectId());
-		gc.packRefs();
+		packRefs(true);
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 		assertEquals(repo.exactRef("HEAD").getTarget().getName(),
 				"refs/heads/master");
@@ -216,9 +218,14 @@ public void dontPackHEAD_bare() throws Exception {
 
 		// check for non-detached HEAD
 		repo.updateRef(Constants.HEAD).link("refs/heads/side");
-		gc.packRefs();
+		packRefs(true);
 		assertSame(repo.exactRef("HEAD").getStorage(), Storage.LOOSE);
 		assertEquals(repo.exactRef("HEAD").getTarget().getObjectId(),
 				second.getId());
 	}
+
+	private void packRefs(boolean all) throws GitAPIException {
+		new PackRefsCommand(repo).setAll(all).call();
+	}
+
 }
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index 024ca77..acfe812 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -597,6 +597,8 @@
 packingCancelledDuringObjectsWriting=Packing cancelled during objects writing
 packObjectCountMismatch=Pack object count mismatch: pack {0} index {1}: {2}
 packRefs=Pack refs
+packRefsFailed=Packing refs failed
+packRefsSuccessful=Packed refs successfully
 packSizeNotSetYet=Pack size not yet set since it has not yet been received
 packTooLargeForIndexVersion1=Pack too large for index version 1
 packWasDeleted=Pack file {0} was deleted, removing it from pack list
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
index 3dc53ec..5bc035a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
@@ -714,6 +714,16 @@ public GarbageCollectCommand gc() {
 	}
 
 	/**
+	 * Return a command object to execute a {@code PackRefs} command
+	 *
+	 * @return a {@link org.eclipse.jgit.api.PackRefsCommand}
+	 * @since 7.1
+	 */
+	public PackRefsCommand packRefs() {
+		return new PackRefsCommand(repo);
+	}
+
+	/**
 	 * Return a command object to find human-readable names of revisions.
 	 *
 	 * @return a {@link org.eclipse.jgit.api.NameRevCommand}.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PackRefsCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PackRefsCommand.java
new file mode 100644
index 0000000..29a69c5
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PackRefsCommand.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2024 Qualcomm Innovation Center, Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+package org.eclipse.jgit.api;
+
+import java.io.IOException;
+
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Optimize storage of references.
+ *
+ * @since 7.1
+ */
+public class PackRefsCommand extends GitCommand<String> {
+	private ProgressMonitor monitor;
+
+	private boolean all;
+
+	/**
+	 * Creates a new {@link PackRefsCommand} instance with default values.
+	 *
+	 * @param repo
+	 * 		the repository this command will be used on
+	 */
+	public PackRefsCommand(Repository repo) {
+		super(repo);
+		this.monitor = NullProgressMonitor.INSTANCE;
+	}
+
+	/**
+	 * Set progress monitor
+	 *
+	 * @param monitor
+	 * 		a progress monitor
+	 * @return this instance
+	 */
+	public PackRefsCommand setProgressMonitor(ProgressMonitor monitor) {
+		this.monitor = monitor;
+		return this;
+	}
+
+	/**
+	 * Specify whether to pack all the references.
+	 *
+	 * @param all
+	 * 		if <code>true</code> all the loose refs will be packed
+	 * @return this instance
+	 */
+	public PackRefsCommand setAll(boolean all) {
+		this.all = all;
+		return this;
+	}
+
+	/**
+	 * Whether to pack all the references
+	 *
+	 * @return whether to pack all the references
+	 */
+	public boolean isAll() {
+		return all;
+	}
+
+	@Override
+	public String call() throws GitAPIException {
+		checkCallable();
+		try {
+			repo.getRefDatabase().packRefs(monitor, this);
+			return JGitText.get().packRefsSuccessful;
+		} catch (IOException e) {
+			throw new JGitInternalException(JGitText.get().packRefsFailed, e);
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index 0980219..2d9d2c5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -627,6 +627,8 @@ public static JGitText get() {
 	/***/ public String packingCancelledDuringObjectsWriting;
 	/***/ public String packObjectCountMismatch;
 	/***/ public String packRefs;
+	/***/ public String packRefsFailed;
+	/***/ public String packRefsSuccessful;
 	/***/ public String packSizeNotSetYet;
 	/***/ public String packTooLargeForIndexVersion1;
 	/***/ public String packWasDeleted;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java
index 80240e5..25b7583 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileReftableDatabase.java
@@ -28,8 +28,10 @@
 import java.util.stream.Collectors;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.PackRefsCommand;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.events.RefsChangedEvent;
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.reftable.MergedReftable;
 import org.eclipse.jgit.internal.storage.reftable.ReftableBatchRefUpdate;
 import org.eclipse.jgit.internal.storage.reftable.ReftableDatabase;
@@ -39,6 +41,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.RefRename;
@@ -108,6 +111,22 @@ public boolean hasFastTipsWithSha1() throws IOException {
 	}
 
 	/**
+	 * {@inheritDoc}
+	 *
+	 * For Reftable, all the data is compacted into a single table.
+	 */
+	@Override
+	public void packRefs(ProgressMonitor pm, PackRefsCommand packRefs)
+			throws IOException {
+		pm.beginTask(JGitText.get().packRefs, 1);
+		try {
+			compactFully();
+		} finally {
+			pm.endTask();
+		}
+	}
+
+	/**
 	 * Runs a full compaction for GC purposes.
 	 * @throws IOException on I/O errors
 	 */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
index 230dc6f..84c8565 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
@@ -33,6 +33,7 @@
 
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.attributes.AttributesNode;
 import org.eclipse.jgit.attributes.AttributesNodeProvider;
@@ -599,7 +600,7 @@ public void autoGC(ProgressMonitor monitor) {
 		gc.setBackground(shouldAutoDetach());
 		try {
 			gc.gc();
-		} catch (ParseException | IOException e) {
+		} catch (ParseException | IOException | GitAPIException e) {
 			throw new JGitInternalException(JGitText.get().gcFailed, e);
 		}
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
index c9da4d8..7f33693 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
@@ -63,6 +63,8 @@
 import java.util.stream.Stream;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.PackRefsCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.errors.CancelledException;
 import org.eclipse.jgit.errors.CorruptObjectException;
@@ -233,9 +235,11 @@ public GC(FileRepository repo) {
 	 * @throws java.text.ParseException
 	 *             If the configuration parameter "gc.pruneexpire" couldn't be
 	 *             parsed
+	 * @throws GitAPIException
+	 *             If packing refs failed
 	 */
 	public CompletableFuture<Collection<Pack>> gc()
-			throws IOException, ParseException {
+			throws IOException, ParseException, GitAPIException {
 		if (!background) {
 			return CompletableFuture.completedFuture(doGc());
 		}
@@ -254,7 +258,7 @@ public CompletableFuture<Collection<Pack>> gc()
 					gcLog.commit();
 				}
 				return newPacks;
-			} catch (IOException | ParseException e) {
+			} catch (IOException | ParseException | GitAPIException e) {
 				try {
 					gcLog.write(e.getMessage());
 					StringWriter sw = new StringWriter();
@@ -277,7 +281,8 @@ private ExecutorService executor() {
 		return (executor != null) ? executor : WorkQueue.getExecutor();
 	}
 
-	private Collection<Pack> doGc() throws IOException, ParseException {
+	private Collection<Pack> doGc()
+			throws IOException, ParseException, GitAPIException {
 		if (automatic && !needGc()) {
 			return Collections.emptyList();
 		}
@@ -286,7 +291,8 @@ private Collection<Pack> doGc() throws IOException, ParseException {
 				return Collections.emptyList();
 			}
 			pm.start(6 /* tasks */);
-			packRefs();
+			new PackRefsCommand(repo).setProgressMonitor(pm).setAll(true)
+					.call();
 			// TODO: implement reflog_expire(pm, repo);
 			Collection<Pack> newPacks = repack();
 			prune(Collections.emptySet());
@@ -780,43 +786,6 @@ private static boolean equals(Ref r1, Ref r2) {
 	}
 
 	/**
-	 * Pack ref storage. For a RefDirectory database, this packs all
-	 * non-symbolic, loose refs into packed-refs. For Reftable, all of the data
-	 * is compacted into a single table.
-	 *
-	 * @throws java.io.IOException
-	 *             if an IO error occurred
-	 */
-	public void packRefs() throws IOException {
-		RefDatabase refDb = repo.getRefDatabase();
-		if (refDb instanceof FileReftableDatabase) {
-			// TODO: abstract this more cleanly.
-			pm.beginTask(JGitText.get().packRefs, 1);
-			try {
-				((FileReftableDatabase) refDb).compactFully();
-			} finally {
-				pm.endTask();
-			}
-			return;
-		}
-
-		Collection<Ref> refs = refDb.getRefsByPrefix(Constants.R_REFS);
-		List<String> refsToBePacked = new ArrayList<>(refs.size());
-		pm.beginTask(JGitText.get().packRefs, refs.size());
-		try {
-			for (Ref ref : refs) {
-				checkCancelled();
-				if (!ref.isSymbolic() && ref.getStorage().isLoose())
-					refsToBePacked.add(ref.getName());
-				pm.update(1);
-			}
-			((RefDirectory) repo.getRefDatabase()).pack(refsToBePacked);
-		} finally {
-			pm.endTask();
-		}
-	}
-
-	/**
 	 * Packs all objects which reachable from any of the heads into one pack
 	 * file. Additionally all objects which are not reachable from any head but
 	 * which are reachable from any of the other refs (e.g. tags), special refs
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
index 6048681..6aa1157 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
@@ -57,6 +57,7 @@
 
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.PackRefsCommand;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.errors.LockFailedException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -69,6 +70,7 @@
 import org.eclipse.jgit.lib.CoreConfig.TrustPackedRefsStat;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefComparator;
 import org.eclipse.jgit.lib.RefDatabase;
@@ -287,6 +289,33 @@ public void refresh() {
 		clearReferences();
 	}
 
+	/**
+	 * {@inheritDoc}
+	 *
+	 * For a RefDirectory database, by default this packs non-symbolic, loose
+	 * tag refs into packed-refs. If {@code all} flag is set, this packs all the
+	 * non-symbolic, loose refs.
+	 */
+	@Override
+	public void packRefs(ProgressMonitor pm, PackRefsCommand packRefs)
+			throws IOException {
+		String prefix = packRefs.isAll() ? R_REFS : R_TAGS;
+		Collection<Ref> refs = getRefsByPrefix(prefix);
+		List<String> refsToBePacked = new ArrayList<>(refs.size());
+		pm.beginTask(JGitText.get().packRefs, refs.size());
+		try {
+			for (Ref ref : refs) {
+				if (!ref.isSymbolic() && ref.getStorage().isLoose()) {
+					refsToBePacked.add(ref.getName());
+				}
+				pm.update(1);
+			}
+			pack(refsToBePacked);
+		} finally {
+			pm.endTask();
+		}
+	}
+
 	@Override
 	public boolean isNameConflicting(String name) throws IOException {
 		// Cannot be nested within an existing reference.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
index 114246b..09cb5a8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
@@ -26,6 +26,7 @@
 
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.PackRefsCommand;
 
 /**
  * Abstraction of name to {@link org.eclipse.jgit.lib.ObjectId} mapping.
@@ -593,4 +594,22 @@ public static Ref findRef(Map<String, Ref> map, String name) {
 		}
 		return null;
 	}
+
+	/**
+	 * Optimize pack ref storage.
+	 *
+	 * @param pm
+	 *            a progress monitor
+	 *
+	 * @param packRefs
+	 *            {@link PackRefsCommand} to control ref packing behavior
+	 *
+	 * @throws java.io.IOException
+	 *             if an IO error occurred
+	 * @since 7.1
+	 */
+	public void packRefs(ProgressMonitor pm, PackRefsCommand packRefs)
+			throws IOException {
+		// nothing
+	}
 }