DfsPackFileMidx: Implement PackIndex over midx
Change-Id: Iccc59fd16be50f813cdad7d7318bc5146a6a6964
diff --git a/org.eclipse.jgit.test/src/org/eclipse/jgit/internal/storage/dfs/MidxTestUtils.java b/org.eclipse.jgit.test/src/org/eclipse/jgit/internal/storage/dfs/MidxTestUtils.java
new file mode 100644
index 0000000..e0674eb
--- /dev/null
+++ b/org.eclipse.jgit.test/src/org/eclipse/jgit/internal/storage/dfs/MidxTestUtils.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2025, Google LLC.
+ *
+ * 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.dfs;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.zip.Deflater;
+
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Helpers to write multipack indexes
+ */
+public class MidxTestUtils {
+ private MidxTestUtils() {
+ }
+
+ /**
+ * Write a single pack into the repo with the blob as contents
+ *
+ * @param db
+ * repository
+ * @param blob
+ * blob to write into the pack
+ * @return object id of the blob written in the pack
+ * @throws IOException
+ * a problem writing in the repo
+ */
+ static ObjectId writePackWithBlob(DfsRepository db, String blob)
+ throws IOException {
+ return writePackWithBlobs(db, blob)[0];
+ }
+
+ /**
+ * Write multiple blobs into a single pack in the repo
+ *
+ * @param db
+ * repository
+ * @param blobs
+ * blobs to write into the pack
+ * @return object ids of the blobs written in the pack, in the same order as
+ * the input parameters
+ * @throws IOException
+ * a problem writing in the repo
+ */
+ static ObjectId[] writePackWithBlobs(DfsRepository db, String... blobs)
+ throws IOException {
+ ObjectId[] oids = new ObjectId[blobs.length];
+
+ DfsInserter ins = (DfsInserter) db.newObjectInserter();
+ ins.setCompressionLevel(Deflater.NO_COMPRESSION);
+ for (int i = 0; i < blobs.length; i++) {
+ oids[i] = ins.insert(OBJ_BLOB, blobs[i].getBytes(UTF_8));
+ }
+ ins.flush();
+ return oids;
+ }
+
+ /**
+ * Write a midx covering the only pack in the repo
+ *
+ * @param db
+ * a repository with a single pack
+ * @return a midx covering that single pack
+ * @throws IOException
+ * a problem writing in the repo
+ */
+ static DfsPackFileMidx writeSinglePackMidx(DfsRepository db)
+ throws IOException {
+ DfsPackFile[] packs = db.getObjectDatabase().getPacks();
+ assertEquals("More than one pack in db", 1, packs.length);
+ return writeSinglePackMidx(db, packs[0]);
+ }
+
+ /**
+ * Write a midx covering a single pack
+ *
+ * @param db
+ * a repository to write the midx covering the pack
+ * @param pack
+ * a pack in the repository that will be covered by a new midx
+ * @return a midx covering that single pack
+ * @throws IOException
+ * a problem writing in the repo
+ */
+ static DfsPackFileMidx writeSinglePackMidx(DfsRepository db,
+ DfsPackFile pack) throws IOException {
+ return writeMultipackIndex(db, new DfsPackFile[] { pack }, null);
+ }
+
+ /**
+ * Write a midx in the repository
+ *
+ * @param db
+ * the repository
+ * @param packs
+ * packs to be covered by this midx
+ * @param base
+ * base of the newly created midx
+ * @return the new midx instance
+ * @throws IOException
+ * a problem writing in the repo
+ */
+ static DfsPackFileMidx writeMultipackIndex(DfsRepository db,
+ DfsPackFile[] packs, DfsPackFileMidx base) throws IOException {
+ DfsPackDescription desc = DfsMidxWriter.writeMidx(
+ NullProgressMonitor.INSTANCE, db.getObjectDatabase(),
+ Arrays.asList(packs),
+ base != null ? base.getPackDescription() : null);
+ db.getObjectDatabase().commitPack(List.of(desc), null);
+ return DfsPackFileMidx.create(DfsBlockCache.getInstance(), desc,
+ Arrays.asList(packs), base);
+ }
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileMidxIndexTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileMidxIndexTest.java
new file mode 100644
index 0000000..847d58f
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsPackFileMidxIndexTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2025, Google LLC.
+ *
+ * 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.dfs;
+
+import static org.eclipse.jgit.internal.storage.dfs.MidxTestUtils.writeMultipackIndex;
+import static org.eclipse.jgit.internal.storage.dfs.MidxTestUtils.writePackWithBlobs;
+import static org.eclipse.jgit.internal.storage.dfs.MidxTestUtils.writeSinglePackMidx;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class DfsPackFileMidxIndexTest {
+
+ private static final ObjectId NOT_IN_PACK = ObjectId
+ .fromString("3f306cb3fcd5116919fecad615524bd6e6ea4ba7");
+
+ private static final List<String> BLOBS = List.of("blob one", "blob two",
+ "blob three", "blob four", "blob five", "blob six");
+
+ @Parameters(name = "{0}")
+ public static Iterable<TestInput> data() throws IOException {
+ return List.of(setupOneMidxOverOnePack(), setupOneMidxOverNPacks(),
+ setupMidxChainEachOverNPacks());
+ }
+
+ private record TestInput(String testDesc, DfsRepository db,
+ DfsPackFileMidx midx, ObjectId[] oids) {
+ @Override
+ public String toString() {
+ return testDesc;
+ }
+
+ }
+
+ private TestInput ti;
+
+ public DfsPackFileMidxIndexTest(TestInput ti) {
+ this.ti = ti;
+ }
+
+ @Test
+ public void getPackIndex_getObjectCount() {
+ try (DfsReader ctx = ti.db().getObjectDatabase().newReader()) {
+ assertEquals(ti.oids().length,
+ ti.midx().getPackIndex(ctx).getObjectCount());
+ }
+ }
+
+ @Test
+ public void getPackIndex_position_findPosition_getObjectId() {
+ try (DfsReader ctx = ti.db.getObjectDatabase().newReader()) {
+ PackIndex idx = ti.midx().getPackIndex(ctx);
+ for (int i = 0; i < ti.oids().length; i++) {
+ ObjectId expected = ti.oids()[i];
+ int position = idx.findPosition(expected);
+ assertNotEquals(-1, position);
+ ObjectId actual = idx.getObjectId(position);
+ assertEquals(expected, actual);
+ }
+ assertEquals(-1, idx.findPosition(NOT_IN_PACK));
+ }
+ }
+
+ @Test
+ public void getPackIndex_offset_findOffset_getOffset() {
+ try (DfsReader ctx = ti.db.getObjectDatabase().newReader()) {
+ PackIndex idx = ti.midx().getPackIndex(ctx);
+ for (int i = 0; i < ti.oids().length; i++) {
+ ObjectId oid = ti.oids()[i];
+ int oidPosition = idx.findPosition(oid);
+
+ long offsetById = idx.findOffset(oid);
+ long offsetByPos = idx.getOffset(oidPosition);
+ assertEquals(offsetById, offsetByPos);
+ }
+ assertEquals(-1, idx.findOffset(NOT_IN_PACK));
+ }
+ }
+
+ @Test
+ public void getPackIndex_objects_contains_hasObjects() {
+ try (DfsReader ctx = ti.db.getObjectDatabase().newReader()) {
+ PackIndex idx = ti.midx().getPackIndex(ctx);
+ for (int i = 0; i < ti.oids().length; i++) {
+ ObjectId oid = ti.oids()[i];
+ assertTrue(idx.contains(oid));
+ assertTrue(idx.hasObject(oid));
+ }
+ assertFalse(idx.contains(NOT_IN_PACK));
+ assertFalse(idx.hasObject(NOT_IN_PACK));
+ }
+ }
+
+ @Test
+ public void getPackIndex_resolve() throws IOException {
+ try (DfsReader ctx = ti.db.getObjectDatabase().newReader()) {
+ PackIndex idx = ti.midx().getPackIndex(ctx);
+ Set<ObjectId> matches = new HashSet<>();
+ // Sha1 of "blob two" = ae4116e0972d85cd751b458fea94ca9eb84dd692
+ idx.resolve(matches, AbbreviatedObjectId.fromString("ae411"), 100);
+ assertEquals(1, matches.size());
+ }
+ }
+
+ static TestInput setupOneMidxOverOnePack() throws IOException {
+ InMemoryRepository db = new InMemoryRepository(
+ new DfsRepositoryDescription("one_midx_one_pack"));
+ ObjectId[] objectIds = writePackWithBlobs(db,
+ BLOBS.toArray(String[]::new));
+ DfsPackFileMidx midx1 = writeSinglePackMidx(db);
+ return new TestInput("one midx - one pack", db, midx1, objectIds);
+ }
+
+ static TestInput setupOneMidxOverNPacks() throws IOException {
+ InMemoryRepository db = new InMemoryRepository(
+ new DfsRepositoryDescription("one_midx_n_packs"));
+
+ ObjectId[] objectIds = BLOBS.stream().map(s -> {
+ try {
+ return MidxTestUtils.writePackWithBlob(db, s);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }).toArray(ObjectId[]::new);
+ DfsPackFileMidx midx1 = writeMultipackIndex(db,
+ db.getObjectDatabase().getPacks(), null);
+ return new TestInput("one midx - n packs", db, midx1, objectIds);
+ }
+
+ static TestInput setupMidxChainEachOverNPacks() throws IOException {
+ InMemoryRepository db = new InMemoryRepository(
+ new DfsRepositoryDescription("two_midx_3_packs_each"));
+
+ ObjectId[] objectIds = BLOBS.stream().map(s -> {
+ try {
+ return MidxTestUtils.writePackWithBlob(db, s);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }).toArray(ObjectId[]::new);
+ DfsPackFile[] packs = db.getObjectDatabase().getPacks();
+ // If the amount of blobs (i.e. packs), adjust the ranges covered by
+ // midx.
+ assertEquals(6, BLOBS.size());
+ DfsPackFileMidx midxBase = MidxTestUtils.writeMultipackIndex(db,
+ Arrays.copyOfRange(packs, 3, 6), null);
+ DfsPackFileMidx midxTip = MidxTestUtils.writeMultipackIndex(db,
+ Arrays.copyOfRange(packs, 0, 3), midxBase);
+ return new TestInput("two midx - 3 packs each", db, midxTip, objectIds);
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFileMidx.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFileMidx.java
index 5f08444..ba73283 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFileMidx.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFileMidx.java
@@ -11,7 +11,9 @@
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
+import java.util.Set;
import java.util.zip.DataFormatException;
import org.eclipse.jgit.annotations.Nullable;
@@ -19,6 +21,8 @@
import org.eclipse.jgit.internal.storage.file.PackIndex;
import org.eclipse.jgit.internal.storage.file.PackReverseIndex;
import org.eclipse.jgit.internal.storage.pack.PackOutputStream;
+import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
@@ -137,9 +141,8 @@ protected int getObjectCount(DfsReader ctx) throws IOException {
}
@Override
- public PackIndex getPackIndex(DfsReader ctx) {
- throw new IllegalStateException(
- "Shouldn't use multipack index if the primary index is needed"); //$NON-NLS-1$
+ public final PackIndex getPackIndex(DfsReader ctx) {
+ return new MidxPackIndex(this, ctx);
}
@Override
@@ -354,4 +357,101 @@ long getPackOffset() {
return midxOffset - packStart;
}
}
+
+ private static class MidxPackIndex implements PackIndex {
+
+ private final DfsPackFileMidx pack;
+
+ private final DfsReader ctx;
+
+ MidxPackIndex(DfsPackFileMidx pack, DfsReader ctx) {
+ this.pack = pack;
+ this.ctx = ctx;
+ }
+
+ @Override
+ public Iterator<MutableEntry> iterator() {
+ throw new UnsupportedOperationException("Not implemented yet");
+ }
+
+ @Override
+ public long getObjectCount() {
+ try {
+ return pack.getObjectCount(ctx);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public long getOffset64Count() {
+ // TODO(ifrade): This method seems to be used only for stats.
+ // Maybe we can just remove it.
+ return 0;
+ }
+
+ @Override
+ public ObjectId getObjectId(long nthPosition) {
+ try {
+ return pack.getObjectAt(ctx, nthPosition);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public long getOffset(long nthPosition) {
+ ObjectId objectAt;
+ try {
+ objectAt = pack.getObjectAt(ctx, nthPosition);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ if (objectAt == null) {
+ return -1;
+ }
+
+ return findOffset(objectAt);
+ }
+
+ @Override
+ public long findOffset(AnyObjectId objId) {
+ try {
+ return pack.findOffset(ctx, objId);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public int findPosition(AnyObjectId objId) {
+ try {
+ return pack.findIdxPosition(ctx, objId);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public long findCRC32(AnyObjectId objId)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasCRC32Support() {
+ return false;
+ }
+
+ @Override
+ public void resolve(Set<ObjectId> matches, AbbreviatedObjectId id,
+ int matchLimit) throws IOException {
+ pack.resolve(ctx, matches, id, matchLimit);
+ }
+
+ @Override
+ public byte[] getChecksum() {
+ throw new UnsupportedOperationException();
+ }
+ }
}