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