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