Merge "MidxPackList: Add a builder to edit the pack list"
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/MidxPackListTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/MidxPackListTest.java
index 217f587..beaef58 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/MidxPackListTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/MidxPackListTest.java
@@ -311,6 +311,168 @@ public void getPlainPacksNotCoveredBy_midxChain() {
 		assertEquals(List.of(), packList.getPlainPacksNotCoveredBy(midxTip));
 	}
 
+	@Test
+	public void builder_delete_onlyPlain() {
+		DfsPackFile a = packPool.pack("a");
+		DfsPackFile b = packPool.pack("b");
+		DfsPackFile c = packPool.pack("c");
+
+		MidxPackList packList = MidxPackList.create(a, b, c).edit().delete(b)
+				.build();
+		assertEquals(List.of(a, c), packList.getAllPlainPacks());
+	}
+
+	@Test
+	public void builder_delete_oneMidxCovering() {
+		DfsPackFile a = packPool.pack("a");
+		DfsPackFile b = packPool.pack("b");
+		DfsPackFile c = packPool.pack("c");
+		DfsPackFileMidx midx = packPool.midx("midx", null, "a", "b", "c");
+
+		MidxPackList packList = MidxPackList.create(midx).edit().delete(b)
+				.build();
+		assertEquals(List.of(c, a), packList.getAllPlainPacks());
+		assertEquals(0, packList.getAllMidxPacks().size());
+	}
+
+	@Test
+	public void builder_delete_singlePackMidx() {
+		DfsPackFile a = packPool.pack("a");
+		DfsPackFileMidx midx = packPool.midx("midx", null, "a");
+
+		MidxPackList packList = MidxPackList.create(midx).edit().delete(a)
+				.build();
+		assertEquals(List.of(), packList.getAllPlainPacks());
+		assertEquals(0, packList.getAllMidxPacks().size());
+	}
+
+	@Test
+	public void builder_delete_midxPlusOne_deleteUncovered() {
+		DfsPackFile a = packPool.pack("a");
+		DfsPackFile b = packPool.pack("b");
+		DfsPackFile c = packPool.pack("c");
+		DfsPackFileMidx midx = packPool.midx("midx", null, "a", "b", "c");
+		DfsPackFile d = packPool.pack("d");
+
+		MidxPackList deleteD = MidxPackList.create(d, midx).edit().delete(d)
+				.build();
+		assertEquals(List.of(c, b, a), deleteD.getAllPlainPacks());
+		assertEquals(midx, deleteD.getTopMidxPack());
+	}
+
+	@Test
+	public void builder_delete_midxPlusOne_deleteCovered() {
+		DfsPackFile a = packPool.pack("a");
+		DfsPackFile b = packPool.pack("b");
+		DfsPackFile c = packPool.pack("c");
+		DfsPackFileMidx midx = packPool.midx("midx", null, "a", "b", "c");
+		DfsPackFile d = packPool.pack("d");
+
+		MidxPackList deleteB = MidxPackList.create(d, midx).edit().delete(b)
+				.build();
+		assertEquals(List.of(d, c, a), deleteB.getAllPlainPacks());
+		assertEquals(0, deleteB.getAllMidxPacks().size());
+	}
+
+	@Test
+	public void builder_delete_nestedMidx() {
+		DfsPackFile a = packPool.pack("a");
+		DfsPackFile b = packPool.pack("b");
+		DfsPackFileMidx midxBase = packPool.midx("midxBase", null, "a", "b");
+		DfsPackFile c = packPool.pack("c");
+		DfsPackFile d = packPool.pack("d");
+		DfsPackFileMidx midxMiddle = packPool.midx("midxMiddle", midxBase, "c",
+				"d");
+		DfsPackFile e = packPool.pack("e");
+		DfsPackFile f = packPool.pack("f");
+		DfsPackFileMidx midxTip = packPool.midx("midxTip", midxMiddle, "e",
+				"f");
+
+		MidxPackList withoutF = MidxPackList.create(midxTip).edit().delete(f)
+				.build();
+		assertEquals(List.of(e, d, c, b, a), withoutF.getAllPlainPacks());
+		assertEquals(List.of(midxMiddle, midxBase), withoutF.getAllMidxPacks());
+		assertEquals(midxMiddle, withoutF.getTopMidxPack());
+
+		MidxPackList withoutE = MidxPackList.create(midxTip).edit().delete(e)
+				.build();
+		assertEquals(List.of(f, d, c, b, a), withoutE.getAllPlainPacks());
+		assertEquals(List.of(midxMiddle, midxBase), withoutE.getAllMidxPacks());
+		assertEquals(midxMiddle, withoutE.getTopMidxPack());
+		assertEquals(Set.of(), withoutE.findAllCoveringMidxs(f));
+		assertEquals(Set.of(midxMiddle, midxBase),
+				withoutE.findAllCoveringMidxs(a));
+
+		MidxPackList withoutD = MidxPackList.create(midxTip).edit().delete(d)
+				.build();
+		assertEquals(List.of(f, e, c, b, a), withoutD.getAllPlainPacks());
+		assertEquals(List.of(midxBase), withoutD.getAllMidxPacks());
+		assertEquals(midxBase, withoutD.getTopMidxPack());
+
+		MidxPackList withoutC = MidxPackList.create(midxTip).edit().delete(c)
+				.build();
+		assertEquals(List.of(f, e, d, b, a), withoutC.getAllPlainPacks());
+		assertEquals(List.of(midxBase), withoutC.getAllMidxPacks());
+		assertEquals(midxBase, withoutC.getTopMidxPack());
+		assertEquals(Set.of(), withoutC.findAllCoveringMidxs(f, e, d, c));
+		assertEquals(Set.of(midxBase), withoutC.findAllCoveringMidxs(a));
+
+		MidxPackList withoutB = MidxPackList.create(midxTip).edit().delete(b)
+				.build();
+		assertEquals(List.of(f, e, d, c, a), withoutB.getAllPlainPacks());
+		assertEquals(List.of(), withoutB.getAllMidxPacks());
+		assertEquals(null, withoutB.getTopMidxPack());
+
+		MidxPackList withoutA = MidxPackList.create(midxTip).edit().delete(a)
+				.build();
+		assertEquals(List.of(f, e, d, c, b), withoutA.getAllPlainPacks());
+		assertEquals(List.of(), withoutA.getAllMidxPacks());
+		assertEquals(null, withoutA.getTopMidxPack());
+		assertEquals(Set.of(), withoutA.findAllCoveringMidxs(f, e, d, c, b));
+	}
+
+	@Test
+	public void builder_delete_nestedMidx_multipleDeletes() throws Exception {
+		DfsPackFile a = packPool.pack("a");
+		DfsPackFile b = packPool.pack("b");
+		DfsPackFileMidx midxBase = packPool.midx("midxBase", null, "a", "b");
+		DfsPackFile c = packPool.pack("c");
+		DfsPackFile d = packPool.pack("d");
+		DfsPackFileMidx midxMiddle = packPool.midx("midxMiddle", midxBase, "c",
+				"d");
+		DfsPackFile e = packPool.pack("e");
+		DfsPackFile f = packPool.pack("f");
+		DfsPackFileMidx midxTip = packPool.midx("midxTip", midxMiddle, "e",
+				"f");
+
+		{
+			MidxPackList deleteFromTopAndMiddle = MidxPackList.create(midxTip)
+					.edit().delete(e).delete(d).build();
+			assertEquals(List.of(f, c, b, a),
+					deleteFromTopAndMiddle.getAllPlainPacks());
+			assertEquals(List.of(midxBase),
+					deleteFromTopAndMiddle.getAllMidxPacks());
+			assertEquals(midxBase, deleteFromTopAndMiddle.getTopMidxPack());
+		}
+		{
+			MidxPackList deleteFromMiddleAndTop = MidxPackList.create(midxTip)
+					.edit().delete(d).delete(e).build();
+			assertEquals(List.of(f, c, b, a),
+					deleteFromMiddleAndTop.getAllPlainPacks());
+			assertEquals(List.of(midxBase),
+					deleteFromMiddleAndTop.getAllMidxPacks());
+			assertEquals(midxBase, deleteFromMiddleAndTop.getTopMidxPack());
+		}
+		{
+			MidxPackList deleteAllFromMidx = MidxPackList.create(midxTip).edit()
+					.delete(d).delete(c).build();
+			assertEquals(List.of(f, e, b, a),
+					deleteAllFromMidx.getAllPlainPacks());
+			assertEquals(List.of(midxBase),
+					deleteAllFromMidx.getAllMidxPacks());
+			assertEquals(midxBase, deleteAllFromMidx.getTopMidxPack());
+		}
+	}
 
 	private static final class PackPool {
 		private static final DfsRepositoryDescription repoDesc = new DfsRepositoryDescription(
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/MidxPackList.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/MidxPackList.java
index 70c9b85..d60ca1e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/MidxPackList.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/MidxPackList.java
@@ -21,6 +21,7 @@
 import java.util.Queue;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.stream.Collectors;
 
 import org.eclipse.jgit.annotations.Nullable;
 
@@ -209,6 +210,151 @@ public List<DfsPackFile> getPlainPacksNotCoveredBy(
 				.toList();
 	}
 
+	/**
+	 * Get a builder to add/remove packs from the list
+	 *
+	 * @return the builder
+	 */
+	public Builder edit() {
+		return new Builder(packs);
+	}
+
+	/**
+	 * Builder to mutate the list of packs.
+	 *
+	 * It removes midxs from the list as packs are removed, to maintain a valid
+	 * view of the packlist
+	 */
+	public final static class Builder {
+
+		private final List<DfsPackFile> packList;
+
+		private Builder(List<DfsPackFile> packs) {
+			this.packList = new ArrayList<>(packs);
+		}
+
+		/**
+		 * Add pack to the packlist
+		 *
+		 * @param toAdd
+		 *            pack to add
+		 * @return this builder
+		 */
+		public Builder add(DfsPackFile toAdd) {
+			packList.add(toAdd);
+			return this;
+		}
+
+		/**
+		 * Delete the pack from the pack list
+		 *
+		 * Including any midx covering it directly or indirectly
+		 *
+		 * @param toDelete
+		 *            a pack to remove from the list
+		 * @return this builder
+		 */
+		public Builder delete(DfsPackFile toDelete) {
+			// Check first top-level packs. In compact these are the most
+			// common packages being replaced.
+			DfsPackDescription toDeleteDesc = toDelete.getPackDescription();
+			for (int i = 0; i < packList.size(); i++) {
+				if (packList.get(i).getPackDescription().equals(toDeleteDesc)) {
+					packList.remove(i);
+					return this;
+				}
+			}
+
+			// If the pack is not "top-level", it may be covered by midx.
+			// We need to remove the pack, its covering midxs, but keep the
+			// other covered packs in their place.
+			// e.g. in the chain midx3(E, F) -> midx2 (C, D), -> midx(A, B),
+			// deleting C from midx3 produces: [F, E, D, midx(A, B)]
+			for (int i = packList.size() - 1; i >= 0; i--) {
+				if (!packList.get(i).getPackDescription()
+						.hasFileExt(MULTI_PACK_INDEX)) {
+					continue;
+				}
+
+				DfsPackFileMidx midx = (DfsPackFileMidx) packList.get(i);
+				if (!containsAny(midx.getAllCoveredPacks(),
+						List.of(toDelete))) {
+					continue;
+				}
+
+				List<DfsPackFile> replacement = getPacksToReplaceMidx(midx,
+						toDelete);
+				if (replacement != null) {
+					packList.remove(i);
+					if (!replacement.isEmpty()) {
+						packList.addAll(i, replacement);
+					}
+				}
+			}
+
+			return this;
+		}
+
+		/**
+		 * Return the list of packs that replace a midx if we remove a pack
+		 *
+		 * When the pack is removed, any midx covering it (and above in the
+		 * chain) is invalid. Each invalid midx is replaced with its covered
+		 * packs (excluding the deleted pack)
+		 *
+		 * @param midx
+		 *            the midx
+		 * @param toDelete
+		 *            the pack to remove
+		 * @return the resulting list of packs after removing the pack. It can
+		 *         be null (nothing to change), empty (replace with nothing,
+		 *         i.e. delete the midx) or a list of packs
+		 */
+		@Nullable
+		private List<DfsPackFile> getPacksToReplaceMidx(DfsPackFileMidx midx,
+				DfsPackFile toDelete) {
+			List<DfsPackFile> result = new ArrayList<>();
+			DfsPackFileMidx current = midx;
+			while (current != null) {
+				if (containsAny(current.getCoveredPacks(), List.of(toDelete))) {
+					List<DfsPackFile> otherCovered = current.getCoveredPacks()
+							.stream()
+							.filter(coveredPack -> !coveredPack
+									.getPackDescription()
+									.equals(toDelete.getPackDescription()))
+							.collect(Collectors.toCollection(ArrayList::new));
+					Collections.reverse(otherCovered);
+					result.addAll(otherCovered);
+					if (current.getMultipackIndexBase() != null) {
+						result.add(current.getMultipackIndexBase());
+					}
+					return result;
+				}
+
+				// The deleted pack must be in the base of this midx, and it
+				// wil invalidate this one. Add all its covered packs to the
+				// result.
+				List<DfsPackFile> tmp = new ArrayList<>(
+						current.getCoveredPacks());
+				Collections.reverse(tmp);
+				result.addAll(tmp);
+				current = current.getMultipackIndexBase();
+			}
+
+			// We didn't find the pack to delete in this midx chain
+			return null;
+		}
+
+		/**
+		 * Create a new pack list with the modifications
+		 *
+		 * @return a new packlist with the changes applied
+		 */
+		public MidxPackList build() {
+			return new MidxPackList(packList);
+		}
+	}
+
 	private static boolean containsAny(List<DfsPackFile> inMidx,
 			List<DfsPackFile> queryPacks) {
 		Set<DfsPackFile> inMidxSet = asSet(inMidx);