View Javadoc
1   /*
2    * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.internal.storage.file;
12  
13  import static org.eclipse.jgit.internal.storage.pack.PackWriter.NONE;
14  import static org.eclipse.jgit.lib.Constants.INFO_ALTERNATES;
15  import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
16  import static org.junit.Assert.assertEquals;
17  import static org.junit.Assert.assertFalse;
18  import static org.junit.Assert.assertNotNull;
19  import static org.junit.Assert.assertTrue;
20  import static org.junit.Assert.fail;
21  
22  import java.io.ByteArrayInputStream;
23  import java.io.ByteArrayOutputStream;
24  import java.io.File;
25  import java.io.FileOutputStream;
26  import java.io.IOException;
27  import java.text.ParseException;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.Collections;
31  import java.util.HashSet;
32  import java.util.List;
33  import java.util.Set;
34  
35  import org.eclipse.jgit.errors.MissingObjectException;
36  import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
37  import org.eclipse.jgit.internal.storage.pack.PackWriter;
38  import org.eclipse.jgit.junit.JGitTestUtil;
39  import org.eclipse.jgit.junit.TestRepository;
40  import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
41  import org.eclipse.jgit.lib.NullProgressMonitor;
42  import org.eclipse.jgit.lib.ObjectId;
43  import org.eclipse.jgit.lib.ObjectIdSet;
44  import org.eclipse.jgit.lib.ObjectInserter;
45  import org.eclipse.jgit.lib.Repository;
46  import org.eclipse.jgit.lib.Sets;
47  import org.eclipse.jgit.revwalk.DepthWalk;
48  import org.eclipse.jgit.revwalk.ObjectWalk;
49  import org.eclipse.jgit.revwalk.RevBlob;
50  import org.eclipse.jgit.revwalk.RevCommit;
51  import org.eclipse.jgit.revwalk.RevObject;
52  import org.eclipse.jgit.revwalk.RevWalk;
53  import org.eclipse.jgit.storage.pack.PackConfig;
54  import org.eclipse.jgit.storage.pack.PackStatistics;
55  import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
56  import org.eclipse.jgit.transport.PackParser;
57  import org.junit.After;
58  import org.junit.Before;
59  import org.junit.Test;
60  
61  public class PackWriterTest extends SampleDataRepositoryTestCase {
62  
63  	private static final List<RevObject> EMPTY_LIST_REVS = Collections
64  			.<RevObject> emptyList();
65  
66  	private static final Set<ObjectIdSet> EMPTY_ID_SET = Collections
67  			.<ObjectIdSet> emptySet();
68  
69  	private PackConfig config;
70  
71  	private PackWriter writer;
72  
73  	private ByteArrayOutputStream os;
74  
75  	private PackFile pack;
76  
77  	private ObjectInserter inserter;
78  
79  	private FileRepository dst;
80  
81  	private RevBlob contentA;
82  
83  	private RevBlob contentB;
84  
85  	private RevBlob contentC;
86  
87  	private RevBlob contentD;
88  
89  	private RevBlob contentE;
90  
91  	private RevCommit c1;
92  
93  	private RevCommit c2;
94  
95  	private RevCommit c3;
96  
97  	private RevCommit c4;
98  
99  	private RevCommit c5;
100 
101 	@Override
102 	@Before
103 	public void setUp() throws Exception {
104 		super.setUp();
105 		os = new ByteArrayOutputStream();
106 		config = new PackConfig(db);
107 
108 		dst = createBareRepository();
109 		File alt = new File(dst.getObjectDatabase().getDirectory(), INFO_ALTERNATES);
110 		alt.getParentFile().mkdirs();
111 		write(alt, db.getObjectDatabase().getDirectory().getAbsolutePath() + "\n");
112 	}
113 
114 	@Override
115 	@After
116 	public void tearDown() throws Exception {
117 		if (writer != null) {
118 			writer.close();
119 			writer = null;
120 		}
121 		if (inserter != null) {
122 			inserter.close();
123 			inserter = null;
124 		}
125 		super.tearDown();
126 	}
127 
128 	/**
129 	 * Test constructor for exceptions, default settings, initialization.
130 	 *
131 	 * @throws IOException
132 	 */
133 	@Test
134 	public void testContructor() throws IOException {
135 		writer = new PackWriter(config, db.newObjectReader());
136 		assertFalse(writer.isDeltaBaseAsOffset());
137 		assertTrue(config.isReuseDeltas());
138 		assertTrue(config.isReuseObjects());
139 		assertEquals(0, writer.getObjectCount());
140 	}
141 
142 	/**
143 	 * Change default settings and verify them.
144 	 */
145 	@Test
146 	public void testModifySettings() {
147 		config.setReuseDeltas(false);
148 		config.setReuseObjects(false);
149 		config.setDeltaBaseAsOffset(false);
150 		assertFalse(config.isReuseDeltas());
151 		assertFalse(config.isReuseObjects());
152 		assertFalse(config.isDeltaBaseAsOffset());
153 
154 		writer = new PackWriter(config, db.newObjectReader());
155 		writer.setDeltaBaseAsOffset(true);
156 		assertTrue(writer.isDeltaBaseAsOffset());
157 		assertFalse(config.isDeltaBaseAsOffset());
158 	}
159 
160 	/**
161 	 * Write empty pack by providing empty sets of interesting/uninteresting
162 	 * objects and check for correct format.
163 	 *
164 	 * @throws IOException
165 	 */
166 	@Test
167 	public void testWriteEmptyPack1() throws IOException {
168 		createVerifyOpenPack(NONE, NONE, false, false);
169 
170 		assertEquals(0, writer.getObjectCount());
171 		assertEquals(0, pack.getObjectCount());
172 		assertEquals("da39a3ee5e6b4b0d3255bfef95601890afd80709", writer
173 				.computeName().name());
174 	}
175 
176 	/**
177 	 * Write empty pack by providing empty iterator of objects to write and
178 	 * check for correct format.
179 	 *
180 	 * @throws IOException
181 	 */
182 	@Test
183 	public void testWriteEmptyPack2() throws IOException {
184 		createVerifyOpenPack(EMPTY_LIST_REVS);
185 
186 		assertEquals(0, writer.getObjectCount());
187 		assertEquals(0, pack.getObjectCount());
188 	}
189 
190 	/**
191 	 * Try to pass non-existing object as uninteresting, with non-ignoring
192 	 * setting.
193 	 *
194 	 * @throws IOException
195 	 */
196 	@Test
197 	public void testNotIgnoreNonExistingObjects() throws IOException {
198 		final ObjectId nonExisting = ObjectId
199 				.fromString("0000000000000000000000000000000000000001");
200 		try {
201 			createVerifyOpenPack(NONE, haves(nonExisting), false, false);
202 			fail("Should have thrown MissingObjectException");
203 		} catch (MissingObjectException x) {
204 			// expected
205 		}
206 	}
207 
208 	/**
209 	 * Try to pass non-existing object as uninteresting, with ignoring setting.
210 	 *
211 	 * @throws IOException
212 	 */
213 	@Test
214 	public void testIgnoreNonExistingObjects() throws IOException {
215 		final ObjectId nonExisting = ObjectId
216 				.fromString("0000000000000000000000000000000000000001");
217 		createVerifyOpenPack(NONE, haves(nonExisting), false, true);
218 		// shouldn't throw anything
219 	}
220 
221 	/**
222 	 * Try to pass non-existing object as uninteresting, with ignoring setting.
223 	 * Use a repo with bitmap indexes because then PackWriter will use
224 	 * PackWriterBitmapWalker which had problems with this situation.
225 	 *
226 	 * @throws IOException
227 	 * @throws ParseException
228 	 */
229 	@Test
230 	public void testIgnoreNonExistingObjectsWithBitmaps() throws IOException,
231 			ParseException {
232 		final ObjectId nonExisting = ObjectId
233 				.fromString("0000000000000000000000000000000000000001");
234 		new GC(db).gc();
235 		createVerifyOpenPack(NONE, haves(nonExisting), false, true, true);
236 		// shouldn't throw anything
237 	}
238 
239 	/**
240 	 * Create pack basing on only interesting objects, then precisely verify
241 	 * content. No delta reuse here.
242 	 *
243 	 * @throws IOException
244 	 */
245 	@Test
246 	public void testWritePack1() throws IOException {
247 		config.setReuseDeltas(false);
248 		writeVerifyPack1();
249 	}
250 
251 	/**
252 	 * Test writing pack without object reuse. Pack content/preparation as in
253 	 * {@link #testWritePack1()}.
254 	 *
255 	 * @throws IOException
256 	 */
257 	@Test
258 	public void testWritePack1NoObjectReuse() throws IOException {
259 		config.setReuseDeltas(false);
260 		config.setReuseObjects(false);
261 		writeVerifyPack1();
262 	}
263 
264 	/**
265 	 * Create pack basing on both interesting and uninteresting objects, then
266 	 * precisely verify content. No delta reuse here.
267 	 *
268 	 * @throws IOException
269 	 */
270 	@Test
271 	public void testWritePack2() throws IOException {
272 		writeVerifyPack2(false);
273 	}
274 
275 	/**
276 	 * Test pack writing with deltas reuse, delta-base first rule. Pack
277 	 * content/preparation as in {@link #testWritePack2()}.
278 	 *
279 	 * @throws IOException
280 	 */
281 	@Test
282 	public void testWritePack2DeltasReuseRefs() throws IOException {
283 		writeVerifyPack2(true);
284 	}
285 
286 	/**
287 	 * Test pack writing with delta reuse. Delta bases referred as offsets. Pack
288 	 * configuration as in {@link #testWritePack2DeltasReuseRefs()}.
289 	 *
290 	 * @throws IOException
291 	 */
292 	@Test
293 	public void testWritePack2DeltasReuseOffsets() throws IOException {
294 		config.setDeltaBaseAsOffset(true);
295 		writeVerifyPack2(true);
296 	}
297 
298 	/**
299 	 * Test pack writing with delta reuse. Raw-data copy (reuse) is made on a
300 	 * pack with CRC32 index. Pack configuration as in
301 	 * {@link #testWritePack2DeltasReuseRefs()}.
302 	 *
303 	 * @throws IOException
304 	 */
305 	@Test
306 	public void testWritePack2DeltasCRC32Copy() throws IOException {
307 		final File packDir = db.getObjectDatabase().getPackDirectory();
308 		final File crc32Pack = new File(packDir,
309 				"pack-34be9032ac282b11fa9babdc2b2a93ca996c9c2f.pack");
310 		final File crc32Idx = new File(packDir,
311 				"pack-34be9032ac282b11fa9babdc2b2a93ca996c9c2f.idx");
312 		copyFile(JGitTestUtil.getTestResourceFile(
313 				"pack-34be9032ac282b11fa9babdc2b2a93ca996c9c2f.idxV2"),
314 				crc32Idx);
315 		db.openPack(crc32Pack);
316 
317 		writeVerifyPack2(true);
318 	}
319 
320 	/**
321 	 * Create pack basing on fixed objects list, then precisely verify content.
322 	 * No delta reuse here.
323 	 *
324 	 * @throws IOException
325 	 * @throws MissingObjectException
326 	 *
327 	 */
328 	@Test
329 	public void testWritePack3() throws MissingObjectException, IOException {
330 		config.setReuseDeltas(false);
331 		final ObjectId forcedOrder[] = new ObjectId[] {
332 				ObjectId.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"),
333 				ObjectId.fromString("c59759f143fb1fe21c197981df75a7ee00290799"),
334 				ObjectId.fromString("aabf2ffaec9b497f0950352b3e582d73035c2035"),
335 				ObjectId.fromString("902d5476fa249b7abc9d84c611577a81381f0327"),
336 				ObjectId.fromString("6ff87c4664981e4397625791c8ea3bbb5f2279a3") ,
337 				ObjectId.fromString("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259") };
338 		try (RevWalk parser = new RevWalk(db)) {
339 			final RevObject forcedOrderRevs[] = new RevObject[forcedOrder.length];
340 			for (int i = 0; i < forcedOrder.length; i++)
341 				forcedOrderRevs[i] = parser.parseAny(forcedOrder[i]);
342 
343 			createVerifyOpenPack(Arrays.asList(forcedOrderRevs));
344 		}
345 
346 		assertEquals(forcedOrder.length, writer.getObjectCount());
347 		verifyObjectsOrder(forcedOrder);
348 		assertEquals("ed3f96b8327c7c66b0f8f70056129f0769323d86", writer
349 				.computeName().name());
350 	}
351 
352 	/**
353 	 * Another pack creation: basing on both interesting and uninteresting
354 	 * objects. No delta reuse possible here, as this is a specific case when we
355 	 * write only 1 commit, associated with 1 tree, 1 blob.
356 	 *
357 	 * @throws IOException
358 	 */
359 	@Test
360 	public void testWritePack4() throws IOException {
361 		writeVerifyPack4(false);
362 	}
363 
364 	/**
365 	 * Test thin pack writing: 1 blob delta base is on objects edge. Pack
366 	 * configuration as in {@link #testWritePack4()}.
367 	 *
368 	 * @throws IOException
369 	 */
370 	@Test
371 	public void testWritePack4ThinPack() throws IOException {
372 		writeVerifyPack4(true);
373 	}
374 
375 	/**
376 	 * Compare sizes of packs created using {@link #testWritePack2()} and
377 	 * {@link #testWritePack2DeltasReuseRefs()}. The pack using deltas should
378 	 * be smaller.
379 	 *
380 	 * @throws Exception
381 	 */
382 	@Test
383 	public void testWritePack2SizeDeltasVsNoDeltas() throws Exception {
384 		config.setReuseDeltas(false);
385 		config.setDeltaCompress(false);
386 		testWritePack2();
387 		final long sizePack2NoDeltas = os.size();
388 		tearDown();
389 		setUp();
390 		testWritePack2DeltasReuseRefs();
391 		final long sizePack2DeltasRefs = os.size();
392 
393 		assertTrue(sizePack2NoDeltas > sizePack2DeltasRefs);
394 	}
395 
396 	/**
397 	 * Compare sizes of packs created using
398 	 * {@link #testWritePack2DeltasReuseRefs()} and
399 	 * {@link #testWritePack2DeltasReuseOffsets()}. The pack with delta bases
400 	 * written as offsets should be smaller.
401 	 *
402 	 * @throws Exception
403 	 */
404 	@Test
405 	public void testWritePack2SizeOffsetsVsRefs() throws Exception {
406 		testWritePack2DeltasReuseRefs();
407 		final long sizePack2DeltasRefs = os.size();
408 		tearDown();
409 		setUp();
410 		testWritePack2DeltasReuseOffsets();
411 		final long sizePack2DeltasOffsets = os.size();
412 
413 		assertTrue(sizePack2DeltasRefs > sizePack2DeltasOffsets);
414 	}
415 
416 	/**
417 	 * Compare sizes of packs created using {@link #testWritePack4()} and
418 	 * {@link #testWritePack4ThinPack()}. Obviously, the thin pack should be
419 	 * smaller.
420 	 *
421 	 * @throws Exception
422 	 */
423 	@Test
424 	public void testWritePack4SizeThinVsNoThin() throws Exception {
425 		testWritePack4();
426 		final long sizePack4 = os.size();
427 		tearDown();
428 		setUp();
429 		testWritePack4ThinPack();
430 		final long sizePack4Thin = os.size();
431 
432 		assertTrue(sizePack4 > sizePack4Thin);
433 	}
434 
435 	@Test
436 	public void testDeltaStatistics() throws Exception {
437 		config.setDeltaCompress(true);
438 		// TestRepository will close repo
439 		FileRepository repo = createBareRepository();
440 		ArrayList<RevObject> blobs = new ArrayList<>();
441 		try (TestRepository<FileRepository> testRepo = new TestRepository<>(
442 				repo)) {
443 			blobs.add(testRepo.blob(genDeltableData(1000)));
444 			blobs.add(testRepo.blob(genDeltableData(1005)));
445 			try (PackWriter pw = new PackWriter(repo)) {
446 				NullProgressMonitor m = NullProgressMonitor.INSTANCE;
447 				pw.preparePack(blobs.iterator());
448 				pw.writePack(m, m, os);
449 				PackStatistics stats = pw.getStatistics();
450 				assertEquals(1, stats.getTotalDeltas());
451 				assertTrue("Delta bytes not set.",
452 						stats.byObjectType(OBJ_BLOB).getDeltaBytes() > 0);
453 			}
454 		}
455 	}
456 
457 	// Generate consistent junk data for building files that delta well
458 	private String genDeltableData(int length) {
459 		assertTrue("Generated data must have a length > 0", length > 0);
460 		char[] data = {'a', 'b', 'c', '\n'};
461 		StringBuilder builder = new StringBuilder(length);
462 		for (int i = 0; i < length; i++) {
463 			builder.append(data[i % 4]);
464 		}
465 		return builder.toString();
466 	}
467 
468 
469 	@Test
470 	public void testWriteIndex() throws Exception {
471 		config.setIndexVersion(2);
472 		writeVerifyPack4(false);
473 
474 		File packFile = pack.getPackFile();
475 		String name = packFile.getName();
476 		String base = name.substring(0, name.lastIndexOf('.'));
477 		File indexFile = new File(packFile.getParentFile(), base + ".idx");
478 
479 		// Validate that IndexPack came up with the right CRC32 value.
480 		final PackIndex idx1 = PackIndex.open(indexFile);
481 		assertTrue(idx1 instanceof PackIndexV2);
482 		assertEquals(0x4743F1E4L, idx1.findCRC32(ObjectId
483 				.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7")));
484 
485 		// Validate that an index written by PackWriter is the same.
486 		final File idx2File = new File(indexFile.getAbsolutePath() + ".2");
487 		try (FileOutputStream is = new FileOutputStream(idx2File)) {
488 			writer.writeIndex(is);
489 		}
490 		final PackIndex idx2 = PackIndex.open(idx2File);
491 		assertTrue(idx2 instanceof PackIndexV2);
492 		assertEquals(idx1.getObjectCount(), idx2.getObjectCount());
493 		assertEquals(idx1.getOffset64Count(), idx2.getOffset64Count());
494 
495 		for (int i = 0; i < idx1.getObjectCount(); i++) {
496 			final ObjectId id = idx1.getObjectId(i);
497 			assertEquals(id, idx2.getObjectId(i));
498 			assertEquals(idx1.findOffset(id), idx2.findOffset(id));
499 			assertEquals(idx1.findCRC32(id), idx2.findCRC32(id));
500 		}
501 	}
502 
503 	@Test
504 	public void testExclude() throws Exception {
505 		// TestRepository closes repo
506 		FileRepository repo = createBareRepository();
507 
508 		try (TestRepository<FileRepository> testRepo = new TestRepository<>(
509 				repo)) {
510 			BranchBuilder bb = testRepo.branch("refs/heads/master");
511 			contentA = testRepo.blob("A");
512 			c1 = bb.commit().add("f", contentA).create();
513 			testRepo.getRevWalk().parseHeaders(c1);
514 			PackIndex pf1 = writePack(repo, wants(c1), EMPTY_ID_SET);
515 			assertContent(pf1, Arrays.asList(c1.getId(), c1.getTree().getId(),
516 					contentA.getId()));
517 			contentB = testRepo.blob("B");
518 			c2 = bb.commit().add("f", contentB).create();
519 			testRepo.getRevWalk().parseHeaders(c2);
520 			PackIndex pf2 = writePack(repo, wants(c2),
521 					Sets.of((ObjectIdSet) pf1));
522 			assertContent(pf2, Arrays.asList(c2.getId(), c2.getTree().getId(),
523 					contentB.getId()));
524 		}
525 	}
526 
527 	private static void assertContent(PackIndex pi, List<ObjectId> expected) {
528 		assertEquals("Pack index has wrong size.", expected.size(),
529 				pi.getObjectCount());
530 		for (int i = 0; i < pi.getObjectCount(); i++)
531 			assertTrue(
532 					"Pack index didn't contain the expected id "
533 							+ pi.getObjectId(i),
534 					expected.contains(pi.getObjectId(i)));
535 	}
536 
537 	@Test
538 	public void testShallowIsMinimalDepth1() throws Exception {
539 		try (FileRepository repo = setupRepoForShallowFetch()) {
540 			PackIndex idx = writeShallowPack(repo, 1, wants(c2), NONE, NONE);
541 			assertContent(idx, Arrays.asList(c2.getId(), c2.getTree().getId(),
542 					contentA.getId(), contentB.getId()));
543 
544 			// Client already has blobs A and B, verify those are not packed.
545 			idx = writeShallowPack(repo, 1, wants(c5), haves(c2), shallows(c2));
546 			assertContent(idx, Arrays.asList(c5.getId(), c5.getTree().getId(),
547 					contentC.getId(), contentD.getId(), contentE.getId()));
548 		}
549 	}
550 
551 	@Test
552 	public void testShallowIsMinimalDepth2() throws Exception {
553 		try (FileRepository repo = setupRepoForShallowFetch()) {
554 			PackIndex idx = writeShallowPack(repo, 2, wants(c2), NONE, NONE);
555 			assertContent(idx,
556 					Arrays.asList(c1.getId(), c2.getId(), c1.getTree().getId(),
557 							c2.getTree().getId(), contentA.getId(),
558 							contentB.getId()));
559 
560 			// Client already has blobs A and B, verify those are not packed.
561 			idx = writeShallowPack(repo, 2, wants(c5), haves(c1, c2),
562 					shallows(c1));
563 			assertContent(idx,
564 					Arrays.asList(c4.getId(), c5.getId(), c4.getTree().getId(),
565 							c5.getTree().getId(), contentC.getId(),
566 							contentD.getId(), contentE.getId()));
567 		}
568 	}
569 
570 	@Test
571 	public void testShallowFetchShallowParentDepth1() throws Exception {
572 		try (FileRepository repo = setupRepoForShallowFetch()) {
573 			PackIndex idx = writeShallowPack(repo, 1, wants(c5), NONE, NONE);
574 			assertContent(idx, Arrays.asList(c5.getId(), c5.getTree().getId(),
575 					contentA.getId(), contentB.getId(), contentC.getId(),
576 					contentD.getId(), contentE.getId()));
577 
578 			idx = writeShallowPack(repo, 1, wants(c4), haves(c5), shallows(c5));
579 			assertContent(idx, Arrays.asList(c4.getId(), c4.getTree().getId()));
580 		}
581 	}
582 
583 	@Test
584 	public void testShallowFetchShallowParentDepth2() throws Exception {
585 		try (FileRepository repo = setupRepoForShallowFetch()) {
586 			PackIndex idx = writeShallowPack(repo, 2, wants(c5), NONE, NONE);
587 			assertContent(idx,
588 					Arrays.asList(c4.getId(), c5.getId(), c4.getTree().getId(),
589 							c5.getTree().getId(), contentA.getId(),
590 							contentB.getId(), contentC.getId(),
591 							contentD.getId(), contentE.getId()));
592 
593 			idx = writeShallowPack(repo, 2, wants(c3), haves(c4, c5),
594 					shallows(c4));
595 			assertContent(idx, Arrays.asList(c2.getId(), c3.getId(),
596 					c2.getTree().getId(), c3.getTree().getId()));
597 		}
598 	}
599 
600 	@Test
601 	public void testShallowFetchShallowAncestorDepth1() throws Exception {
602 		try (FileRepository repo = setupRepoForShallowFetch()) {
603 			PackIndex idx = writeShallowPack(repo, 1, wants(c5), NONE, NONE);
604 			assertContent(idx, Arrays.asList(c5.getId(), c5.getTree().getId(),
605 					contentA.getId(), contentB.getId(), contentC.getId(),
606 					contentD.getId(), contentE.getId()));
607 
608 			idx = writeShallowPack(repo, 1, wants(c3), haves(c5), shallows(c5));
609 			assertContent(idx, Arrays.asList(c3.getId(), c3.getTree().getId()));
610 		}
611 	}
612 
613 	@Test
614 	public void testShallowFetchShallowAncestorDepth2() throws Exception {
615 		try (FileRepository repo = setupRepoForShallowFetch()) {
616 			PackIndex idx = writeShallowPack(repo, 2, wants(c5), NONE, NONE);
617 			assertContent(idx,
618 					Arrays.asList(c4.getId(), c5.getId(), c4.getTree().getId(),
619 							c5.getTree().getId(), contentA.getId(),
620 							contentB.getId(), contentC.getId(),
621 							contentD.getId(), contentE.getId()));
622 
623 			idx = writeShallowPack(repo, 2, wants(c2), haves(c4, c5),
624 					shallows(c4));
625 			assertContent(idx, Arrays.asList(c1.getId(), c2.getId(),
626 					c1.getTree().getId(), c2.getTree().getId()));
627 		}
628 	}
629 
630 	private FileRepository setupRepoForShallowFetch() throws Exception {
631 		FileRepository repo = createBareRepository();
632 		// TestRepository will close the repo, but we need to return an open
633 		// one!
634 		repo.incrementOpen();
635 		try (TestRepository<Repository> r = new TestRepository<>(repo)) {
636 			BranchBuilder bb = r.branch("refs/heads/master");
637 			contentA = r.blob("A");
638 			contentB = r.blob("B");
639 			contentC = r.blob("C");
640 			contentD = r.blob("D");
641 			contentE = r.blob("E");
642 			c1 = bb.commit().add("a", contentA).create();
643 			c2 = bb.commit().add("b", contentB).create();
644 			c3 = bb.commit().add("c", contentC).create();
645 			c4 = bb.commit().add("d", contentD).create();
646 			c5 = bb.commit().add("e", contentE).create();
647 			r.getRevWalk().parseHeaders(c5); // fully initialize the tip RevCommit
648 			return repo;
649 		}
650 	}
651 
652 	private static PackIndex writePack(FileRepository repo,
653 			Set<? extends ObjectId> want, Set<ObjectIdSet> excludeObjects)
654 					throws IOException {
655 		try (RevWalk walk = new RevWalk(repo)) {
656 			return writePack(repo, walk, 0, want, NONE, excludeObjects);
657 		}
658 	}
659 
660 	private static PackIndex writeShallowPack(FileRepository repo, int depth,
661 			Set<? extends ObjectId> want, Set<? extends ObjectId> have,
662 			Set<? extends ObjectId> shallow) throws IOException {
663 		// During negotiation, UploadPack would have set up a DepthWalk and
664 		// marked the client's "shallow" commits. Emulate that here.
665 		try (DepthWalk.RevWalk walk = new DepthWalk.RevWalk(repo, depth - 1)) {
666 			walk.assumeShallow(shallow);
667 			return writePack(repo, walk, depth, want, have, EMPTY_ID_SET);
668 		}
669 	}
670 
671 	private static PackIndex writePack(FileRepository repo, RevWalk walk,
672 			int depth, Set<? extends ObjectId> want,
673 			Set<? extends ObjectId> have, Set<ObjectIdSet> excludeObjects)
674 					throws IOException {
675 		try (PackWriter pw = new PackWriter(repo)) {
676 			pw.setDeltaBaseAsOffset(true);
677 			pw.setReuseDeltaCommits(false);
678 			for (ObjectIdSet idx : excludeObjects) {
679 				pw.excludeObjects(idx);
680 			}
681 			if (depth > 0) {
682 				pw.setShallowPack(depth, null);
683 			}
684 			// ow doesn't need to be closed; caller closes walk.
685 			ObjectWalk ow = walk.toObjectWalkWithSameObjects();
686 
687 			pw.preparePack(NullProgressMonitor.INSTANCE, ow, want, have, NONE);
688 			String id = pw.computeName().getName();
689 			File packdir = repo.getObjectDatabase().getPackDirectory();
690 			File packFile = new File(packdir, "pack-" + id + ".pack");
691 			try (FileOutputStream packOS = new FileOutputStream(packFile)) {
692 				pw.writePack(NullProgressMonitor.INSTANCE,
693 						NullProgressMonitor.INSTANCE, packOS);
694 			}
695 			File idxFile = new File(packdir, "pack-" + id + ".idx");
696 			try (FileOutputStream idxOS = new FileOutputStream(idxFile)) {
697 				pw.writeIndex(idxOS);
698 			}
699 			return PackIndex.open(idxFile);
700 		}
701 	}
702 
703 	// TODO: testWritePackDeltasCycle()
704 	// TODO: testWritePackDeltasDepth()
705 
706 	private void writeVerifyPack1() throws IOException {
707 		final HashSet<ObjectId> interestings = new HashSet<>();
708 		interestings.add(ObjectId
709 				.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"));
710 		createVerifyOpenPack(interestings, NONE, false, false);
711 
712 		final ObjectId expectedOrder[] = new ObjectId[] {
713 				ObjectId.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"),
714 				ObjectId.fromString("c59759f143fb1fe21c197981df75a7ee00290799"),
715 				ObjectId.fromString("540a36d136cf413e4b064c2b0e0a4db60f77feab"),
716 				ObjectId.fromString("aabf2ffaec9b497f0950352b3e582d73035c2035"),
717 				ObjectId.fromString("902d5476fa249b7abc9d84c611577a81381f0327"),
718 				ObjectId.fromString("4b825dc642cb6eb9a060e54bf8d69288fbee4904"),
719 				ObjectId.fromString("6ff87c4664981e4397625791c8ea3bbb5f2279a3"),
720 				ObjectId.fromString("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259") };
721 
722 		assertEquals(expectedOrder.length, writer.getObjectCount());
723 		verifyObjectsOrder(expectedOrder);
724 		assertEquals("34be9032ac282b11fa9babdc2b2a93ca996c9c2f", writer
725 				.computeName().name());
726 	}
727 
728 	private void writeVerifyPack2(boolean deltaReuse) throws IOException {
729 		config.setReuseDeltas(deltaReuse);
730 		final HashSet<ObjectId> interestings = new HashSet<>();
731 		interestings.add(ObjectId
732 				.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"));
733 		final HashSet<ObjectId> uninterestings = new HashSet<>();
734 		uninterestings.add(ObjectId
735 				.fromString("540a36d136cf413e4b064c2b0e0a4db60f77feab"));
736 		createVerifyOpenPack(interestings, uninterestings, false, false);
737 
738 		final ObjectId expectedOrder[] = new ObjectId[] {
739 				ObjectId.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"),
740 				ObjectId.fromString("c59759f143fb1fe21c197981df75a7ee00290799"),
741 				ObjectId.fromString("aabf2ffaec9b497f0950352b3e582d73035c2035"),
742 				ObjectId.fromString("902d5476fa249b7abc9d84c611577a81381f0327"),
743 				ObjectId.fromString("6ff87c4664981e4397625791c8ea3bbb5f2279a3") ,
744 				ObjectId.fromString("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259") };
745 		if (!config.isReuseDeltas() && !config.isDeltaCompress()) {
746 			// If no deltas are in the file the final two entries swap places.
747 			swap(expectedOrder, 4, 5);
748 		}
749 		assertEquals(expectedOrder.length, writer.getObjectCount());
750 		verifyObjectsOrder(expectedOrder);
751 		assertEquals("ed3f96b8327c7c66b0f8f70056129f0769323d86", writer
752 				.computeName().name());
753 	}
754 
755 	private static void swap(ObjectId[] arr, int a, int b) {
756 		ObjectId tmp = arr[a];
757 		arr[a] = arr[b];
758 		arr[b] = tmp;
759 	}
760 
761 	private void writeVerifyPack4(final boolean thin) throws IOException {
762 		final HashSet<ObjectId> interestings = new HashSet<>();
763 		interestings.add(ObjectId
764 				.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"));
765 		final HashSet<ObjectId> uninterestings = new HashSet<>();
766 		uninterestings.add(ObjectId
767 				.fromString("c59759f143fb1fe21c197981df75a7ee00290799"));
768 		createVerifyOpenPack(interestings, uninterestings, thin, false);
769 
770 		final ObjectId writtenObjects[] = new ObjectId[] {
771 				ObjectId.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"),
772 				ObjectId.fromString("aabf2ffaec9b497f0950352b3e582d73035c2035"),
773 				ObjectId.fromString("5b6e7c66c276e7610d4a73c70ec1a1f7c1003259") };
774 		assertEquals(writtenObjects.length, writer.getObjectCount());
775 		ObjectId expectedObjects[];
776 		if (thin) {
777 			expectedObjects = new ObjectId[4];
778 			System.arraycopy(writtenObjects, 0, expectedObjects, 0,
779 					writtenObjects.length);
780 			expectedObjects[3] = ObjectId
781 					.fromString("6ff87c4664981e4397625791c8ea3bbb5f2279a3");
782 
783 		} else {
784 			expectedObjects = writtenObjects;
785 		}
786 		verifyObjectsOrder(expectedObjects);
787 		assertEquals("cded4b74176b4456afa456768b2b5aafb41c44fc", writer
788 				.computeName().name());
789 	}
790 
791 	private void createVerifyOpenPack(final Set<ObjectId> interestings,
792 			final Set<ObjectId> uninterestings, final boolean thin,
793 			final boolean ignoreMissingUninteresting)
794 			throws MissingObjectException, IOException {
795 		createVerifyOpenPack(interestings, uninterestings, thin,
796 				ignoreMissingUninteresting, false);
797 	}
798 
799 	private void createVerifyOpenPack(final Set<ObjectId> interestings,
800 			final Set<ObjectId> uninterestings, final boolean thin,
801 			final boolean ignoreMissingUninteresting, boolean useBitmaps)
802 			throws MissingObjectException, IOException {
803 		NullProgressMonitor m = NullProgressMonitor.INSTANCE;
804 		writer = new PackWriter(config, db.newObjectReader());
805 		writer.setUseBitmaps(useBitmaps);
806 		writer.setThin(thin);
807 		writer.setIgnoreMissingUninteresting(ignoreMissingUninteresting);
808 		writer.preparePack(m, interestings, uninterestings);
809 		writer.writePack(m, m, os);
810 		writer.close();
811 		verifyOpenPack(thin);
812 	}
813 
814 	private void createVerifyOpenPack(List<RevObject> objectSource)
815 			throws MissingObjectException, IOException {
816 		NullProgressMonitor m = NullProgressMonitor.INSTANCE;
817 		writer = new PackWriter(config, db.newObjectReader());
818 		writer.preparePack(objectSource.iterator());
819 		assertEquals(objectSource.size(), writer.getObjectCount());
820 		writer.writePack(m, m, os);
821 		writer.close();
822 		verifyOpenPack(false);
823 	}
824 
825 	private void verifyOpenPack(boolean thin) throws IOException {
826 		final byte[] packData = os.toByteArray();
827 
828 		if (thin) {
829 			PackParser p = index(packData);
830 			try {
831 				p.parse(NullProgressMonitor.INSTANCE);
832 				fail("indexer should grumble about missing object");
833 			} catch (IOException x) {
834 				// expected
835 			}
836 		}
837 
838 		ObjectDirectoryPackParser p = (ObjectDirectoryPackParser) index(packData);
839 		p.setKeepEmpty(true);
840 		p.setAllowThin(thin);
841 		p.setIndexVersion(2);
842 		p.parse(NullProgressMonitor.INSTANCE);
843 		pack = p.getPackFile();
844 		assertNotNull("have PackFile after parsing", pack);
845 	}
846 
847 	private PackParser index(byte[] packData) throws IOException {
848 		if (inserter == null)
849 			inserter = dst.newObjectInserter();
850 		return inserter.newPackParser(new ByteArrayInputStream(packData));
851 	}
852 
853 	private void verifyObjectsOrder(ObjectId objectsOrder[]) {
854 		final List<PackIndex.MutableEntry> entries = new ArrayList<>();
855 
856 		for (MutableEntry me : pack) {
857 			entries.add(me.cloneEntry());
858 		}
859 		Collections.sort(entries, (MutableEntry o1, MutableEntry o2) -> Long
860 				.signum(o1.getOffset() - o2.getOffset()));
861 
862 		int i = 0;
863 		for (MutableEntry me : entries) {
864 			assertEquals(objectsOrder[i++].toObjectId(), me.toObjectId());
865 		}
866 	}
867 
868 	private static Set<ObjectId> haves(ObjectId... objects) {
869 		return Sets.of(objects);
870 	}
871 
872 	private static Set<ObjectId> wants(ObjectId... objects) {
873 		return Sets.of(objects);
874 	}
875 
876 	private static Set<ObjectId> shallows(ObjectId... objects) {
877 		return Sets.of(objects);
878 	}
879 }