Browse Source

archive: Add tar support

Unlike ZIP files, tar files do not treat symlinks as ordinary files
with a different mode, so tar support involves a little more code than
would be ideal.

Change-Id: Ica2568f4a0e443bf4b955ef0c029bc8eec62d369
tags/v2.2.0.201212191850-r
Jonathan Nieder 11 years ago
parent
commit
78009782cd

+ 160
- 7
org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java View File

@@ -53,11 +53,15 @@ import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.Object;
import java.lang.String;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

@@ -91,6 +95,13 @@ public class ArchiveTest extends CLIRepositoryTestCase {
assertArrayEquals(new String[0], listZipEntries(result));
}

@Test
public void testEmptyTar() throws Exception {
final byte[] result = CLIGitCommand.rawExecute( //
"git archive --format=tar " + emptyTree, db);
assertArrayEquals(new String[0], listTarEntries(result));
}

@Test
public void testArchiveWithFiles() throws Exception {
writeTrashFile("a", "a file with content!");
@@ -132,6 +143,32 @@ public class ArchiveTest extends CLIRepositoryTestCase {
assertArrayEquals(expect, actual);
}

@Test
public void testTarWithSubdir() throws Exception {
writeTrashFile("a", "a file with content!");
writeTrashFile("b.c", "before subdir in git sort order");
writeTrashFile("b0c", "after subdir in git sort order");
writeTrashFile("c", "");
git.add().addFilepattern("a").call();
git.add().addFilepattern("b.c").call();
git.add().addFilepattern("b0c").call();
git.add().addFilepattern("c").call();
git.commit().setMessage("populate toplevel").call();
writeTrashFile("b/b", "file in subdirectory");
writeTrashFile("b/a", "another file in subdirectory");
git.add().addFilepattern("b").call();
git.commit().setMessage("add subdir").call();

final byte[] result = CLIGitCommand.rawExecute( //
"git archive --format=tar master", db);
String[] expect = { "a", "b.c", "b0c", "b/a", "b/b", "c" };
String[] actual = listTarEntries(result);

Arrays.sort(expect);
Arrays.sort(actual);
assertArrayEquals(expect, actual);
}

@Test
public void testArchivePreservesMode() throws Exception {
writeTrashFile("plain", "a file with content");
@@ -158,6 +195,32 @@ public class ArchiveTest extends CLIRepositoryTestCase {
assertContainsEntryWithMode("zip-with-modes.zip", "l", "symlink");
}

@Test
public void testTarPreservesMode() throws Exception {
writeTrashFile("plain", "a file with content");
writeTrashFile("executable", "an executable file");
writeTrashFile("symlink", "plain");
git.add().addFilepattern("plain").call();
git.add().addFilepattern("executable").call();
git.add().addFilepattern("symlink").call();

DirCache cache = db.lockDirCache();
cache.getEntry("executable").setFileMode(FileMode.EXECUTABLE_FILE);
cache.getEntry("symlink").setFileMode(FileMode.SYMLINK);
cache.write();
cache.commit();
cache.unlock();

git.commit().setMessage("three files with different modes").call();

final byte[] archive = CLIGitCommand.rawExecute( //
"git archive --format=tar master", db);
writeRaw("with-modes.tar", archive);
assertTarContainsEntry("with-modes.tar", "-rw-r--r--", "plain");
assertTarContainsEntry("with-modes.tar", "-rwxr-xr-x", "executable");
assertTarContainsEntry("with-modes.tar", "l", "symlink -> plain");
}

@Test
public void testArchivePreservesContent() throws Exception {
final String payload = "“The quick brown fox jumps over the lazy dog!”";
@@ -171,23 +234,45 @@ public class ArchiveTest extends CLIRepositoryTestCase {
zipEntryContent(result, "xyzzy"));
}

private void assertContainsEntryWithMode(String zipFilename, String mode, String name) //
throws Exception {
@Test
public void testTarPreservesContent() throws Exception {
final String payload = "“The quick brown fox jumps over the lazy dog!”";
writeTrashFile("xyzzy", payload);
git.add().addFilepattern("xyzzy").call();
git.commit().setMessage("add file with content").call();

final byte[] result = CLIGitCommand.rawExecute( //
"git archive --format=tar HEAD", db);
assertArrayEquals(new String[] { payload }, //
tarEntryContent(result, "xyzzy"));
}

private Process spawnAssumingCommandPresent(String... cmdline) {
final File cwd = db.getWorkTree();
final ProcessBuilder procBuilder = new ProcessBuilder("zipinfo", zipFilename) //
final ProcessBuilder procBuilder = new ProcessBuilder(cmdline) //
.directory(cwd) //
.redirectErrorStream(true);
Process proc = null;
try {
proc = procBuilder.start();
} catch (IOException e) {
// On machines without a "zipinfo" command, let the test pass.
// On machines without `cmdline[0]`, let the test pass.
assumeNoException(e);
}

proc.getOutputStream().close();
final BufferedReader reader = new BufferedReader( //
return proc;
}

private BufferedReader readFromProcess(Process proc) throws Exception {
return new BufferedReader( //
new InputStreamReader(proc.getInputStream(), "UTF-8"));
}

private void grepForEntry(String name, String mode, String... cmdline) //
throws Exception {
final Process proc = spawnAssumingCommandPresent(cmdline);
proc.getOutputStream().close();
final BufferedReader reader = readFromProcess(proc);
try {
String line;
while ((line = reader.readLine()) != null)
@@ -201,6 +286,16 @@ public class ArchiveTest extends CLIRepositoryTestCase {
}
}

private void assertContainsEntryWithMode(String zipFilename, String mode, String name) //
throws Exception {
grepForEntry(name, mode, "zipinfo", zipFilename);
}

private void assertTarContainsEntry(String tarfile, String mode, String name) //
throws Exception {
grepForEntry(name, mode, "tar", "tvf", tarfile);
}

private void writeRaw(String filename, byte[] data) //
throws IOException {
final File path = new File(db.getWorkTree(), filename);
@@ -224,6 +319,43 @@ public class ArchiveTest extends CLIRepositoryTestCase {
return l.toArray(new String[l.size()]);
}

private static Future<Object> writeAsync(final OutputStream stream, final byte[] data) {
final ExecutorService executor = Executors.newSingleThreadExecutor();

return executor.submit(new Callable<Object>() { //
public Object call() throws IOException {
try {
stream.write(data);
return null;
} finally {
stream.close();
}
}
});
}

private String[] listTarEntries(byte[] tarData) throws Exception {
final List<String> l = new ArrayList<String>();
final Process proc = spawnAssumingCommandPresent("tar", "tf", "-");
final BufferedReader reader = readFromProcess(proc);
final OutputStream out = proc.getOutputStream();

// Dump tarball to tar stdin in background
final Future<?> writing = writeAsync(out, tarData);

try {
String line;
while ((line = reader.readLine()) != null)
l.add(line);

return l.toArray(new String[l.size()]);
} finally {
writing.get();
reader.close();
proc.destroy();
}
}

private static String[] zipEntryContent(byte[] zipData, String path) //
throws IOException {
final ZipInputStream in = new ZipInputStream( //
@@ -246,4 +378,25 @@ public class ArchiveTest extends CLIRepositoryTestCase {
// not found
return null;
}

private String[] tarEntryContent(byte[] tarData, String path) //
throws Exception {
final List<String> l = new ArrayList<String>();
final Process proc = spawnAssumingCommandPresent("tar", "Oxf", "-", path);
final BufferedReader reader = readFromProcess(proc);
final OutputStream out = proc.getOutputStream();
final Future<?> writing = writeAsync(out, tarData);

try {
String line;
while ((line = reader.readLine()) != null)
l.add(line);

return l.toArray(new String[l.size()]);
} finally {
writing.get();
reader.close();
proc.destroy();
}
}
}

+ 1
- 0
org.eclipse.jgit.pgm/META-INF/MANIFEST.MF View File

@@ -7,6 +7,7 @@ Bundle-Vendor: %provider_name
Bundle-Localization: plugin
Bundle-RequiredExecutionEnvironment: J2SE-1.5
Import-Package: org.apache.commons.compress.archivers;version="[1.3,2.0)",
org.apache.commons.compress.archivers.tar;version="[1.3,2.0)",
org.apache.commons.compress.archivers.zip;version="[1.3,2.0)",
org.eclipse.jgit.api;version="[2.2.0,2.3.0)",
org.eclipse.jgit.api.errors;version="[2.2.0,2.3.0)",

+ 1
- 1
org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties View File

@@ -197,7 +197,7 @@ usage_addFileContentsToTheIndex=Add file contents to the index
usage_alterTheDetailShown=alter the detail shown
usage_approveDestructionOfRepository=approve destruction of repository
usage_archive=zip up files from the named tree
usage_archiveFormat=archive format. Currently supported formats: 'zip'
usage_archiveFormat=archive format. Currently supported formats: 'tar', 'zip'
usage_blameLongRevision=show long revision
usage_blameRange=annotate only the given range
usage_blameRawTimestamp=show raw timestamp

+ 37
- 1
org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Archive.java View File

@@ -53,6 +53,9 @@ import java.text.MessageFormat;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveOutputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.archivers.tar.TarConstants;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.eclipse.jgit.lib.FileMode;
@@ -111,7 +114,8 @@ class Archive extends TextBuiltin {
}

public enum Format {
ZIP
ZIP,
TAR
};

private static interface Archiver {
@@ -149,6 +153,38 @@ class Archive extends TextBuiltin {
out.closeArchiveEntry();
}
});
fmts.put(Format.TAR, new Archiver() {
@Override
public ArchiveOutputStream createArchiveOutputStream(OutputStream s) {
return new TarArchiveOutputStream(s);
}

@Override
public void putEntry(String path, FileMode mode, //
ObjectLoader loader, ArchiveOutputStream out) //
throws IOException {
if (mode == FileMode.SYMLINK) {
final TarArchiveEntry entry = new TarArchiveEntry( //
path, TarConstants.LF_SYMLINK);
entry.setLinkName(new String( //
loader.getCachedBytes(100), "UTF-8"));
out.putArchiveEntry(entry);
out.closeArchiveEntry();
return;
}

final TarArchiveEntry entry = new TarArchiveEntry(path);
if (mode == FileMode.REGULAR_FILE ||
mode == FileMode.EXECUTABLE_FILE)
entry.setMode(mode.getBits());
else
warnArchiveEntryModeIgnored(path);
entry.setSize(loader.getSize());
out.putArchiveEntry(entry);
loader.copyTo(out);
out.closeArchiveEntry();
}
});
formats = fmts;
}
}

Loading…
Cancel
Save