Merge branch 'master' into stable-5.11

* master:
  Manually set status of jmh dependencies
  Update DEPENDENCIES report for 5.11.0
  Add dependency to dash-licenses
  PackFile: Add id + ext based constructors
  GC: deleteOrphans: Use PackFile
  PackExt: Convert to Enum
  Restore preserved packs during missing object seeks
  Pack: Replace extensions bitset with bitmapIdx PackFile
  PackDirectory: Use PackFile to ensure we find preserved packs
  GC: Use PackFile to de-dup logic
  Create a PackFile class for Pack filenames

Change-Id: I1d56517cb6a95e10aed22cdb9e5f3e504872d110
diff --git a/DEPENDENCIES b/DEPENDENCIES
new file mode 100644
index 0000000..bffe3d9
--- /dev/null
+++ b/DEPENDENCIES
@@ -0,0 +1,66 @@
+maven/mavencentral/args4j/args4j/2.33, MIT, approved, CQ11068
+maven/mavencentral/com.google.code.gson/gson/2.8.6, Apache-2.0, approved, CQ23102
+maven/mavencentral/com.googlecode.javaewah/JavaEWAH/1.1.7, Apache-2.0, approved, CQ11658
+maven/mavencentral/com.jcraft/jsch/0.1.55, BSD-3-Clause, approved, CQ19435
+maven/mavencentral/com.jcraft/jzlib/1.1.1, BSD-2-Clause, approved, CQ6218
+maven/mavencentral/commons-codec/commons-codec/1.11, Apache-2.0, approved, CQ15971
+maven/mavencentral/commons-logging/commons-logging/1.2, Apache-2.0, approved, CQ10162
+maven/mavencentral/javax.servlet/javax.servlet-api/3.1.0, Apache-2.0 AND (CDDL-1.1 OR GPL-2.0 WITH Classpath-exception-2.0), approved, emo_ip_team
+maven/mavencentral/junit/junit/4.13, , approved, CQ22796
+maven/mavencentral/log4j/log4j/1.2.15, Apache-2.0, approved, CQ7837
+maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.9.0, Apache-2.0, approved, clearlydefined
+maven/mavencentral/net.bytebuddy/byte-buddy/1.9.0, Apache-2.0, approved, clearlydefined
+maven/mavencentral/net.i2p.crypto/eddsa/0.3.0, CC0, approved, CQ17804
+maven/mavencentral/net.sf.jopt-simple/jopt-simple/4.6, MIT, approved, clearlydefined
+maven/mavencentral/org.apache.ant/ant-launcher/1.10.8, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560
+maven/mavencentral/org.apache.ant/ant/1.10.8, Apache-2.0 AND W3C AND LicenseRef-Public-Domain, approved, CQ15560
+maven/mavencentral/org.apache.commons/commons-compress/1.19, Apache-2.0, approved, clearlydefined
+maven/mavencentral/org.apache.commons/commons-math3/3.2, Apache-2.0, approved, clearlydefined
+maven/mavencentral/org.apache.httpcomponents/httpclient/4.5.13, Apache-2.0, approved, CQ22761
+maven/mavencentral/org.apache.httpcomponents/httpcore/4.4.14, Apache-2.0, approved, CQ18704
+maven/mavencentral/org.apache.sshd/sshd-common/2.6.0, Apache-2.0 AND ISC, approved, CQ22992
+maven/mavencentral/org.apache.sshd/sshd-core/2.6.0, Apache-2.0 AND ISC, approved, CQ22992
+maven/mavencentral/org.apache.sshd/sshd-osgi/2.6.0, Apache-2.0 AND ISC, approved, CQ22992
+maven/mavencentral/org.apache.sshd/sshd-sftp/2.6.0, Apache-2.0 AND ISC, approved, CQ22993
+maven/mavencentral/org.assertj/assertj-core/3.14.0, Apache-2.0, approved, clearlydefined
+maven/mavencentral/org.bouncycastle/bcpg-jdk15on/1.65, Apache-2.0, approved, CQ21975
+maven/mavencentral/org.bouncycastle/bcpkix-jdk15on/1.65, MIT AND LicenseRef-Public-Domain, approved, CQ21976
+maven/mavencentral/org.bouncycastle/bcprov-jdk15on/1.65.01, MIT AND LicenseRef-Public-Domain, approved, CQ21977
+maven/mavencentral/org.eclipse.jetty/jetty-http/9.4.36.v20210114, , approved, eclipse
+maven/mavencentral/org.eclipse.jetty/jetty-io/9.4.36.v20210114, , approved, eclipse
+maven/mavencentral/org.eclipse.jetty/jetty-security/9.4.36.v20210114, , approved, eclipse
+maven/mavencentral/org.eclipse.jetty/jetty-server/9.4.36.v20210114, , approved, eclipse
+maven/mavencentral/org.eclipse.jetty/jetty-servlet/9.4.36.v20210114, , approved, eclipse
+maven/mavencentral/org.eclipse.jetty/jetty-util-ajax/9.4.36.v20210114, , approved, eclipse
+maven/mavencentral/org.eclipse.jetty/jetty-util/9.4.36.v20210114, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant.test/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ant/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.archive/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.gpg.bc/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.apache/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.server/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.http.test/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.http/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit.ssh/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.junit/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server.test/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.server/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs.test/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.lfs/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm.test/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.pgm/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache.test/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.apache/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ssh.jsch/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.test/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit.ui/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.eclipse.jgit/org.eclipse.jgit/5.11.0-SNAPSHOT, , approved, eclipse
+maven/mavencentral/org.hamcrest/hamcrest-core/1.3, BSD-2-Clause, approved, CQ7063
+maven/mavencentral/org.mockito/mockito-core/2.23.0, MIT, approved, CQ17976
+maven/mavencentral/org.objenesis/objenesis/2.6, Apache-2.0, approved, CQ15478
+maven/mavencentral/org.openjdk.jmh/jmh-core/1.21, GPL-2.0, approved, CQ20517
+maven/mavencentral/org.openjdk.jmh/jmh-generator-annprocess/1.21, GPL-2.0, approved, CQ20518
+maven/mavencentral/org.osgi/org.osgi.core/4.3.1, Apache-2.0, approved, CQ10111
+maven/mavencentral/org.slf4j/slf4j-api/1.7.30, MIT, approved, CQ13368
+maven/mavencentral/org.slf4j/slf4j-log4j12/1.7.30, MIT, approved, CQ7665
+maven/mavencentral/org.tukaani/xz/1.8, LicenseRef-Public-Domain, approved, CQ15386
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
index e3eb2c5..0232156 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
@@ -44,7 +44,9 @@
 import org.eclipse.jgit.internal.storage.file.LockFile;
 import org.eclipse.jgit.internal.storage.file.ObjectDirectory;
 import org.eclipse.jgit.internal.storage.file.Pack;
+import org.eclipse.jgit.internal.storage.file.PackFile;
 import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackWriter;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
@@ -906,23 +908,22 @@
 			ObjectDirectory odb = (ObjectDirectory) db.getObjectDatabase();
 			NullProgressMonitor m = NullProgressMonitor.INSTANCE;
 
-			final File pack, idx;
+			final PackFile pack, idx;
 			try (PackWriter pw = new PackWriter(db)) {
 				Set<ObjectId> all = new HashSet<>();
 				for (Ref r : db.getRefDatabase().getRefs())
 					all.add(r.getObjectId());
 				pw.preparePack(m, all, PackWriter.NONE);
 
-				final ObjectId name = pw.computeName();
-
-				pack = nameFor(odb, name, ".pack");
+				pack = new PackFile(odb.getPackDirectory(), pw.computeName(),
+						PackExt.PACK);
 				try (OutputStream out =
 						new BufferedOutputStream(new FileOutputStream(pack))) {
 					pw.writePack(m, m, out);
 				}
 				pack.setReadOnly();
 
-				idx = nameFor(odb, name, ".idx");
+				idx = pack.create(PackExt.INDEX);
 				try (OutputStream out =
 						new BufferedOutputStream(new FileOutputStream(idx))) {
 					pw.writeIndex(out);
@@ -960,11 +961,6 @@
 		}
 	}
 
-	private static File nameFor(ObjectDirectory odb, ObjectId name, String t) {
-		File packdir = odb.getPackDirectory();
-		return new File(packdir, "pack-" + name.name() + t);
-	}
-
 	private void writeFile(File p, byte[] bin) throws IOException,
 			ObjectWritingException {
 		final LockFile lck = new LockFile(p);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java
index 45d864d..bd36337 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AbbreviationTest.java
@@ -28,6 +28,7 @@
 import java.util.List;
 
 import org.eclipse.jgit.errors.AmbiguousObjectException;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
@@ -144,10 +145,9 @@
 			objects.add(new PackedObjectInfo(ObjectId.fromRaw(idBuf)));
 		}
 
-		String packName = "pack-" + id.name();
 		File packDir = db.getObjectDatabase().getPackDirectory();
-		File idxFile = new File(packDir, packName + ".idx");
-		File packFile = new File(packDir, packName + ".pack");
+		PackFile idxFile = new PackFile(packDir, id, PackExt.INDEX);
+		PackFile packFile = idxFile.create(PackExt.PACK);
 		FileUtils.mkdir(packDir, true);
 		try (OutputStream dst = new BufferedOutputStream(
 				new FileOutputStream(idxFile))) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java
index da3b5bb..df5d952 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ConcurrentRepackTest.java
@@ -27,6 +27,7 @@
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackWriter;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.AnyObjectId;
@@ -193,9 +194,10 @@
 				pw.addObject(o);
 			}
 
-			final ObjectId name = pw.computeName();
-			final File packFile = fullPackFileName(name, ".pack");
-			final File idxFile = fullPackFileName(name, ".idx");
+			PackFile packFile = new PackFile(
+					db.getObjectDatabase().getPackDirectory(), pw.computeName(),
+					PackExt.PACK);
+			PackFile idxFile = packFile.create(PackExt.INDEX);
 			final File[] files = new File[] { packFile, idxFile };
 			write(files, pw);
 			return files;
@@ -242,11 +244,6 @@
 		}
 	}
 
-	private File fullPackFileName(ObjectId name, String suffix) {
-		final File packdir = db.getObjectDatabase().getPackDirectory();
-		return new File(packdir, "pack-" + name.name() + suffix);
-	}
-
 	private RevObject writeBlob(Repository repo, String data)
 			throws IOException {
 		final byte[] bytes = Constants.encode(data);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java
index 42e4238..8dc1ddb 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcBasicPackingTest.java
@@ -23,6 +23,7 @@
 
 import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
 import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -295,7 +296,7 @@
 		// pack loose object into packfile
 		gc.setExpireAgeMillis(0);
 		gc.gc();
-		File oldPackfile = tr.getRepository().getObjectDatabase().getPacks()
+		PackFile oldPackfile = tr.getRepository().getObjectDatabase().getPacks()
 				.iterator().next().getPackFile();
 		assertTrue(oldPackfile.exists());
 
@@ -309,12 +310,59 @@
 		configureGc(gc, false).setPreserveOldPacks(true);
 		gc.gc();
 
-		File oldPackDir = repo.getObjectDatabase().getPreservedDirectory();
-		String oldPackFileName = oldPackfile.getName();
-		String oldPackName = oldPackFileName.substring(0,
-				oldPackFileName.lastIndexOf('.')) + ".old-pack";  //$NON-NLS-1$
-		File preservePackFile = new File(oldPackDir, oldPackName);
-		assertTrue(preservePackFile.exists());
+		File preservedPackFile = oldPackfile.createPreservedForDirectory(
+				repo.getObjectDatabase().getPreservedDirectory());
+		assertTrue(preservedPackFile.exists());
+	}
+
+	@Test
+	public void testPruneAndRestoreOldPacks() throws Exception {
+		String tempRef = "refs/heads/soon-to-be-unreferenced";
+		BranchBuilder bb = tr.branch(tempRef);
+		bb.commit().add("A", "A").add("B", "B").create();
+
+		// Verify setup conditions
+		stats = gc.getStatistics();
+		assertEquals(4, stats.numberOfLooseObjects);
+		assertEquals(0, stats.numberOfPackedObjects);
+
+		// Force all referenced objects into packs (to avoid having loose objects)
+		configureGc(gc, false);
+		gc.setExpireAgeMillis(0);
+		gc.setPackExpireAgeMillis(0);
+		gc.gc();
+		stats = gc.getStatistics();
+		assertEquals(0, stats.numberOfLooseObjects);
+		assertEquals(4, stats.numberOfPackedObjects);
+		assertEquals(1, stats.numberOfPackFiles);
+
+		// Delete the temp ref, orphaning its commit
+		RefUpdate update = tr.getRepository().getRefDatabase().newUpdate(tempRef, false);
+		update.setForceUpdate(true);
+		ObjectId objectId = update.getOldObjectId(); // remember it so we can restore it!
+		RefUpdate.Result result = update.delete();
+		assertEquals(RefUpdate.Result.FORCED, result);
+
+		fsTick();
+
+		// Repack with only orphaned commit, so packfile will be pruned
+		configureGc(gc, false).setPreserveOldPacks(true);
+		gc.gc();
+		stats = gc.getStatistics();
+		assertEquals(0, stats.numberOfLooseObjects);
+		assertEquals(0, stats.numberOfPackedObjects);
+		assertEquals(0, stats.numberOfPackFiles);
+
+		// Restore the temp ref to the deleted commit, should restore old-packs!
+		update = tr.getRepository().getRefDatabase().newUpdate(tempRef, false);
+		update.setNewObjectId(objectId);
+		update.setExpectedOldObjectId(null);
+		result = update.update();
+		assertEquals(RefUpdate.Result.NEW, result);
+
+		stats = gc.getStatistics();
+		assertEquals(4, stats.numberOfPackedObjects);
+		assertEquals(1, stats.numberOfPackFiles);
 	}
 
 	private PackConfig configureGc(GC myGc, boolean aggressive) {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java
index 8472983..5fcdd37 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcKeepFilesTest.java
@@ -14,10 +14,10 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import java.io.File;
 import java.util.Iterator;
 
 import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
 import org.junit.Test;
 
@@ -40,10 +40,7 @@
 				.iterator();
 		Pack singlePack = packIt.next();
 		assertFalse(packIt.hasNext());
-		String packFileName = singlePack.getPackFile().getPath();
-		String keepFileName = packFileName.substring(0,
-				packFileName.lastIndexOf('.')) + ".keep";
-		File keepFile = new File(keepFileName);
+		PackFile keepFile = singlePack.getPackFile().create(PackExt.KEEP);
 		assertFalse(keepFile.exists());
 		assertTrue(keepFile.createNewFile());
 		bb.commit().add("A", "A2").add("B", "B2").create();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileTest.java
new file mode 100644
index 0000000..619cfca
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackFileTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2021 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.internal.storage.file;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public class PackFileTest {
+	private static final ObjectId TEST_OID = ObjectId
+			.fromString("0123456789012345678901234567890123456789");
+
+	private static final String TEST_ID = TEST_OID.name();
+
+	private static final String PREFIX = "pack-";
+
+	private static final String OLD_PREFIX = "old-";
+
+	private static final String OLD_PACK = PREFIX + TEST_ID + "." + OLD_PREFIX
+			+ PackExt.PACK.getExtension();
+
+	private static final File TEST_PACK_DIR = new File(
+			"/path/to/repo.git/objects/pack");
+
+	private static final File TEST_PRESERVED_DIR = new File(TEST_PACK_DIR,
+			"preserved");
+
+	private static final PackFile TEST_PACKFILE_NO_EXT = new PackFile(
+			new File(TEST_PACK_DIR, PREFIX + TEST_ID));
+
+	@Test
+	public void objectsAreSameFromAnyConstructor() throws Exception {
+		String name = PREFIX + TEST_ID + "." + PackExt.PACK.getExtension();
+		File pack = new File(TEST_PACK_DIR, name);
+		PackFile pf = new PackFile(pack);
+		PackFile pfFromDirAndName = new PackFile(TEST_PACK_DIR, name);
+		assertPackFilesEqual(pf, pfFromDirAndName);
+
+		PackFile pfFromOIdAndExt = new PackFile(TEST_PACK_DIR, TEST_OID,
+				PackExt.PACK);
+		assertPackFilesEqual(pf, pfFromOIdAndExt);
+
+		PackFile pfFromIdAndExt = new PackFile(TEST_PACK_DIR, TEST_ID,
+				PackExt.PACK);
+		assertPackFilesEqual(pf, pfFromIdAndExt);
+	}
+
+	@Test
+	public void idIsSameFromFileWithOrWithoutExt() throws Exception {
+		PackFile packWithExt = new PackFile(new File(TEST_PACK_DIR,
+				PREFIX + TEST_ID + "." + PackExt.PACK.getExtension()));
+		assertEquals(packWithExt.getId(), TEST_PACKFILE_NO_EXT.getId());
+	}
+
+	@Test
+	public void idIsSameFromFileWithOrWithoutPrefix() throws Exception {
+		PackFile packWithoutPrefix = new PackFile(
+				new File(TEST_PACK_DIR, TEST_ID));
+		assertEquals(packWithoutPrefix.getId(), TEST_PACKFILE_NO_EXT.getId());
+	}
+
+	@Test
+	public void canCreatePreservedFromFile() throws Exception {
+		PackFile preserved = new PackFile(
+				new File(TEST_PRESERVED_DIR, OLD_PACK));
+		assertTrue(preserved.getName().contains(OLD_PACK));
+		assertEquals(preserved.getId(), TEST_ID);
+		assertEquals(preserved.getPackExt(), PackExt.PACK);
+	}
+
+	@Test
+	public void canCreatePreservedFromDirAndName() throws Exception {
+		PackFile preserved = new PackFile(TEST_PRESERVED_DIR, OLD_PACK);
+		assertTrue(preserved.getName().contains(OLD_PACK));
+		assertEquals(preserved.getId(), TEST_ID);
+		assertEquals(preserved.getPackExt(), PackExt.PACK);
+	}
+
+	@Test
+	public void cannotCreatePreservedNoExtFromNonPreservedNoExt()
+			throws Exception {
+		assertThrows(IllegalArgumentException.class, () -> TEST_PACKFILE_NO_EXT
+				.createPreservedForDirectory(TEST_PRESERVED_DIR));
+	}
+
+	@Test
+	public void canCreateAnyExtFromAnyExt() throws Exception {
+		for (PackExt from : PackExt.values()) {
+			PackFile dotFrom = TEST_PACKFILE_NO_EXT.create(from);
+			for (PackExt to : PackExt.values()) {
+				PackFile dotTo = dotFrom.create(to);
+				File expected = new File(TEST_PACK_DIR,
+						PREFIX + TEST_ID + "." + to.getExtension());
+				assertEquals(dotTo.getPackExt(), to);
+				assertEquals(dotFrom.getId(), dotTo.getId());
+				assertEquals(expected.getName(), dotTo.getName());
+			}
+		}
+	}
+
+	@Test
+	public void canCreatePreservedFromAnyExt() throws Exception {
+		for (PackExt ext : PackExt.values()) {
+			PackFile nonPreserved = TEST_PACKFILE_NO_EXT.create(ext);
+			PackFile preserved = nonPreserved
+					.createPreservedForDirectory(TEST_PRESERVED_DIR);
+			File expected = new File(TEST_PRESERVED_DIR,
+					PREFIX + TEST_ID + "." + OLD_PREFIX + ext.getExtension());
+			assertEquals(preserved.getName(), expected.getName());
+			assertEquals(preserved.getId(), TEST_ID);
+			assertEquals(preserved.getPackExt(), nonPreserved.getPackExt());
+		}
+	}
+
+	@Test
+	public void canCreateAnyPreservedExtFromAnyPreservedExt() throws Exception {
+		// Preserved PackFiles must have an extension
+		PackFile preserved = new PackFile(TEST_PRESERVED_DIR, OLD_PACK);
+		for (PackExt from : PackExt.values()) {
+			PackFile preservedWithExt = preserved.create(from);
+			for (PackExt to : PackExt.values()) {
+				PackFile preservedNewExt = preservedWithExt.create(to);
+				File expected = new File(TEST_PRESERVED_DIR, PREFIX + TEST_ID
+						+ "." + OLD_PREFIX + to.getExtension());
+				assertEquals(preservedNewExt.getPackExt(), to);
+				assertEquals(preservedWithExt.getId(), preservedNewExt.getId());
+				assertEquals(preservedNewExt.getName(), expected.getName());
+			}
+		}
+	}
+
+	@Test
+	public void canCreateNonPreservedFromAnyPreservedExt() throws Exception {
+		// Preserved PackFiles must have an extension
+		PackFile preserved = new PackFile(TEST_PRESERVED_DIR, OLD_PACK);
+		for (PackExt ext : PackExt.values()) {
+			PackFile preservedWithExt = preserved.create(ext);
+			PackFile nonPreserved = preservedWithExt
+					.createForDirectory(TEST_PACK_DIR);
+			File expected = new File(TEST_PACK_DIR,
+					PREFIX + TEST_ID + "." + ext.getExtension());
+			assertEquals(nonPreserved.getName(), expected.getName());
+			assertEquals(nonPreserved.getId(), TEST_ID);
+			assertEquals(nonPreserved.getPackExt(),
+					preservedWithExt.getPackExt());
+		}
+	}
+
+	private void assertPackFilesEqual(PackFile p1, PackFile p2) {
+		// for test purposes, considered equal if id, name, and ext are equal
+		assertEquals(p1.getId(), p2.getId());
+		assertEquals(p1.getPackExt(), p2.getPackExt());
+		assertEquals(p1.getName(), p2.getName());
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java
index 182e422..a359654 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackTest.java
@@ -246,8 +246,8 @@
 
 			File dir = new File(repo.getObjectDatabase().getDirectory(),
 					"pack");
-			File packName = new File(dir, idA.name() + ".pack");
-			File idxName = new File(dir, idA.name() + ".idx");
+			PackFile packName = new PackFile(dir, idA.name() + ".pack");
+			PackFile idxName = packName.create(PackExt.INDEX);
 
 			try (FileOutputStream f = new FileOutputStream(packName)) {
 				f.write(packContents.toByteArray());
@@ -261,7 +261,7 @@
 				new PackIndexWriterV1(f).write(list, footer);
 			}
 
-			Pack pack = new Pack(packName, PackExt.INDEX.getBit());
+			Pack pack = new Pack(packName, null);
 			try {
 				pack.get(wc, b);
 				fail("expected LargeObjectException.ExceedsByteArrayLimit");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
index 214ddb9..e422ab9 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
@@ -34,6 +34,7 @@
 
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackWriter;
 import org.eclipse.jgit.junit.JGitTestUtil;
 import org.eclipse.jgit.junit.TestRepository;
@@ -305,9 +306,9 @@
 	@Test
 	public void testWritePack2DeltasCRC32Copy() throws IOException {
 		final File packDir = db.getObjectDatabase().getPackDirectory();
-		final File crc32Pack = new File(packDir,
+		final PackFile crc32Pack = new PackFile(packDir,
 				"pack-34be9032ac282b11fa9babdc2b2a93ca996c9c2f.pack");
-		final File crc32Idx = new File(packDir,
+		final PackFile crc32Idx = new PackFile(packDir,
 				"pack-34be9032ac282b11fa9babdc2b2a93ca996c9c2f.idx");
 		copyFile(JGitTestUtil.getTestResourceFile(
 				"pack-34be9032ac282b11fa9babdc2b2a93ca996c9c2f.idxV2"),
@@ -471,10 +472,8 @@
 		config.setIndexVersion(2);
 		writeVerifyPack4(false);
 
-		File packFile = pack.getPackFile();
-		String name = packFile.getName();
-		String base = name.substring(0, name.lastIndexOf('.'));
-		File indexFile = new File(packFile.getParentFile(), base + ".idx");
+		PackFile packFile = pack.getPackFile();
+		PackFile indexFile = packFile.create(PackExt.INDEX);
 
 		// Validate that IndexPack came up with the right CRC32 value.
 		final PackIndex idx1 = PackIndex.open(indexFile);
@@ -685,14 +684,14 @@
 			ObjectWalk ow = walk.toObjectWalkWithSameObjects();
 
 			pw.preparePack(NullProgressMonitor.INSTANCE, ow, want, have, NONE);
-			String id = pw.computeName().getName();
 			File packdir = repo.getObjectDatabase().getPackDirectory();
-			File packFile = new File(packdir, "pack-" + id + ".pack");
+			PackFile packFile = new PackFile(packdir, pw.computeName(),
+					PackExt.PACK);
 			try (FileOutputStream packOS = new FileOutputStream(packFile)) {
 				pw.writePack(NullProgressMonitor.INSTANCE,
 						NullProgressMonitor.INSTANCE, packOS);
 			}
-			File idxFile = new File(packdir, "pack-" + id + ".idx");
+			PackFile idxFile = packFile.create(PackExt.INDEX);
 			try (FileOutputStream idxOS = new FileOutputStream(idxFile)) {
 				pw.writeIndex(idxOS);
 			}
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 c00203d..9695e57 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -743,6 +743,7 @@
 unmergedPaths=Repository contains unmerged paths
 unpackException=Exception while parsing pack stream
 unreadablePackIndex=Unreadable pack index: {0}
+unrecognizedPackExtension=Unrecognized pack extension: {0}
 unrecognizedRef=Unrecognized ref: {0}
 unsetMark=Mark not set
 unsupportedAlternates=Alternates not supported
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 9d215ca..95265fe 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -771,6 +771,7 @@
 	/***/ public String unmergedPaths;
 	/***/ public String unpackException;
 	/***/ public String unreadablePackIndex;
+	/***/ public String unrecognizedPackExtension;
 	/***/ public String unrecognizedRef;
 	/***/ public String unsetMark;
 	/***/ public String unsupportedAlternates;
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 75de3be..9ffff9f 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
@@ -12,6 +12,8 @@
 
 import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.KEEP;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -346,7 +348,7 @@
 				if (shouldLoosen) {
 					loosen(inserter, reader, oldPack, ids);
 				}
-				prunePack(oldName);
+				prunePack(oldPack.getPackFile());
 			}
 		}
 
@@ -360,19 +362,17 @@
 	 * moves the pack file to the preserved directory
 	 *
 	 * @param packFile
-	 * @param packName
-	 * @param ext
 	 * @param deleteOptions
 	 * @throws IOException
 	 */
-	private void removeOldPack(File packFile, String packName, PackExt ext,
-			int deleteOptions) throws IOException {
+	private void removeOldPack(PackFile packFile, int deleteOptions)
+			throws IOException {
 		if (pconfig.isPreserveOldPacks()) {
 			File oldPackDir = repo.getObjectDatabase().getPreservedDirectory();
 			FileUtils.mkdir(oldPackDir, true);
 
-			String oldPackName = "pack-" + packName + ".old-" + ext.getExtension();  //$NON-NLS-1$ //$NON-NLS-2$
-			File oldPackFile = new File(oldPackDir, oldPackName);
+			PackFile oldPackFile = packFile
+					.createPreservedForDirectory(oldPackDir);
 			FileUtils.rename(packFile, oldPackFile);
 		} else {
 			FileUtils.delete(packFile, deleteOptions);
@@ -401,27 +401,21 @@
 	 * ".index" file and when failing to delete the ".pack" file we are left
 	 * with a ".pack" file without a ".index" file.
 	 *
-	 * @param packName
+	 * @param packFile
 	 */
-	private void prunePack(String packName) {
-		PackExt[] extensions = PackExt.values();
+	private void prunePack(PackFile packFile) {
 		try {
 			// Delete the .pack file first and if this fails give up on deleting
 			// the other files
 			int deleteOptions = FileUtils.RETRY | FileUtils.SKIP_MISSING;
-			for (PackExt ext : extensions)
-				if (PackExt.PACK.equals(ext)) {
-					File f = nameFor(packName, "." + ext.getExtension()); //$NON-NLS-1$
-					removeOldPack(f, packName, ext, deleteOptions);
-					break;
-				}
+			removeOldPack(packFile.create(PackExt.PACK), deleteOptions);
+
 			// The .pack file has been deleted. Delete as many as the other
 			// files as you can.
 			deleteOptions |= FileUtils.IGNORE_ERRORS;
-			for (PackExt ext : extensions) {
+			for (PackExt ext : PackExt.values()) {
 				if (!PackExt.PACK.equals(ext)) {
-					File f = nameFor(packName, "." + ext.getExtension()); //$NON-NLS-1$
-					removeOldPack(f, packName, ext, deleteOptions);
+					removeOldPack(packFile.create(ext), deleteOptions);
 				}
 			}
 		} catch (IOException e) {
@@ -973,20 +967,21 @@
 			return;
 		}
 
-		String base = null;
+		String latestId = null;
 		for (String n : fileNames) {
-			if (n.endsWith(PACK_EXT) || n.endsWith(KEEP_EXT)) {
-				base = n.substring(0, n.lastIndexOf('.'));
-			} else {
-				if (base == null || !n.startsWith(base)) {
-					try {
-						Path delete = packDir.resolve(n);
-						FileUtils.delete(delete.toFile(),
-								FileUtils.RETRY | FileUtils.SKIP_MISSING);
-						LOG.warn(JGitText.get().deletedOrphanInPackDir, delete);
-					} catch (IOException e) {
-						LOG.error(e.getMessage(), e);
-					}
+			PackFile pf = new PackFile(packDir.toFile(), n);
+			PackExt ext = pf.getPackExt();
+			if (ext.equals(PACK) || ext.equals(KEEP)) {
+				latestId = pf.getId();
+			}
+			if (latestId == null || !pf.getId().equals(latestId)) {
+				// no pack or keep for this id
+				try {
+					FileUtils.delete(pf,
+							FileUtils.RETRY | FileUtils.SKIP_MISSING);
+					LOG.warn(JGitText.get().deletedOrphanInPackDir, pf);
+				} catch (IOException e) {
+					LOG.error(e.getMessage(), e);
 				}
 			}
 		}
@@ -1168,7 +1163,7 @@
 			checkCancelled();
 
 			// create temporary files
-			String id = pw.computeName().getName();
+			ObjectId id = pw.computeName();
 			File packdir = repo.getObjectDatabase().getPackDirectory();
 			packdir.mkdirs();
 			tmpPack = File.createTempFile("gc_", ".pack_tmp", packdir); //$NON-NLS-1$ //$NON-NLS-2$
@@ -1218,7 +1213,8 @@
 			}
 
 			// rename the temporary files to real files
-			File realPack = nameFor(id, ".pack"); //$NON-NLS-1$
+			File packDir = repo.getObjectDatabase().getPackDirectory();
+			PackFile realPack = new PackFile(packDir, id, PackExt.PACK);
 
 			repo.getObjectDatabase().closeAllPackHandles(realPack);
 			tmpPack.setReadOnly();
@@ -1228,8 +1224,7 @@
 				File tmpExt = tmpEntry.getValue();
 				tmpExt.setReadOnly();
 
-				File realExt = nameFor(id,
-						"." + tmpEntry.getKey().getExtension()); //$NON-NLS-1$
+				PackFile realExt = new PackFile(packDir, id, tmpEntry.getKey());
 				try {
 					FileUtils.rename(tmpExt, realExt,
 							StandardCopyOption.ATOMIC_MOVE);
@@ -1275,11 +1270,6 @@
 		}
 	}
 
-	private File nameFor(String name, String ext) {
-		File packdir = repo.getObjectDatabase().getPackDirectory();
-		return new File(packdir, "pack-" + name + ext); //$NON-NLS-1$
-	}
-
 	private void checkCancelled() throws CancelledException {
 		if (pm.isCancelled() || Thread.currentThread().isInterrupted()) {
 			throw new CancelledException(JGitText.get().operationCanceled);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java
index ae5bce6..f112947 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java
@@ -17,6 +17,7 @@
 
 import org.eclipse.jgit.internal.storage.pack.CachedPack;
 import org.eclipse.jgit.internal.storage.pack.ObjectToPack;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackOutputStream;
 import org.eclipse.jgit.internal.storage.pack.StoredObjectRepresentation;
 
@@ -88,6 +89,6 @@
 
 	private String getPackFilePath(String packName) {
 		final File packDir = odb.getPackDirectory();
-		return new File(packDir, "pack-" + packName + ".pack").getPath(); //$NON-NLS-1$ //$NON-NLS-2$
+		return new PackFile(packDir, packName, PackExt.PACK).getPath();
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
index e71a960..627facc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
@@ -11,8 +11,9 @@
 package org.eclipse.jgit.internal.storage.file;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -79,7 +80,7 @@
 
 	private final PackDirectory packed;
 
-	private final File preservedDirectory;
+	private final PackDirectory preserved;
 
 	private final File alternatesFile;
 
@@ -117,10 +118,11 @@
 		objects = dir;
 		infoDirectory = new File(objects, "info"); //$NON-NLS-1$
 		File packDirectory = new File(objects, "pack"); //$NON-NLS-1$
-		preservedDirectory = new File(packDirectory, "preserved"); //$NON-NLS-1$
+		File preservedDirectory = new File(packDirectory, "preserved"); //$NON-NLS-1$
 		alternatesFile = new File(objects, Constants.INFO_ALTERNATES);
 		loose = new LooseObjects(objects);
 		packed = new PackDirectory(config, packDirectory);
+		preserved = new PackDirectory(config, preservedDirectory);
 		this.fs = fs;
 		this.shallowFile = shallowFile;
 
@@ -156,7 +158,7 @@
 	 * @return the location of the <code>preserved</code> directory.
 	 */
 	public final File getPreservedDirectory() {
-		return preservedDirectory;
+		return preserved.getDirectory();
 	}
 
 	/** {@inheritDoc} */
@@ -216,26 +218,26 @@
 	 * Add a single existing pack to the list of available pack files.
 	 */
 	@Override
-	public Pack openPack(File pack)
-			throws IOException {
-		final String p = pack.getName();
-		if (p.length() != 50 || !p.startsWith("pack-") || !p.endsWith(".pack")) //$NON-NLS-1$ //$NON-NLS-2$
-			throw new IOException(MessageFormat.format(JGitText.get().notAValidPack, pack));
-
-		// The pack and index are assumed to exist. The existence of other
-		// extensions needs to be explicitly checked.
-		//
-		int extensions = PACK.getBit() | INDEX.getBit();
-		final String base = p.substring(0, p.length() - 4);
-		for (PackExt ext : PackExt.values()) {
-			if ((extensions & ext.getBit()) == 0) {
-				final String name = base + ext.getExtension();
-				if (new File(pack.getParentFile(), name).exists())
-					extensions |= ext.getBit();
-			}
+	public Pack openPack(File pack) throws IOException {
+		PackFile pf;
+		try {
+			pf = new PackFile(pack);
+		} catch (IllegalArgumentException e) {
+			throw new IOException(
+					MessageFormat.format(JGitText.get().notAValidPack, pack),
+					e);
 		}
 
-		Pack res = new Pack(pack, extensions);
+		String p = pf.getName();
+		// TODO(nasserg): See if PackFile can do these checks instead
+		if (p.length() != 50 || !p.startsWith("pack-") //$NON-NLS-1$
+				|| !pf.getPackExt().equals(PACK)) {
+			throw new IOException(
+					MessageFormat.format(JGitText.get().notAValidPack, pack));
+		}
+
+		PackFile bitmapIdx = pf.create(BITMAP_INDEX);
+		Pack res = new Pack(pack, bitmapIdx.exists() ? bitmapIdx : null);
 		packed.insert(res);
 		return res;
 	}
@@ -250,7 +252,13 @@
 	@Override
 	public boolean has(AnyObjectId objectId) {
 		return loose.hasCached(objectId)
-				|| hasPackedInSelfOrAlternate(objectId, null)
+				|| hasPackedOrLooseInSelfOrAlternate(objectId)
+				|| (restoreFromSelfOrAlternate(objectId, null)
+						&& hasPackedOrLooseInSelfOrAlternate(objectId));
+	}
+
+	private boolean hasPackedOrLooseInSelfOrAlternate(AnyObjectId objectId) {
+		return hasPackedInSelfOrAlternate(objectId, null)
 				|| hasLooseInSelfOrAlternate(objectId, null);
 	}
 
@@ -319,6 +327,15 @@
 	@Override
 	ObjectLoader openObject(WindowCursor curs, AnyObjectId objectId)
 			throws IOException {
+		ObjectLoader ldr = openObjectWithoutRestoring(curs, objectId);
+		if (ldr == null && restoreFromSelfOrAlternate(objectId, null)) {
+			ldr = openObjectWithoutRestoring(curs, objectId);
+		}
+		return ldr;
+	}
+
+	private ObjectLoader openObjectWithoutRestoring(WindowCursor curs, AnyObjectId objectId)
+			throws IOException {
 		if (loose.hasCached(objectId)) {
 			ObjectLoader ldr = openLooseObject(curs, objectId);
 			if (ldr != null) {
@@ -380,8 +397,16 @@
 	}
 
 	@Override
-	long getObjectSize(WindowCursor curs, AnyObjectId id)
-			throws IOException {
+	long getObjectSize(WindowCursor curs, AnyObjectId id) throws IOException {
+		long sz = getObjectSizeWithoutRestoring(curs, id);
+		if (0 > sz && restoreFromSelfOrAlternate(id, null)) {
+			sz = getObjectSizeWithoutRestoring(curs, id);
+		}
+		return sz;
+	}
+
+	private long getObjectSizeWithoutRestoring(WindowCursor curs,
+			AnyObjectId id) throws IOException {
 		if (loose.hasCached(id)) {
 			long len = loose.getSize(curs, id);
 			if (0 <= len) {
@@ -449,6 +474,51 @@
 		}
 	}
 
+	private boolean restoreFromSelfOrAlternate(AnyObjectId objectId,
+			Set<AlternateHandle.Id> skips) {
+		if (restoreFromSelf(objectId)) {
+			return true;
+		}
+
+		skips = addMe(skips);
+		for (AlternateHandle alt : myAlternates()) {
+			if (!skips.contains(alt.getId())) {
+				if (alt.db.restoreFromSelfOrAlternate(objectId, skips)) {
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+
+	private boolean restoreFromSelf(AnyObjectId objectId) {
+		Pack preservedPack = preserved.getPack(objectId);
+		if (preservedPack == null) {
+			return false;
+		}
+		PackFile preservedFile = new PackFile(preservedPack.getPackFile());
+		// Restore the index last since the set will be considered for use once
+		// the index appears.
+		for (PackExt ext : PackExt.values()) {
+			if (!INDEX.equals(ext)) {
+				restore(preservedFile.create(ext));
+			}
+		}
+		restore(preservedFile.create(INDEX));
+		return true;
+	}
+
+	private boolean restore(PackFile preservedPack) {
+		PackFile restored = preservedPack
+				.createForDirectory(packed.getDirectory());
+		try {
+			Files.createLink(restored.toPath(), preservedPack.toPath());
+		} catch (IOException e) {
+			return false;
+		}
+		return true;
+	}
+
 	@Override
 	InsertLooseObjectResult insertUnpackedObject(File tmp, ObjectId id,
 			boolean createDuplicate) throws IOException {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
index 04d2ff8..dba8ccd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
@@ -27,6 +27,7 @@
 
 import org.eclipse.jgit.errors.LockFailedException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.CoreConfig;
@@ -426,10 +427,10 @@
 			d.update(oeBytes);
 		}
 
-		final String name = ObjectId.fromRaw(d.digest()).name();
-		final File packDir = new File(db.getDirectory(), "pack"); //$NON-NLS-1$
-		final File finalPack = new File(packDir, "pack-" + name + ".pack"); //$NON-NLS-1$ //$NON-NLS-2$
-		final File finalIdx = new File(packDir, "pack-" + name + ".idx"); //$NON-NLS-1$ //$NON-NLS-2$
+		ObjectId id = ObjectId.fromRaw(d.digest());
+		File packDir = new File(db.getDirectory(), "pack"); //$NON-NLS-1$
+		PackFile finalPack = new PackFile(packDir, id, PackExt.PACK);
+		PackFile finalIdx = finalPack.create(PackExt.INDEX);
 		final PackLock keep = new PackLock(finalPack, db.getFS());
 
 		if (!packDir.exists() && !packDir.mkdir() && !packDir.exists()) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java
index d928633..5efd4c5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/Pack.java
@@ -12,7 +12,6 @@
 
 package org.eclipse.jgit.internal.storage.file;
 
-import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.KEEP;
 
@@ -38,6 +37,7 @@
 import java.util.zip.DataFormatException;
 import java.util.zip.Inflater;
 
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -51,7 +51,6 @@
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.pack.BinaryDelta;
 import org.eclipse.jgit.internal.storage.pack.ObjectToPack;
-import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackOutputStream;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.AnyObjectId;
@@ -78,13 +77,9 @@
 	public static final Comparator<Pack> SORT = (a, b) -> b.packLastModified
 			.compareTo(a.packLastModified);
 
-	private final File packFile;
+	private final PackFile packFile;
 
-	private final int extensions;
-
-	private File keepFile;
-
-	private volatile String packName;
+	private PackFile keepFile;
 
 	final int hash;
 
@@ -107,7 +102,8 @@
 
 	private volatile Exception invalidatingCause;
 
-	private boolean invalidBitmap;
+	@Nullable
+	private PackFile bitmapIdxFile;
 
 	private AtomicInteger transientErrorCount = new AtomicInteger();
 
@@ -133,14 +129,14 @@
 	 *
 	 * @param packFile
 	 *            path of the <code>.pack</code> file holding the data.
-	 * @param extensions
-	 *            additional pack file extensions with the same base as the pack
+	 * @param bitmapIdxFile
+	 *            existing bitmap index file with the same base as the pack
 	 */
-	public Pack(File packFile, int extensions) {
-		this.packFile = packFile;
+	public Pack(File packFile, @Nullable PackFile bitmapIdxFile) {
+		this.packFile = new PackFile(packFile);
 		this.fileSnapshot = PackFileSnapshot.save(packFile);
 		this.packLastModified = fileSnapshot.lastModifiedInstant();
-		this.extensions = extensions;
+		this.bitmapIdxFile = bitmapIdxFile;
 
 		// Multiply by 31 here so we can more directly combine with another
 		// value in WindowCache.hash(), without doing the multiply there.
@@ -156,16 +152,18 @@
 				idx = loadedIdx;
 				if (idx == null) {
 					if (invalid) {
-						throw new PackInvalidException(packFile, invalidatingCause);
+						throw new PackInvalidException(packFile,
+								invalidatingCause);
 					}
 					try {
 						long start = System.currentTimeMillis();
-						idx = PackIndex.open(extFile(INDEX));
+						PackFile idxFile = packFile.create(INDEX);
+						idx = PackIndex.open(idxFile);
 						if (LOG.isDebugEnabled()) {
 							LOG.debug(String.format(
 									"Opening pack index %s, size %.3f MB took %d ms", //$NON-NLS-1$
-									extFile(INDEX).getAbsolutePath(),
-									Float.valueOf(extFile(INDEX).length()
+									idxFile.getAbsolutePath(),
+									Float.valueOf(idxFile.length()
 											/ (1024f * 1024)),
 									Long.valueOf(System.currentTimeMillis()
 											- start)));
@@ -205,7 +203,7 @@
 	 *
 	 * @return the File object which locates this pack on disk.
 	 */
-	public File getPackFile() {
+	public PackFile getPackFile() {
 		return packFile;
 	}
 
@@ -225,16 +223,7 @@
 	 * @return name extracted from {@code pack-*.pack} pattern.
 	 */
 	public String getPackName() {
-		String name = packName;
-		if (name == null) {
-			name = getPackFile().getName();
-			if (name.startsWith("pack-")) //$NON-NLS-1$
-				name = name.substring("pack-".length()); //$NON-NLS-1$
-			if (name.endsWith(".pack")) //$NON-NLS-1$
-				name = name.substring(0, name.length() - ".pack".length()); //$NON-NLS-1$
-			packName = name;
-		}
-		return name;
+		return packFile.getId();
 	}
 
 	/**
@@ -261,8 +250,9 @@
 	 * @return true if a .keep file exist.
 	 */
 	public boolean shouldBeKept() {
-		if (keepFile == null)
-			keepFile = extFile(KEEP);
+		if (keepFile == null) {
+			keepFile = packFile.create(KEEP);
+		}
 		return keepFile.exists();
 	}
 
@@ -1132,26 +1122,28 @@
 	}
 
 	synchronized PackBitmapIndex getBitmapIndex() throws IOException {
-		if (invalid || invalidBitmap)
+		if (invalid || bitmapIdxFile == null) {
 			return null;
-		if (bitmapIdx == null && hasExt(BITMAP_INDEX)) {
+		}
+		if (bitmapIdx == null) {
 			final PackBitmapIndex idx;
 			try {
-				idx = PackBitmapIndex.open(extFile(BITMAP_INDEX), idx(),
+				idx = PackBitmapIndex.open(bitmapIdxFile, idx(),
 						getReverseIdx());
 			} catch (FileNotFoundException e) {
 				// Once upon a time this bitmap file existed. Now it
 				// has been removed. Most likely an external gc  has
 				// removed this packfile and the bitmap
-				 invalidBitmap = true;
-				 return null;
+				bitmapIdxFile = null;
+				return null;
 			}
 
 			// At this point, idx() will have set packChecksum.
-			if (Arrays.equals(packChecksum, idx.packChecksum))
+			if (Arrays.equals(packChecksum, idx.packChecksum)) {
 				bitmapIdx = idx;
-			else
-				invalidBitmap = true;
+			} else {
+				bitmapIdxFile = null;
+			}
 		}
 		return bitmapIdx;
 	}
@@ -1187,17 +1179,6 @@
 		}
 	}
 
-	private File extFile(PackExt ext) {
-		String p = packFile.getName();
-		int dot = p.lastIndexOf('.');
-		String b = (dot < 0) ? p : p.substring(0, dot);
-		return new File(packFile.getParentFile(), b + '.' + ext.getExtension());
-	}
-
-	private boolean hasExt(PackExt ext) {
-		return (extensions & ext.getBit()) != 0;
-	}
-
 	@SuppressWarnings("nls")
 	@Override
 	public String toString() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java
index b2ba36b..73745d8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackDirectory.java
@@ -10,6 +10,8 @@
 
 package org.eclipse.jgit.internal.storage.file;
 
+import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
+import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
 
 import java.io.File;
@@ -20,13 +22,14 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.EnumMap;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.PackInvalidException;
 import org.eclipse.jgit.errors.PackMismatchException;
@@ -121,21 +124,36 @@
 	 *
 	 * @param objectId
 	 *            identity of the object to test for existence of.
-	 * @return true if the specified object is stored in this PackDirectory.
+	 * @return {@code true} if the specified object is stored in this PackDirectory.
 	 */
 	boolean has(AnyObjectId objectId) {
+		return getPack(objectId) != null;
+	}
+
+	/**
+	 * Get the {@link org.eclipse.jgit.internal.storage.file.Pack} for the
+	 * specified object if it is stored in this PackDirectory.
+	 *
+	 * @param objectId
+	 *            identity of the object to find the Pack for.
+	 * @return {@link org.eclipse.jgit.internal.storage.file.Pack} which
+	 *         contains the specified object or {@code null} if it is not stored
+	 *         in this PackDirectory.
+	 */
+	@Nullable
+	Pack getPack(AnyObjectId objectId) {
 		PackList pList;
 		do {
 			pList = packList.get();
 			for (Pack p : pList.packs) {
 				try {
 					if (p.hasObject(objectId)) {
-						return true;
+						return p;
 					}
 				} catch (IOException e) {
-					// The hasObject call should have only touched the index,
-					// so any failure here indicates the index is unreadable
-					// by this process, and the pack is likewise not readable.
+					// The hasObject call should have only touched the index, so
+					// any failure here indicates the index is unreadable by
+					// this process, and the pack is likewise not readable.
 					LOG.warn(MessageFormat.format(
 							JGitText.get().unableToReadPackfile,
 							p.getPackFile().getAbsolutePath()), e);
@@ -143,7 +161,7 @@
 				}
 			}
 		} while (searchPacksAgain(pList));
-		return false;
+		return null;
 	}
 
 	/**
@@ -398,43 +416,29 @@
 	private PackList scanPacksImpl(PackList old) {
 		final Map<String, Pack> forReuse = reuseMap(old);
 		final FileSnapshot snapshot = FileSnapshot.save(directory);
-		final Set<String> names = listPackDirectory();
-		final List<Pack> list = new ArrayList<>(names.size() >> 2);
+		Map<String, Map<PackExt, PackFile>> packFilesByExtById = getPackFilesByExtById();
+		List<Pack> list = new ArrayList<>(packFilesByExtById.size());
 		boolean foundNew = false;
-		for (String indexName : names) {
-			// Must match "pack-[0-9a-f]{40}.idx" to be an index.
-			//
-			if (indexName.length() != 49 || !indexName.endsWith(".idx")) { //$NON-NLS-1$
-				continue;
-			}
-
-			final String base = indexName.substring(0, indexName.length() - 3);
-			int extensions = 0;
-			for (PackExt ext : PackExt.values()) {
-				if (names.contains(base + ext.getExtension())) {
-					extensions |= ext.getBit();
-				}
-			}
-
-			if ((extensions & PACK.getBit()) == 0) {
+		for (Map<PackExt, PackFile> packFilesByExt : packFilesByExtById
+				.values()) {
+			PackFile packFile = packFilesByExt.get(PACK);
+			if (packFile == null || !packFilesByExt.containsKey(INDEX)) {
 				// Sometimes C Git's HTTP fetch transport leaves a
 				// .idx file behind and does not download the .pack.
 				// We have to skip over such useless indexes.
-				//
+				// Also skip if we don't have any index for this id
 				continue;
 			}
 
-			final String packName = base + PACK.getExtension();
-			final File packFile = new File(directory, packName);
-			final Pack oldPack = forReuse.get(packName);
+			Pack oldPack = forReuse.get(packFile.getName());
 			if (oldPack != null
 					&& !oldPack.getFileSnapshot().isModified(packFile)) {
-				forReuse.remove(packName);
+				forReuse.remove(packFile.getName());
 				list.add(oldPack);
 				continue;
 			}
 
-			list.add(new Pack(packFile, extensions));
+			list.add(new Pack(packFile, packFilesByExt.get(BITMAP_INDEX)));
 			foundNew = true;
 		}
 
@@ -487,18 +491,42 @@
 		return forReuse;
 	}
 
-	private Set<String> listPackDirectory() {
+	/**
+	 * Scans the pack directory for
+	 * {@link org.eclipse.jgit.internal.storage.file.PackFile}s and returns them
+	 * organized by their extensions and their pack ids
+	 *
+	 * Skips files in the directory that we cannot create a
+	 * {@link org.eclipse.jgit.internal.storage.file.PackFile} for.
+	 *
+	 * @return a map of {@link org.eclipse.jgit.internal.storage.file.PackFile}s
+	 *         and {@link org.eclipse.jgit.internal.storage.pack.PackExt}s keyed
+	 *         by pack ids
+	 */
+	private Map<String, Map<PackExt, PackFile>> getPackFilesByExtById() {
 		final String[] nameList = directory.list();
 		if (nameList == null) {
-			return Collections.emptySet();
+			return Collections.emptyMap();
 		}
-		final Set<String> nameSet = new HashSet<>(nameList.length << 1);
+		Map<String, Map<PackExt, PackFile>> packFilesByExtById = new HashMap<>(
+				nameList.length / 2); // assume roughly 2 files per id
 		for (String name : nameList) {
-			if (name.startsWith("pack-")) { //$NON-NLS-1$
-				nameSet.add(name);
+			try {
+				PackFile pack = new PackFile(directory, name);
+				if (pack.getPackExt() != null) {
+					Map<PackExt, PackFile> packByExt = packFilesByExtById
+							.get(pack.getId());
+					if (packByExt == null) {
+						packByExt = new EnumMap<>(PackExt.class);
+						packFilesByExtById.put(pack.getId(), packByExt);
+					}
+					packByExt.put(pack.getPackExt(), pack);
+				}
+			} catch (IllegalArgumentException e) {
+				continue;
 			}
 		}
-		return nameSet;
+		return packFilesByExtById;
 	}
 
 	static final class PackList {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java
new file mode 100644
index 0000000..19979d0
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2021 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.internal.storage.file;
+
+import java.io.File;
+import java.text.MessageFormat;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * A pack file (or pack related) File.
+ *
+ * Example: "pack-0123456789012345678901234567890123456789.idx"
+ */
+public class PackFile extends File {
+	private static final long serialVersionUID = 1L;
+
+	private static final String PREFIX = "pack-"; //$NON-NLS-1$
+
+	private final String base; // PREFIX + id i.e.
+								// pack-0123456789012345678901234567890123456789
+
+	private final String id; // i.e. 0123456789012345678901234567890123456789
+
+	private final boolean hasOldPrefix;
+
+	private final PackExt packExt;
+
+	private static String createName(String id, PackExt extension) {
+		return PREFIX + id + '.' + extension.getExtension();
+	}
+
+	/**
+	 * Create a PackFile for a pack or related file.
+	 *
+	 * @param file
+	 *            File pointing to the location of the file.
+	 */
+	public PackFile(File file) {
+		this(file.getParentFile(), file.getName());
+	}
+
+	/**
+	 * Create a PackFile for a pack or related file.
+	 *
+	 * @param directory
+	 *            Directory to create the PackFile in.
+	 * @param id
+	 *            the {@link ObjectId} for this pack
+	 * @param ext
+	 *            the <code>packExt</code> of the name.
+	 */
+	public PackFile(File directory, ObjectId id, PackExt ext) {
+		this(directory, id.name(), ext);
+	}
+
+	/**
+	 * Create a PackFile for a pack or related file.
+	 *
+	 * @param directory
+	 *            Directory to create the PackFile in.
+	 * @param id
+	 *            the <code>id</code> (40 Hex char) section of the pack name.
+	 * @param ext
+	 *            the <code>packExt</code> of the name.
+	 */
+	public PackFile(File directory, String id, PackExt ext) {
+		this(directory, createName(id, ext));
+	}
+
+	/**
+	 * Create a PackFile for a pack or related file.
+	 *
+	 * @param directory
+	 *            Directory to create the PackFile in.
+	 * @param name
+	 *            Filename (last path section) of the PackFile
+	 */
+	public PackFile(File directory, String name) {
+		super(directory, name);
+		int dot = name.lastIndexOf('.');
+
+		if (dot < 0) {
+			base = name;
+			hasOldPrefix = false;
+			packExt = null;
+		} else {
+			base = name.substring(0, dot);
+			String tail = name.substring(dot + 1); // ["old-"] + extension
+			packExt = getPackExt(tail);
+			String old = tail.substring(0,
+					tail.length() - getExtension().length());
+			hasOldPrefix = old.equals(getExtPrefix(true));
+		}
+
+		id = base.startsWith(PREFIX) ? base.substring(PREFIX.length()) : base;
+	}
+
+	/**
+	 * Getter for the field <code>id</code>.
+	 *
+	 * @return the <code>id</code> (40 Hex char) section of the name.
+	 */
+	public String getId() {
+		return id;
+	}
+
+	/**
+	 * Getter for the field <code>packExt</code>.
+	 *
+	 * @return the <code>packExt</code> of the name.
+	 */
+	public PackExt getPackExt() {
+		return packExt;
+	}
+
+	/**
+	 * Create a new similar PackFile with the given extension instead.
+	 *
+	 * @param ext
+	 *            PackExt the extension to use.
+	 * @return a PackFile instance with specified extension
+	 */
+	public PackFile create(PackExt ext) {
+		return new PackFile(getParentFile(), getName(ext));
+	}
+
+	/**
+	 * Create a new similar PackFile in the given directory.
+	 *
+	 * @param directory
+	 *            Directory to create the new PackFile in.
+	 * @return a PackFile in the given directory
+	 */
+	public PackFile createForDirectory(File directory) {
+		return new PackFile(directory, getName(false));
+	}
+
+	/**
+	 * Create a new similar preserved PackFile in the given directory.
+	 *
+	 * @param directory
+	 *            Directory to create the new PackFile in.
+	 * @return a PackFile in the given directory with "old-" prefixing the
+	 *         extension
+	 */
+	public PackFile createPreservedForDirectory(File directory) {
+		return new PackFile(directory, getName(true));
+	}
+
+	private String getName(PackExt ext) {
+		return base + '.' + getExtPrefix(hasOldPrefix) + ext.getExtension();
+	}
+
+	private String getName(boolean isPreserved) {
+		return base + '.' + getExtPrefix(isPreserved) + getExtension();
+	}
+
+	private String getExtension() {
+		return packExt == null ? "" : packExt.getExtension(); //$NON-NLS-1$
+	}
+
+	private static String getExtPrefix(boolean isPreserved) {
+		return isPreserved ? "old-" : ""; //$NON-NLS-1$ //$NON-NLS-2$
+	}
+
+	private static PackExt getPackExt(String endsWithExtension) {
+		for (PackExt ext : PackExt.values()) {
+			if (endsWithExtension.endsWith(ext.getExtension())) {
+				return ext;
+			}
+		}
+		throw new IllegalArgumentException(MessageFormat.format(
+				JGitText.get().unrecognizedPackExtension, endsWithExtension));
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java
index a27a2b0..d6209c4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInserter.java
@@ -76,6 +76,7 @@
 import org.eclipse.jgit.errors.LargeObjectException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
@@ -273,16 +274,16 @@
 		}
 
 		Collections.sort(objectList);
-		File tmpIdx = idxFor(tmpPack);
+		File tmpIdx = idxFor(tmpPack); // TODO(nasserg) Use PackFile?
 		writePackIndex(tmpIdx, packHash, objectList);
 
-		File realPack = new File(db.getPackDirectory(),
-				"pack-" + computeName(objectList).name() + ".pack"); //$NON-NLS-1$ //$NON-NLS-2$
+		PackFile realPack = new PackFile(db.getPackDirectory(),
+				computeName(objectList), PackExt.PACK);
 		db.closeAllPackHandles(realPack);
 		tmpPack.setReadOnly();
 		FileUtils.rename(tmpPack, realPack, ATOMIC_MOVE);
 
-		File realIdx = idxFor(realPack);
+		PackFile realIdx = realPack.create(PackExt.INDEX);
 		tmpIdx.setReadOnly();
 		try {
 			FileUtils.rename(tmpIdx, realIdx, ATOMIC_MOVE);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java
index bedc693..6fb775d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackExt.java
@@ -13,66 +13,26 @@
 /**
  * A pack file extension.
  */
-public class PackExt {
-	private static volatile PackExt[] VALUES = new PackExt[] {};
-
+public enum PackExt {
 	/** A pack file extension. */
-	public static final PackExt PACK = newPackExt("pack"); //$NON-NLS-1$
+	PACK("pack"), //$NON-NLS-1$
 
 	/** A pack index file extension. */
-	public static final PackExt INDEX = newPackExt("idx"); //$NON-NLS-1$
+	INDEX("idx"), //$NON-NLS-1$
 
 	/** A keep pack file extension. */
-	public static final PackExt KEEP = newPackExt("keep"); //$NON-NLS-1$
+	KEEP("keep"), //$NON-NLS-1$
 
 	/** A pack bitmap index file extension. */
-	public static final PackExt BITMAP_INDEX = newPackExt("bitmap"); //$NON-NLS-1$
+	BITMAP_INDEX("bitmap"), //$NON-NLS-1$
 
 	/** A reftable file. */
-	public static final PackExt REFTABLE = newPackExt("ref"); //$NON-NLS-1$
-
-	/**
-	 * Get all of the PackExt values.
-	 *
-	 * @return all of the PackExt values.
-	 */
-	public static PackExt[] values() {
-		return VALUES;
-	}
-
-	/**
-	 * Returns a PackExt for the file extension and registers it in the values
-	 * array.
-	 *
-	 * @param ext
-	 *            the file extension.
-	 * @return the PackExt for the ext
-	 */
-	public static synchronized PackExt newPackExt(String ext) {
-		PackExt[] dst = new PackExt[VALUES.length + 1];
-		for (int i = 0; i < VALUES.length; i++) {
-			PackExt packExt = VALUES[i];
-			if (packExt.getExtension().equals(ext))
-				return packExt;
-			dst[i] = packExt;
-		}
-		if (VALUES.length >= 32)
-			throw new IllegalStateException(
-					"maximum number of pack extensions exceeded"); //$NON-NLS-1$
-
-		PackExt value = new PackExt(ext, VALUES.length);
-		dst[VALUES.length] = value;
-		VALUES = dst;
-		return value;
-	}
+	REFTABLE("ref"); //$NON-NLS-1$
 
 	private final String ext;
 
-	private final int pos;
-
-	private PackExt(String ext, int pos) {
+	private PackExt(String ext) {
 		this.ext = ext;
-		this.pos = pos;
 	}
 
 	/**
@@ -85,12 +45,12 @@
 	}
 
 	/**
-	 * Get the position of the extension in the values array.
+	 * Get the position of the extension in the enum declaration.
 	 *
-	 * @return the position of the extension in the values array.
+	 * @return the position of the extension in the enum declaration.
 	 */
 	public int getPosition() {
-		return pos;
+		return ordinal();
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java
index f2eac8d..03ef852 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkPushConnection.java
@@ -13,6 +13,7 @@
 import static org.eclipse.jgit.transport.WalkRemoteObjectDatabase.ROOT_DIR;
 
 import java.io.BufferedOutputStream;
+import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.ArrayList;
@@ -26,6 +27,8 @@
 
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.file.PackFile;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackWriter;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
@@ -189,9 +192,8 @@
 
 	private void sendpack(final List<RemoteRefUpdate> updates,
 			final ProgressMonitor monitor) throws TransportException {
-		String pathPack = null;
-		String pathIdx = null;
-
+		PackFile pack = null;
+		PackFile idx = null;
 		try (PackWriter writer = new PackWriter(transport.getPackConfig(),
 				local.newObjectReader())) {
 
@@ -217,31 +219,33 @@
 			for (String n : dest.getPackNames())
 				packNames.put(n, n);
 
-			final String base = "pack-" + writer.computeName().name(); //$NON-NLS-1$
-			final String packName = base + ".pack"; //$NON-NLS-1$
-			pathPack = "pack/" + packName; //$NON-NLS-1$
-			pathIdx = "pack/" + base + ".idx"; //$NON-NLS-1$ //$NON-NLS-2$
+			File packDir = new File("pack"); //$NON-NLS-1$
+			pack = new PackFile(packDir, writer.computeName(),
+					PackExt.PACK);
+			idx = pack.create(PackExt.INDEX);
 
-			if (packNames.remove(packName) != null) {
+			if (packNames.remove(pack.getName()) != null) {
 				// The remote already contains this pack. We should
 				// remove the index before overwriting to prevent bad
 				// offsets from appearing to clients.
 				//
 				dest.writeInfoPacks(packNames.keySet());
-				dest.deleteFile(pathIdx);
+				dest.deleteFile(idx.getPath());
 			}
 
 			// Write the pack file, then the index, as readers look the
 			// other direction (index, then pack file).
 			//
-			String wt = "Put " + base.substring(0, 12); //$NON-NLS-1$
+			String wt = "Put " + pack.getName().substring(0, 12); //$NON-NLS-1$
 			try (OutputStream os = new BufferedOutputStream(
-					dest.writeFile(pathPack, monitor, wt + "..pack"))) { //$NON-NLS-1$
+					dest.writeFile(pack.getPath(), monitor,
+							wt + "." + pack.getPackExt().getExtension()))) { //$NON-NLS-1$
 				writer.writePack(monitor, monitor, os);
 			}
 
 			try (OutputStream os = new BufferedOutputStream(
-					dest.writeFile(pathIdx, monitor, wt + "..idx"))) { //$NON-NLS-1$
+					dest.writeFile(idx.getPath(), monitor,
+							wt + "." + idx.getPackExt().getExtension()))) { //$NON-NLS-1$
 				writer.writeIndex(os);
 			}
 
@@ -250,22 +254,22 @@
 			// and discover the most recent objects there.
 			//
 			final ArrayList<String> infoPacks = new ArrayList<>();
-			infoPacks.add(packName);
+			infoPacks.add(pack.getName());
 			infoPacks.addAll(packNames.keySet());
 			dest.writeInfoPacks(infoPacks);
 
 		} catch (IOException err) {
-			safeDelete(pathIdx);
-			safeDelete(pathPack);
+			safeDelete(idx);
+			safeDelete(pack);
 
 			throw new TransportException(uri, JGitText.get().cannotStoreObjects, err);
 		}
 	}
 
-	private void safeDelete(String path) {
+	private void safeDelete(File path) {
 		if (path != null) {
 			try {
-				dest.deleteFile(path);
+				dest.deleteFile(path.getPath());
 			} catch (IOException cleanupFailure) {
 				// Ignore the deletion failure. We probably are
 				// already failing and were just trying to pick
diff --git a/pom.xml b/pom.xml
index 4846aff..d938d1a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -202,6 +202,14 @@
       <id>repo.eclipse.org.cbi-snapshots</id>
       <url>https://repo.eclipse.org/content/repositories/cbi-snapshots/</url>
     </pluginRepository>
+    <pluginRepository>
+      <id>repo.eclipse.org.dash-releases</id>
+      <url>https://repo.eclipse.org/content/repositories/dash-licenses-releases/</url>
+    </pluginRepository>
+    <pluginRepository>
+      <id>repo.eclipse.org.dash-snapshots</id>
+      <url>https://repo.eclipse.org/content/repositories/dash-licenses-snapshots/</url>
+    </pluginRepository>
   </pluginRepositories>
 
   <build>
@@ -391,6 +399,11 @@
           <artifactId>spring-boot-maven-plugin</artifactId>
           <version>2.4.1</version>
         </plugin>
+        <plugin>
+          <groupId>org.eclipse.dash</groupId>
+          <artifactId>license-tool-plugin</artifactId>
+          <version>0.0.1-SNAPSHOT</version>
+        </plugin>
       </plugins>
     </pluginManagement>
 
@@ -549,6 +562,10 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-report-plugin</artifactId>
       </plugin>
+      <plugin>
+        <groupId>org.eclipse.dash</groupId>
+        <artifactId>license-tool-plugin</artifactId>
+      </plugin>
     </plugins>
   </build>