View Javadoc
1   /*
2    * Copyright (C) 2012 Google Inc.
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  package org.eclipse.jgit.pgm;
44  
45  import static org.junit.Assert.assertArrayEquals;
46  import static org.junit.Assert.fail;
47  import static org.junit.Assume.assumeNoException;
48  
49  import java.io.BufferedInputStream;
50  import java.io.BufferedReader;
51  import java.io.ByteArrayInputStream;
52  import java.io.File;
53  import java.io.FileInputStream;
54  import java.io.FileOutputStream;
55  import java.io.IOException;
56  import java.io.InputStreamReader;
57  import java.io.OutputStream;
58  import java.util.ArrayList;
59  import java.util.Arrays;
60  import java.util.List;
61  import java.util.concurrent.Callable;
62  import java.util.concurrent.ExecutorService;
63  import java.util.concurrent.Executors;
64  import java.util.concurrent.Future;
65  import java.util.zip.ZipEntry;
66  import java.util.zip.ZipInputStream;
67  
68  import org.eclipse.jgit.api.Git;
69  import org.eclipse.jgit.dircache.DirCache;
70  import org.eclipse.jgit.lib.CLIRepositoryTestCase;
71  import org.eclipse.jgit.lib.FileMode;
72  import org.junit.Before;
73  import org.junit.Test;
74  
75  public class ArchiveTest extends CLIRepositoryTestCase {
76  	private Git git;
77  	private String emptyTree;
78  
79  	@Override
80  	@Before
81  	public void setUp() throws Exception {
82  		super.setUp();
83  		git = new Git(db);
84  		git.commit().setMessage("initial commit").call();
85  		emptyTree = db.resolve("HEAD^{tree}").abbreviate(12).name();
86  	}
87  
88  	@Test
89  	public void testEmptyArchive() throws Exception {
90  		byte[] result = CLIGitCommand.executeRaw(
91  				"git archive --format=zip " + emptyTree, db).outBytes();
92  		assertArrayEquals(new String[0], listZipEntries(result));
93  	}
94  
95  	@Test
96  	public void testEmptyTar() throws Exception {
97  		byte[] result = CLIGitCommand.executeRaw(
98  				"git archive --format=tar " + emptyTree, db).outBytes();
99  		assertArrayEquals(new String[0], listTarEntries(result));
100 	}
101 
102 	@Test
103 	public void testUnrecognizedFormat() throws Exception {
104 		String[] expect = new String[] {
105 				"fatal: Unknown archive format 'nonsense'", "" };
106 		String[] actual = executeUnchecked(
107 				"git archive --format=nonsense " + emptyTree);
108 		assertArrayEquals(expect, actual);
109 	}
110 
111 	@Test
112 	public void testArchiveWithFiles() throws Exception {
113 		writeTrashFile("a", "a file with content!");
114 		writeTrashFile("c", ""); // empty file
115 		writeTrashFile("unrelated", "another file, just for kicks");
116 		git.add().addFilepattern("a").call();
117 		git.add().addFilepattern("c").call();
118 		git.commit().setMessage("populate toplevel").call();
119 
120 		byte[] result = CLIGitCommand.executeRaw(
121 				"git archive --format=zip HEAD", db).outBytes();
122 		assertArrayEquals(new String[] { "a", "c" },
123 				listZipEntries(result));
124 	}
125 
126 	private void commitGreeting() throws Exception {
127 		writeTrashFile("greeting", "hello, world!");
128 		git.add().addFilepattern("greeting").call();
129 		git.commit().setMessage("a commit with a file").call();
130 	}
131 
132 	@Test
133 	public void testDefaultFormatIsTar() throws Exception {
134 		commitGreeting();
135 		byte[] result = CLIGitCommand.executeRaw(
136 				"git archive HEAD", db).outBytes();
137 		assertArrayEquals(new String[] { "greeting" },
138 				listTarEntries(result));
139 	}
140 
141 	private static String shellQuote(String s) {
142 		return "'" + s.replace("'", "'\\''") + "'";
143 	}
144 
145 	@Test
146 	public void testFormatOverridesFilename() throws Exception {
147 		File archive = new File(db.getWorkTree(), "format-overrides-name.tar");
148 		String path = archive.getAbsolutePath();
149 
150 		commitGreeting();
151 		assertArrayEquals(new String[] { "" },
152 				execute("git archive " +
153 					"--format=zip " +
154 					shellQuote("--output=" + path) + " " +
155 					"HEAD"));
156 		assertContainsEntryWithMode(path, "", "greeting");
157 		assertIsZip(archive);
158 	}
159 
160 	@Test
161 	public void testUnrecognizedExtensionMeansTar() throws Exception {
162 		File archive = new File(db.getWorkTree(), "example.txt");
163 		String path = archive.getAbsolutePath();
164 
165 		commitGreeting();
166 		assertArrayEquals(new String[] { "" },
167 				execute("git archive " +
168 					shellQuote("--output=" + path) + " " +
169 					"HEAD"));
170 		assertTarContainsEntry(path, "", "greeting");
171 		assertIsTar(archive);
172 	}
173 
174 	@Test
175 	public void testNoExtensionMeansTar() throws Exception {
176 		File archive = new File(db.getWorkTree(), "example");
177 		String path = archive.getAbsolutePath();
178 
179 		commitGreeting();
180 		assertArrayEquals(new String[] { "" },
181 				execute("git archive " +
182 					shellQuote("--output=" + path) + " " +
183 					"HEAD"));
184 		assertIsTar(archive);
185 	}
186 
187 	@Test
188 	public void testExtensionMatchIsAnchored() throws Exception {
189 		File archive = new File(db.getWorkTree(), "two-extensions.zip.bak");
190 		String path = archive.getAbsolutePath();
191 
192 		commitGreeting();
193 		assertArrayEquals(new String[] { "" },
194 				execute("git archive " +
195 					shellQuote("--output=" + path) + " " +
196 					"HEAD"));
197 		assertIsTar(archive);
198 	}
199 
200 	@Test
201 	public void testZipExtension() throws Exception {
202 		File archiveWithDot = new File(db.getWorkTree(), "greeting.zip");
203 		File archiveNoDot = new File(db.getWorkTree(), "greetingzip");
204 
205 		commitGreeting();
206 		execute("git archive " +
207 			shellQuote("--output=" + archiveWithDot.getAbsolutePath()) + " " +
208 			"HEAD");
209 		execute("git archive " +
210 			shellQuote("--output=" + archiveNoDot.getAbsolutePath()) + " " +
211 			"HEAD");
212 		assertIsZip(archiveWithDot);
213 		assertIsTar(archiveNoDot);
214 	}
215 
216 	@Test
217 	public void testTarExtension() throws Exception {
218 		File archive = new File(db.getWorkTree(), "tarball.tar");
219 		String path = archive.getAbsolutePath();
220 
221 		commitGreeting();
222 		assertArrayEquals(new String[] { "" },
223 				execute("git archive " +
224 					shellQuote("--output=" + path) + " " +
225 					"HEAD"));
226 		assertIsTar(archive);
227 	}
228 
229 	@Test
230 	public void testTgzExtensions() throws Exception {
231 		commitGreeting();
232 
233 		for (String ext : Arrays.asList("tar.gz", "tgz")) {
234 			File archiveWithDot = new File(db.getWorkTree(), "tarball." + ext);
235 			File archiveNoDot = new File(db.getWorkTree(), "tarball" + ext);
236 
237 			execute("git archive " +
238 				shellQuote("--output=" + archiveWithDot.getAbsolutePath()) + " " +
239 				"HEAD");
240 			execute("git archive " +
241 				shellQuote("--output=" + archiveNoDot.getAbsolutePath()) + " " +
242 				"HEAD");
243 			assertIsGzip(archiveWithDot);
244 			assertIsTar(archiveNoDot);
245 		}
246 	}
247 
248 	@Test
249 	public void testTbz2Extension() throws Exception {
250 		commitGreeting();
251 
252 		for (String ext : Arrays.asList("tar.bz2", "tbz", "tbz2")) {
253 			File archiveWithDot = new File(db.getWorkTree(), "tarball." + ext);
254 			File archiveNoDot = new File(db.getWorkTree(), "tarball" + ext);
255 
256 			execute("git archive " +
257 				shellQuote("--output=" + archiveWithDot.getAbsolutePath()) + " " +
258 				"HEAD");
259 			execute("git archive " +
260 				shellQuote("--output=" + archiveNoDot.getAbsolutePath()) + " " +
261 				"HEAD");
262 			assertIsBzip2(archiveWithDot);
263 			assertIsTar(archiveNoDot);
264 		}
265 	}
266 
267 	@Test
268 	public void testTxzExtension() throws Exception {
269 		commitGreeting();
270 
271 		for (String ext : Arrays.asList("tar.xz", "txz")) {
272 			File archiveWithDot = new File(db.getWorkTree(), "tarball." + ext);
273 			File archiveNoDot = new File(db.getWorkTree(), "tarball" + ext);
274 
275 			execute("git archive " +
276 				shellQuote("--output=" + archiveWithDot.getAbsolutePath()) + " " +
277 				"HEAD");
278 			execute("git archive " +
279 				shellQuote("--output=" + archiveNoDot.getAbsolutePath()) + " " +
280 				"HEAD");
281 			assertIsXz(archiveWithDot);
282 			assertIsTar(archiveNoDot);
283 		}
284 	}
285 
286 	@Test
287 	public void testArchiveWithSubdir() throws Exception {
288 		writeTrashFile("a", "a file with content!");
289 		writeTrashFile("b.c", "before subdir in git sort order");
290 		writeTrashFile("b0c", "after subdir in git sort order");
291 		writeTrashFile("c", "");
292 		git.add().addFilepattern("a").call();
293 		git.add().addFilepattern("b.c").call();
294 		git.add().addFilepattern("b0c").call();
295 		git.add().addFilepattern("c").call();
296 		git.commit().setMessage("populate toplevel").call();
297 		writeTrashFile("b/b", "file in subdirectory");
298 		writeTrashFile("b/a", "another file in subdirectory");
299 		git.add().addFilepattern("b").call();
300 		git.commit().setMessage("add subdir").call();
301 
302 		byte[] result = CLIGitCommand.executeRaw(
303 				"git archive --format=zip master", db).outBytes();
304 		String[] expect = { "a", "b.c", "b0c", "b/", "b/a", "b/b", "c" };
305 		String[] actual = listZipEntries(result);
306 
307 		Arrays.sort(expect);
308 		Arrays.sort(actual);
309 		assertArrayEquals(expect, actual);
310 	}
311 
312 	@Test
313 	public void testTarWithSubdir() throws Exception {
314 		writeTrashFile("a", "a file with content!");
315 		writeTrashFile("b.c", "before subdir in git sort order");
316 		writeTrashFile("b0c", "after subdir in git sort order");
317 		writeTrashFile("c", "");
318 		git.add().addFilepattern("a").call();
319 		git.add().addFilepattern("b.c").call();
320 		git.add().addFilepattern("b0c").call();
321 		git.add().addFilepattern("c").call();
322 		git.commit().setMessage("populate toplevel").call();
323 		writeTrashFile("b/b", "file in subdirectory");
324 		writeTrashFile("b/a", "another file in subdirectory");
325 		git.add().addFilepattern("b").call();
326 		git.commit().setMessage("add subdir").call();
327 
328 		byte[] result = CLIGitCommand.executeRaw(
329 				"git archive --format=tar master", db).outBytes();
330 		String[] expect = { "a", "b.c", "b0c", "b/", "b/a", "b/b", "c" };
331 		String[] actual = listTarEntries(result);
332 
333 		Arrays.sort(expect);
334 		Arrays.sort(actual);
335 		assertArrayEquals(expect, actual);
336 	}
337 
338 	private void commitBazAndFooSlashBar() throws Exception {
339 		writeTrashFile("baz", "a file");
340 		writeTrashFile("foo/bar", "another file");
341 		git.add().addFilepattern("baz").call();
342 		git.add().addFilepattern("foo").call();
343 		git.commit().setMessage("sample commit").call();
344 	}
345 
346 	@Test
347 	public void testArchivePrefixOption() throws Exception {
348 		commitBazAndFooSlashBar();
349 		byte[] result = CLIGitCommand.executeRaw(
350 				"git archive --prefix=x/ --format=zip master", db).outBytes();
351 		String[] expect = { "x/", "x/baz", "x/foo/", "x/foo/bar" };
352 		String[] actual = listZipEntries(result);
353 
354 		Arrays.sort(expect);
355 		Arrays.sort(actual);
356 		assertArrayEquals(expect, actual);
357 	}
358 
359 	@Test
360 	public void testTarPrefixOption() throws Exception {
361 		commitBazAndFooSlashBar();
362 		byte[] result = CLIGitCommand.executeRaw(
363 				"git archive --prefix=x/ --format=tar master", db).outBytes();
364 		String[] expect = { "x/", "x/baz", "x/foo/", "x/foo/bar" };
365 		String[] actual = listTarEntries(result);
366 
367 		Arrays.sort(expect);
368 		Arrays.sort(actual);
369 		assertArrayEquals(expect, actual);
370 	}
371 
372 	private void commitFoo() throws Exception {
373 		writeTrashFile("foo", "a file");
374 		git.add().addFilepattern("foo").call();
375 		git.commit().setMessage("boring commit").call();
376 	}
377 
378 	@Test
379 	public void testPrefixDoesNotNormalizeDoubleSlash() throws Exception {
380 		commitFoo();
381 		byte[] result = CLIGitCommand.executeRaw(
382 				"git archive --prefix=x// --format=zip master", db).outBytes();
383 		String[] expect = { "x/", "x//foo" };
384 		assertArrayEquals(expect, listZipEntries(result));
385 	}
386 
387 	@Test
388 	public void testPrefixDoesNotNormalizeDoubleSlashInTar() throws Exception {
389 		commitFoo();
390 		byte[] result = CLIGitCommand.executeRaw(
391 				"git archive --prefix=x// --format=tar master", db).outBytes();
392 		String[] expect = { "x/", "x//foo" };
393 		assertArrayEquals(expect, listTarEntries(result));
394 	}
395 
396 	/**
397 	 * The prefix passed to "git archive" need not end with '/'.
398 	 * In practice it is not very common to have a nonempty prefix
399 	 * that does not name a directory (and hence end with /), but
400 	 * since git has historically supported other prefixes, we do,
401 	 * too.
402 	 *
403 	 * @throws Exception
404 	 */
405 	@Test
406 	public void testPrefixWithoutTrailingSlash() throws Exception {
407 		commitBazAndFooSlashBar();
408 		byte[] result = CLIGitCommand.executeRaw(
409 				"git archive --prefix=my- --format=zip master", db).outBytes();
410 		String[] expect = { "my-baz", "my-foo/", "my-foo/bar" };
411 		String[] actual = listZipEntries(result);
412 
413 		Arrays.sort(expect);
414 		Arrays.sort(actual);
415 		assertArrayEquals(expect, actual);
416 	}
417 
418 	@Test
419 	public void testTarPrefixWithoutTrailingSlash() throws Exception {
420 		commitBazAndFooSlashBar();
421 		byte[] result = CLIGitCommand.executeRaw(
422 				"git archive --prefix=my- --format=tar master", db).outBytes();
423 		String[] expect = { "my-baz", "my-foo/", "my-foo/bar" };
424 		String[] actual = listTarEntries(result);
425 
426 		Arrays.sort(expect);
427 		Arrays.sort(actual);
428 		assertArrayEquals(expect, actual);
429 	}
430 
431 	@Test
432 	public void testArchiveIncludesSubmoduleDirectory() throws Exception {
433 		writeTrashFile("a", "a file with content!");
434 		writeTrashFile("c", "after submodule");
435 		git.add().addFilepattern("a").call();
436 		git.add().addFilepattern("c").call();
437 		git.commit().setMessage("initial commit").call();
438 		git.submoduleAdd().setURI("./.").setPath("b").call().close();
439 		git.commit().setMessage("add submodule").call();
440 
441 		byte[] result = CLIGitCommand.executeRaw(
442 				"git archive --format=zip master", db).outBytes();
443 		String[] expect = { ".gitmodules", "a", "b/", "c" };
444 		String[] actual = listZipEntries(result);
445 
446 		Arrays.sort(expect);
447 		Arrays.sort(actual);
448 		assertArrayEquals(expect, actual);
449 	}
450 
451 	@Test
452 	public void testTarIncludesSubmoduleDirectory() throws Exception {
453 		writeTrashFile("a", "a file with content!");
454 		writeTrashFile("c", "after submodule");
455 		git.add().addFilepattern("a").call();
456 		git.add().addFilepattern("c").call();
457 		git.commit().setMessage("initial commit").call();
458 		git.submoduleAdd().setURI("./.").setPath("b").call().close();
459 		git.commit().setMessage("add submodule").call();
460 
461 		byte[] result = CLIGitCommand.executeRaw(
462 				"git archive --format=tar master", db).outBytes();
463 		String[] expect = { ".gitmodules", "a", "b/", "c" };
464 		String[] actual = listTarEntries(result);
465 
466 		Arrays.sort(expect);
467 		Arrays.sort(actual);
468 		assertArrayEquals(expect, actual);
469 	}
470 
471 	@Test
472 	public void testArchivePreservesMode() throws Exception {
473 		writeTrashFile("plain", "a file with content");
474 		writeTrashFile("executable", "an executable file");
475 		writeTrashFile("symlink", "plain");
476 		writeTrashFile("dir/content", "clutter in a subdir");
477 		git.add().addFilepattern("plain").call();
478 		git.add().addFilepattern("executable").call();
479 		git.add().addFilepattern("symlink").call();
480 		git.add().addFilepattern("dir").call();
481 
482 		DirCache cache = db.lockDirCache();
483 		cache.getEntry("executable").setFileMode(FileMode.EXECUTABLE_FILE);
484 		cache.getEntry("symlink").setFileMode(FileMode.SYMLINK);
485 		cache.write();
486 		cache.commit();
487 		cache.unlock();
488 
489 		git.commit().setMessage("three files with different modes").call();
490 
491 		byte[] zipData = CLIGitCommand.executeRaw(
492 				"git archive --format=zip master", db).outBytes();
493 		writeRaw("zip-with-modes.zip", zipData);
494 		assertContainsEntryWithMode("zip-with-modes.zip", "-rw-", "plain");
495 		assertContainsEntryWithMode("zip-with-modes.zip", "-rwx", "executable");
496 		assertContainsEntryWithMode("zip-with-modes.zip", "l", "symlink");
497 		assertContainsEntryWithMode("zip-with-modes.zip", "-rw-", "dir/");
498 	}
499 
500 	@Test
501 	public void testTarPreservesMode() throws Exception {
502 		writeTrashFile("plain", "a file with content");
503 		writeTrashFile("executable", "an executable file");
504 		writeTrashFile("symlink", "plain");
505 		writeTrashFile("dir/content", "clutter in a subdir");
506 		git.add().addFilepattern("plain").call();
507 		git.add().addFilepattern("executable").call();
508 		git.add().addFilepattern("symlink").call();
509 		git.add().addFilepattern("dir").call();
510 
511 		DirCache cache = db.lockDirCache();
512 		cache.getEntry("executable").setFileMode(FileMode.EXECUTABLE_FILE);
513 		cache.getEntry("symlink").setFileMode(FileMode.SYMLINK);
514 		cache.write();
515 		cache.commit();
516 		cache.unlock();
517 
518 		git.commit().setMessage("three files with different modes").call();
519 
520 		byte[] archive = CLIGitCommand.executeRaw(
521 				"git archive --format=tar master", db).outBytes();
522 		writeRaw("with-modes.tar", archive);
523 		assertTarContainsEntry("with-modes.tar", "-rw-r--r--", "plain");
524 		assertTarContainsEntry("with-modes.tar", "-rwxr-xr-x", "executable");
525 		assertTarContainsEntry("with-modes.tar", "l", "symlink -> plain");
526 		assertTarContainsEntry("with-modes.tar", "drwxr-xr-x", "dir/");
527 	}
528 
529 	@Test
530 	public void testArchiveWithLongFilename() throws Exception {
531 		StringBuilder filename = new StringBuilder();
532 		List<String> l = new ArrayList<>();
533 		for (int i = 0; i < 20; i++) {
534 			filename.append("1234567890/");
535 			l.add(filename.toString());
536 		}
537 		filename.append("1234567890");
538 		l.add(filename.toString());
539 		writeTrashFile(filename.toString(), "file with long path");
540 		git.add().addFilepattern("1234567890").call();
541 		git.commit().setMessage("file with long name").call();
542 
543 		byte[] result = CLIGitCommand.executeRaw(
544 				"git archive --format=zip HEAD", db).outBytes();
545 		assertArrayEquals(l.toArray(new String[l.size()]),
546 				listZipEntries(result));
547 	}
548 
549 	@Test
550 	public void testTarWithLongFilename() throws Exception {
551 		StringBuilder filename = new StringBuilder();
552 		List<String> l = new ArrayList<>();
553 		for (int i = 0; i < 20; i++) {
554 			filename.append("1234567890/");
555 			l.add(filename.toString());
556 		}
557 		filename.append("1234567890");
558 		l.add(filename.toString());
559 		writeTrashFile(filename.toString(), "file with long path");
560 		git.add().addFilepattern("1234567890").call();
561 		git.commit().setMessage("file with long name").call();
562 
563 		byte[] result = CLIGitCommand.executeRaw(
564 				"git archive --format=tar HEAD", db).outBytes();
565 		assertArrayEquals(l.toArray(new String[l.size()]),
566 				listTarEntries(result));
567 	}
568 
569 	@Test
570 	public void testArchivePreservesContent() throws Exception {
571 		String payload = "“The quick brown fox jumps over the lazy dog!”";
572 		writeTrashFile("xyzzy", payload);
573 		git.add().addFilepattern("xyzzy").call();
574 		git.commit().setMessage("add file with content").call();
575 
576 		byte[] result = CLIGitCommand.executeRaw(
577 				"git archive --format=zip HEAD", db).outBytes();
578 		assertArrayEquals(new String[] { payload },
579 				zipEntryContent(result, "xyzzy"));
580 	}
581 
582 	@Test
583 	public void testTarPreservesContent() throws Exception {
584 		String payload = "“The quick brown fox jumps over the lazy dog!”";
585 		writeTrashFile("xyzzy", payload);
586 		git.add().addFilepattern("xyzzy").call();
587 		git.commit().setMessage("add file with content").call();
588 
589 		byte[] result = CLIGitCommand.executeRaw(
590 				"git archive --format=tar HEAD", db).outBytes();
591 		assertArrayEquals(new String[] { payload },
592 				tarEntryContent(result, "xyzzy"));
593 	}
594 
595 	private Process spawnAssumingCommandPresent(String... cmdline) {
596 		File cwd = db.getWorkTree();
597 		ProcessBuilder procBuilder = new ProcessBuilder(cmdline)
598 				.directory(cwd)
599 				.redirectErrorStream(true);
600 		Process proc = null;
601 		try {
602 			proc = procBuilder.start();
603 		} catch (IOException e) {
604 			// On machines without `cmdline[0]`, let the test pass.
605 			assumeNoException(e);
606 		}
607 
608 		return proc;
609 	}
610 
611 	private BufferedReader readFromProcess(Process proc) throws Exception {
612 		return new BufferedReader(
613 				new InputStreamReader(proc.getInputStream(), "UTF-8"));
614 	}
615 
616 	private void grepForEntry(String name, String mode, String... cmdline)
617 			throws Exception {
618 		Process proc = spawnAssumingCommandPresent(cmdline);
619 		proc.getOutputStream().close();
620 		BufferedReader reader = readFromProcess(proc);
621 		try {
622 			String line;
623 			while ((line = reader.readLine()) != null)
624 				if (line.startsWith(mode) && line.endsWith(name))
625 					// found it!
626 					return;
627 			fail("expected entry " + name + " with mode " + mode + " but found none");
628 		} finally {
629 			proc.getOutputStream().close();
630 			proc.destroy();
631 		}
632 	}
633 
634 	private void assertMagic(long offset, byte[] magicBytes, File file) throws Exception {
635 		BufferedInputStream in = new BufferedInputStream(
636 				new FileInputStream(file));
637 		try {
638 			in.skip(offset);
639 
640 			byte[] actual = new byte[magicBytes.length];
641 			in.read(actual);
642 			assertArrayEquals(magicBytes, actual);
643 		} finally {
644 			in.close();
645 		}
646 	}
647 
648 	private void assertMagic(byte[] magicBytes, File file) throws Exception {
649 		assertMagic(0, magicBytes, file);
650 	}
651 
652 	private void assertIsTar(File file) throws Exception {
653 		assertMagic(257, new byte[] { 'u', 's', 't', 'a', 'r', 0 }, file);
654 	}
655 
656 	private void assertIsZip(File file) throws Exception {
657 		assertMagic(new byte[] { 'P', 'K', 3, 4 }, file);
658 	}
659 
660 	private void assertIsGzip(File file) throws Exception {
661 		assertMagic(new byte[] { 037, (byte) 0213 }, file);
662 	}
663 
664 	private void assertIsBzip2(File file) throws Exception {
665 		assertMagic(new byte[] { 'B', 'Z', 'h' }, file);
666 	}
667 
668 	private void assertIsXz(File file) throws Exception {
669 		assertMagic(new byte[] { (byte) 0xfd, '7', 'z', 'X', 'Z', 0 }, file);
670 	}
671 
672 	private void assertContainsEntryWithMode(String zipFilename, String mode, String name)
673 			throws Exception {
674 		grepForEntry(name, mode, "zipinfo", zipFilename);
675 	}
676 
677 	private void assertTarContainsEntry(String tarfile, String mode, String name)
678 			throws Exception {
679 		grepForEntry(name, mode, "tar", "tvf", tarfile);
680 	}
681 
682 	private void writeRaw(String filename, byte[] data)
683 			throws IOException {
684 		File path = new File(db.getWorkTree(), filename);
685 		OutputStream out = new FileOutputStream(path);
686 		try {
687 			out.write(data);
688 		} finally {
689 			out.close();
690 		}
691 	}
692 
693 	private static String[] listZipEntries(byte[] zipData) throws IOException {
694 		List<String> l = new ArrayList<>();
695 		ZipInputStream in = new ZipInputStream(
696 				new ByteArrayInputStream(zipData));
697 
698 		ZipEntry e;
699 		while ((e = in.getNextEntry()) != null)
700 			l.add(e.getName());
701 		in.close();
702 		return l.toArray(new String[l.size()]);
703 	}
704 
705 	private static Future<Object> writeAsync(final OutputStream stream, final byte[] data) {
706 		ExecutorService executor = Executors.newSingleThreadExecutor();
707 
708 		return executor.submit(new Callable<Object>() {
709 			@Override
710 			public Object call() throws IOException {
711 				try {
712 					stream.write(data);
713 					return null;
714 				} finally {
715 					stream.close();
716 				}
717 			}
718 		});
719 	}
720 
721 	private String[] listTarEntries(byte[] tarData) throws Exception {
722 		List<String> l = new ArrayList<>();
723 		Process proc = spawnAssumingCommandPresent("tar", "tf", "-");
724 		BufferedReader reader = readFromProcess(proc);
725 		OutputStream out = proc.getOutputStream();
726 
727 		// Dump tarball to tar stdin in background
728 		Future<?> writing = writeAsync(out, tarData);
729 
730 		try {
731 			String line;
732 			while ((line = reader.readLine()) != null)
733 				l.add(line);
734 
735 			return l.toArray(new String[l.size()]);
736 		} finally {
737 			writing.get();
738 			reader.close();
739 			proc.destroy();
740 		}
741 	}
742 
743 	private static String[] zipEntryContent(byte[] zipData, String path)
744 			throws IOException {
745 		ZipInputStream in = new ZipInputStream(
746 				new ByteArrayInputStream(zipData));
747 		ZipEntry e;
748 		while ((e = in.getNextEntry()) != null) {
749 			if (!e.getName().equals(path))
750 				continue;
751 
752 			// found!
753 			List<String> l = new ArrayList<>();
754 			BufferedReader reader = new BufferedReader(
755 					new InputStreamReader(in, "UTF-8"));
756 			String line;
757 			while ((line = reader.readLine()) != null)
758 				l.add(line);
759 			return l.toArray(new String[l.size()]);
760 		}
761 
762 		// not found
763 		return null;
764 	}
765 
766 	private String[] tarEntryContent(byte[] tarData, String path)
767 			throws Exception {
768 		List<String> l = new ArrayList<>();
769 		Process proc = spawnAssumingCommandPresent("tar", "Oxf", "-", path);
770 		BufferedReader reader = readFromProcess(proc);
771 		OutputStream out = proc.getOutputStream();
772 		Future<?> writing = writeAsync(out, tarData);
773 
774 		try {
775 			String line;
776 			while ((line = reader.readLine()) != null)
777 				l.add(line);
778 
779 			return l.toArray(new String[l.size()]);
780 		} finally {
781 			writing.get();
782 			reader.close();
783 			proc.destroy();
784 		}
785 	}
786 }