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