Entries for directories are optional and mostly wasted space in most archive formats (except as a place to hang ownership and filesystem permissions), but "git archive" includes them. Follow suit. This will make it easier in a later change to include empty directories as placeholders for missing submodules. Change-Id: I1810c686bcc9eb4d73498e4d3e763e18787b088a Signed-off-by: Jonathan Nieder <jrn@google.com>tags/v3.3.0.201402191814-rc1
pathDoesNotMatchMode=Path {0} does not match mode {1} | |||||
unsupportedMode=Unsupported mode {0} |
import java.util.Arrays; | import java.util.Arrays; | ||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.List; | import java.util.List; | ||||
import java.text.MessageFormat; | |||||
import org.apache.commons.compress.archivers.ArchiveOutputStream; | import org.apache.commons.compress.archivers.ArchiveOutputStream; | ||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry; | import org.apache.commons.compress.archivers.tar.TarArchiveEntry; | ||||
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; | import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; | ||||
import org.apache.commons.compress.archivers.tar.TarConstants; | import org.apache.commons.compress.archivers.tar.TarConstants; | ||||
import org.eclipse.jgit.api.ArchiveCommand; | import org.eclipse.jgit.api.ArchiveCommand; | ||||
import org.eclipse.jgit.archive.internal.ArchiveText; | |||||
import org.eclipse.jgit.lib.FileMode; | import org.eclipse.jgit.lib.FileMode; | ||||
import org.eclipse.jgit.lib.ObjectLoader; | import org.eclipse.jgit.lib.ObjectLoader; | ||||
return; | return; | ||||
} | } | ||||
// TarArchiveEntry detects directories by checking | |||||
// for '/' at the end of the filename. | |||||
if (path.endsWith("/") && mode != FileMode.TREE) | |||||
throw new IllegalArgumentException(MessageFormat.format( | |||||
ArchiveText.get().pathDoesNotMatchMode, path, mode)); | |||||
if (!path.endsWith("/") && mode == FileMode.TREE) | |||||
path = path + "/"; | |||||
final TarArchiveEntry entry = new TarArchiveEntry(path); | final TarArchiveEntry entry = new TarArchiveEntry(path); | ||||
if (mode == FileMode.REGULAR_FILE || | |||||
mode == FileMode.EXECUTABLE_FILE) { | |||||
if (mode == FileMode.TREE) { | |||||
out.putArchiveEntry(entry); | |||||
out.closeArchiveEntry(); | |||||
return; | |||||
} | |||||
if (mode == FileMode.REGULAR_FILE) { | |||||
// ok | |||||
} else if (mode == FileMode.EXECUTABLE_FILE) { | |||||
entry.setMode(mode.getBits()); | entry.setMode(mode.getBits()); | ||||
} else { | } else { | ||||
// TODO(jrn): Let the caller know the tree contained | |||||
// an entry with unsupported mode (e.g., a submodule). | |||||
// Unsupported mode (e.g., GITLINK). | |||||
throw new IllegalArgumentException(MessageFormat.format( | |||||
ArchiveText.get().unsupportedMode, mode)); | |||||
} | } | ||||
entry.setSize(loader.getSize()); | entry.setSize(loader.getSize()); | ||||
out.putArchiveEntry(entry); | out.putArchiveEntry(entry); |
import java.io.IOException; | import java.io.IOException; | ||||
import java.io.OutputStream; | import java.io.OutputStream; | ||||
import java.text.MessageFormat; | |||||
import java.util.Arrays; | import java.util.Arrays; | ||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.List; | import java.util.List; | ||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; | import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; | ||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; | import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; | ||||
import org.eclipse.jgit.api.ArchiveCommand; | import org.eclipse.jgit.api.ArchiveCommand; | ||||
import org.eclipse.jgit.archive.internal.ArchiveText; | |||||
import org.eclipse.jgit.lib.FileMode; | import org.eclipse.jgit.lib.FileMode; | ||||
import org.eclipse.jgit.lib.ObjectLoader; | import org.eclipse.jgit.lib.ObjectLoader; | ||||
public void putEntry(ArchiveOutputStream out, | public void putEntry(ArchiveOutputStream out, | ||||
String path, FileMode mode, ObjectLoader loader) | String path, FileMode mode, ObjectLoader loader) | ||||
throws IOException { | throws IOException { | ||||
// ZipArchiveEntry detects directories by checking | |||||
// for '/' at the end of the filename. | |||||
if (path.endsWith("/") && mode != FileMode.TREE) | |||||
throw new IllegalArgumentException(MessageFormat.format( | |||||
ArchiveText.get().pathDoesNotMatchMode, path, mode)); | |||||
if (!path.endsWith("/") && mode == FileMode.TREE) | |||||
path = path + "/"; | |||||
final ZipArchiveEntry entry = new ZipArchiveEntry(path); | final ZipArchiveEntry entry = new ZipArchiveEntry(path); | ||||
if (mode == FileMode.TREE) { | |||||
out.putArchiveEntry(entry); | |||||
out.closeArchiveEntry(); | |||||
return; | |||||
} | |||||
if (mode == FileMode.REGULAR_FILE) { | if (mode == FileMode.REGULAR_FILE) { | ||||
// ok | // ok | ||||
|| mode == FileMode.SYMLINK) { | || mode == FileMode.SYMLINK) { | ||||
entry.setUnixMode(mode.getBits()); | entry.setUnixMode(mode.getBits()); | ||||
} else { | } else { | ||||
// TODO(jrn): Let the caller know the tree contained | |||||
// an entry with unsupported mode (e.g., a submodule). | |||||
// Unsupported mode (e.g., GITLINK). | |||||
throw new IllegalArgumentException(MessageFormat.format( | |||||
ArchiveText.get().unsupportedMode, mode)); | |||||
} | } | ||||
entry.setSize(loader.getSize()); | entry.setSize(loader.getSize()); | ||||
out.putArchiveEntry(entry); | out.putArchiveEntry(entry); |
/* | |||||
* Copyright (C) 2013, Google Inc. | |||||
* and other copyright owners as documented in the project's IP log. | |||||
* | |||||
* This program and the accompanying materials are made available | |||||
* under the terms of the Eclipse Distribution License v1.0 which | |||||
* accompanies this distribution, is reproduced below, and is | |||||
* available at http://www.eclipse.org/org/documents/edl-v10.php | |||||
* | |||||
* All rights reserved. | |||||
* | |||||
* Redistribution and use in source and binary forms, with or | |||||
* without modification, are permitted provided that the following | |||||
* conditions are met: | |||||
* | |||||
* - Redistributions of source code must retain the above copyright | |||||
* notice, this list of conditions and the following disclaimer. | |||||
* | |||||
* - Redistributions in binary form must reproduce the above | |||||
* copyright notice, this list of conditions and the following | |||||
* disclaimer in the documentation and/or other materials provided | |||||
* with the distribution. | |||||
* | |||||
* - Neither the name of the Eclipse Foundation, Inc. nor the | |||||
* names of its contributors may be used to endorse or promote | |||||
* products derived from this software without specific prior | |||||
* written permission. | |||||
* | |||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND | |||||
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, | |||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | |||||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR | |||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT | |||||
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, | |||||
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF | |||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |||||
*/ | |||||
package org.eclipse.jgit.archive.internal; | |||||
import org.eclipse.jgit.nls.NLS; | |||||
import org.eclipse.jgit.nls.TranslationBundle; | |||||
/** | |||||
* Translation bundle for archivers | |||||
*/ | |||||
public class ArchiveText extends TranslationBundle { | |||||
/** | |||||
* @return an instance of this translation bundle | |||||
*/ | |||||
public static ArchiveText get() { | |||||
return NLS.getBundleFor(ArchiveText.class); | |||||
} | |||||
// @formatter:off | |||||
/***/ public String pathDoesNotMatchMode; | |||||
/***/ public String unsupportedMode; | |||||
} |
package org.eclipse.jgit.pgm; | package org.eclipse.jgit.pgm; | ||||
import static org.junit.Assert.assertArrayEquals; | import static org.junit.Assert.assertArrayEquals; | ||||
import static org.junit.Assert.assertEquals; | |||||
import static org.junit.Assert.fail; | import static org.junit.Assert.fail; | ||||
import static org.junit.Assume.assumeNoException; | import static org.junit.Assume.assumeNoException; | ||||
final byte[] result = CLIGitCommand.rawExecute( // | final byte[] result = CLIGitCommand.rawExecute( // | ||||
"git archive --format=zip master", db); | "git archive --format=zip master", db); | ||||
String[] expect = { "a", "b.c", "b0c", "b/a", "b/b", "c" }; | |||||
String[] expect = { "a", "b.c", "b0c", "b/", "b/a", "b/b", "c" }; | |||||
String[] actual = listZipEntries(result); | String[] actual = listZipEntries(result); | ||||
Arrays.sort(expect); | Arrays.sort(expect); | ||||
final byte[] result = CLIGitCommand.rawExecute( // | final byte[] result = CLIGitCommand.rawExecute( // | ||||
"git archive --format=tar master", db); | "git archive --format=tar master", db); | ||||
String[] expect = { "a", "b.c", "b0c", "b/a", "b/b", "c" }; | |||||
String[] expect = { "a", "b.c", "b0c", "b/", "b/a", "b/b", "c" }; | |||||
String[] actual = listTarEntries(result); | String[] actual = listTarEntries(result); | ||||
Arrays.sort(expect); | Arrays.sort(expect); | ||||
commitBazAndFooSlashBar(); | commitBazAndFooSlashBar(); | ||||
byte[] result = CLIGitCommand.rawExecute( | byte[] result = CLIGitCommand.rawExecute( | ||||
"git archive --prefix=x/ --format=zip master", db); | "git archive --prefix=x/ --format=zip master", db); | ||||
String[] expect = { "x/baz", "x/foo/bar" }; | |||||
String[] expect = { "x/baz", "x/foo/", "x/foo/bar" }; | |||||
String[] actual = listZipEntries(result); | String[] actual = listZipEntries(result); | ||||
Arrays.sort(expect); | Arrays.sort(expect); | ||||
commitBazAndFooSlashBar(); | commitBazAndFooSlashBar(); | ||||
byte[] result = CLIGitCommand.rawExecute( | byte[] result = CLIGitCommand.rawExecute( | ||||
"git archive --prefix=x/ --format=tar master", db); | "git archive --prefix=x/ --format=tar master", db); | ||||
String[] expect = { "x/baz", "x/foo/bar" }; | |||||
String[] expect = { "x/baz", "x/foo/", "x/foo/bar" }; | |||||
String[] actual = listTarEntries(result); | String[] actual = listTarEntries(result); | ||||
Arrays.sort(expect); | Arrays.sort(expect); | ||||
commitBazAndFooSlashBar(); | commitBazAndFooSlashBar(); | ||||
byte[] result = CLIGitCommand.rawExecute( | byte[] result = CLIGitCommand.rawExecute( | ||||
"git archive --prefix=my- --format=zip master", db); | "git archive --prefix=my- --format=zip master", db); | ||||
String[] expect = { "my-baz", "my-foo/bar" }; | |||||
String[] expect = { "my-baz", "my-foo/", "my-foo/bar" }; | |||||
String[] actual = listZipEntries(result); | String[] actual = listZipEntries(result); | ||||
Arrays.sort(expect); | Arrays.sort(expect); | ||||
commitBazAndFooSlashBar(); | commitBazAndFooSlashBar(); | ||||
final byte[] result = CLIGitCommand.rawExecute( // | final byte[] result = CLIGitCommand.rawExecute( // | ||||
"git archive --prefix=my- --format=tar master", db); | "git archive --prefix=my- --format=tar master", db); | ||||
String[] expect = { "my-baz", "my-foo/bar" }; | |||||
String[] expect = { "my-baz", "my-foo/", "my-foo/bar" }; | |||||
String[] actual = listTarEntries(result); | String[] actual = listTarEntries(result); | ||||
Arrays.sort(expect); | Arrays.sort(expect); | ||||
writeTrashFile("plain", "a file with content"); | writeTrashFile("plain", "a file with content"); | ||||
writeTrashFile("executable", "an executable file"); | writeTrashFile("executable", "an executable file"); | ||||
writeTrashFile("symlink", "plain"); | writeTrashFile("symlink", "plain"); | ||||
writeTrashFile("dir/content", "clutter in a subdir"); | |||||
git.add().addFilepattern("plain").call(); | git.add().addFilepattern("plain").call(); | ||||
git.add().addFilepattern("executable").call(); | git.add().addFilepattern("executable").call(); | ||||
git.add().addFilepattern("symlink").call(); | git.add().addFilepattern("symlink").call(); | ||||
git.add().addFilepattern("dir").call(); | |||||
DirCache cache = db.lockDirCache(); | DirCache cache = db.lockDirCache(); | ||||
cache.getEntry("executable").setFileMode(FileMode.EXECUTABLE_FILE); | cache.getEntry("executable").setFileMode(FileMode.EXECUTABLE_FILE); | ||||
assertContainsEntryWithMode("zip-with-modes.zip", "-rw-", "plain"); | assertContainsEntryWithMode("zip-with-modes.zip", "-rw-", "plain"); | ||||
assertContainsEntryWithMode("zip-with-modes.zip", "-rwx", "executable"); | assertContainsEntryWithMode("zip-with-modes.zip", "-rwx", "executable"); | ||||
assertContainsEntryWithMode("zip-with-modes.zip", "l", "symlink"); | assertContainsEntryWithMode("zip-with-modes.zip", "l", "symlink"); | ||||
assertContainsEntryWithMode("zip-with-modes.zip", "-rw-", "dir/"); | |||||
} | } | ||||
@Test | @Test | ||||
writeTrashFile("plain", "a file with content"); | writeTrashFile("plain", "a file with content"); | ||||
writeTrashFile("executable", "an executable file"); | writeTrashFile("executable", "an executable file"); | ||||
writeTrashFile("symlink", "plain"); | writeTrashFile("symlink", "plain"); | ||||
writeTrashFile("dir/content", "clutter in a subdir"); | |||||
git.add().addFilepattern("plain").call(); | git.add().addFilepattern("plain").call(); | ||||
git.add().addFilepattern("executable").call(); | git.add().addFilepattern("executable").call(); | ||||
git.add().addFilepattern("symlink").call(); | git.add().addFilepattern("symlink").call(); | ||||
git.add().addFilepattern("dir").call(); | |||||
DirCache cache = db.lockDirCache(); | DirCache cache = db.lockDirCache(); | ||||
cache.getEntry("executable").setFileMode(FileMode.EXECUTABLE_FILE); | cache.getEntry("executable").setFileMode(FileMode.EXECUTABLE_FILE); | ||||
assertTarContainsEntry("with-modes.tar", "-rw-r--r--", "plain"); | assertTarContainsEntry("with-modes.tar", "-rw-r--r--", "plain"); | ||||
assertTarContainsEntry("with-modes.tar", "-rwxr-xr-x", "executable"); | assertTarContainsEntry("with-modes.tar", "-rwxr-xr-x", "executable"); | ||||
assertTarContainsEntry("with-modes.tar", "l", "symlink -> plain"); | assertTarContainsEntry("with-modes.tar", "l", "symlink -> plain"); | ||||
assertTarContainsEntry("with-modes.tar", "drwxr-xr-x", "dir/"); | |||||
} | } | ||||
@Test | @Test | ||||
public void testArchiveWithLongFilename() throws Exception { | public void testArchiveWithLongFilename() throws Exception { | ||||
String filename = "1234567890"; | |||||
for (int i = 0; i < 20; i++) | |||||
filename = filename + "/1234567890"; | |||||
String filename = ""; | |||||
final List<String> l = new ArrayList<String>(); | |||||
for (int i = 0; i < 20; i++) { | |||||
filename = filename + "1234567890/"; | |||||
l.add(filename); | |||||
} | |||||
filename = filename + "1234567890"; | |||||
l.add(filename); | |||||
writeTrashFile(filename, "file with long path"); | writeTrashFile(filename, "file with long path"); | ||||
git.add().addFilepattern("1234567890").call(); | git.add().addFilepattern("1234567890").call(); | ||||
git.commit().setMessage("file with long name").call(); | git.commit().setMessage("file with long name").call(); | ||||
final byte[] result = CLIGitCommand.rawExecute( // | final byte[] result = CLIGitCommand.rawExecute( // | ||||
"git archive --format=zip HEAD", db); | "git archive --format=zip HEAD", db); | ||||
assertArrayEquals(new String[] { filename }, | |||||
assertArrayEquals(l.toArray(new String[l.size()]), | |||||
listZipEntries(result)); | listZipEntries(result)); | ||||
} | } | ||||
@Test | @Test | ||||
public void testTarWithLongFilename() throws Exception { | public void testTarWithLongFilename() throws Exception { | ||||
String filename = "1234567890"; | |||||
for (int i = 0; i < 20; i++) | |||||
filename = filename + "/1234567890"; | |||||
String filename = ""; | |||||
final List<String> l = new ArrayList<String>(); | |||||
for (int i = 0; i < 20; i++) { | |||||
filename = filename + "1234567890/"; | |||||
l.add(filename); | |||||
} | |||||
filename = filename + "1234567890"; | |||||
l.add(filename); | |||||
writeTrashFile(filename, "file with long path"); | writeTrashFile(filename, "file with long path"); | ||||
git.add().addFilepattern("1234567890").call(); | git.add().addFilepattern("1234567890").call(); | ||||
git.commit().setMessage("file with long name").call(); | git.commit().setMessage("file with long name").call(); | ||||
final byte[] result = CLIGitCommand.rawExecute( // | final byte[] result = CLIGitCommand.rawExecute( // | ||||
"git archive --format=tar HEAD", db); | "git archive --format=tar HEAD", db); | ||||
assertArrayEquals(new String[] { filename }, | |||||
assertArrayEquals(l.toArray(new String[l.size()]), | |||||
listTarEntries(result)); | listTarEntries(result)); | ||||
} | } | ||||
* archive object from createArchiveOutputStream | * archive object from createArchiveOutputStream | ||||
* @param path | * @param path | ||||
* full filename relative to the root of the archive | * full filename relative to the root of the archive | ||||
* (with trailing '/' for directories) | |||||
* @param mode | * @param mode | ||||
* mode (for example FileMode.REGULAR_FILE or | * mode (for example FileMode.REGULAR_FILE or | ||||
* FileMode.SYMLINK) | * FileMode.SYMLINK) | ||||
* @param loader | * @param loader | ||||
* blob object with data for this entry | |||||
* blob object with data for this entry (null for | |||||
* directories) | |||||
* @throws IOException | * @throws IOException | ||||
* thrown by the underlying output stream for I/O errors | * thrown by the underlying output stream for I/O errors | ||||
*/ | */ | ||||
final RevWalk rw = new RevWalk(walk.getObjectReader()); | final RevWalk rw = new RevWalk(walk.getObjectReader()); | ||||
walk.reset(rw.parseTree(tree)); | walk.reset(rw.parseTree(tree)); | ||||
walk.setRecursive(true); | |||||
while (walk.next()) { | while (walk.next()) { | ||||
final String name = pfx + walk.getPathString(); | final String name = pfx + walk.getPathString(); | ||||
final FileMode mode = walk.getFileMode(0); | final FileMode mode = walk.getFileMode(0); | ||||
if (mode == FileMode.TREE) | |||||
// ZIP entries for directories are optional. | |||||
// Leave them out, mimicking "git archive". | |||||
if (walk.isSubtree()) { | |||||
fmt.putEntry(outa, name + "/", mode, null); | |||||
walk.enterSubtree(); | |||||
continue; | continue; | ||||
} | |||||
walk.getObjectId(idBuf, 0); | walk.getObjectId(idBuf, 0); | ||||
fmt.putEntry(outa, name, mode, reader.open(idBuf)); | fmt.putEntry(outa, name, mode, reader.open(idBuf)); | ||||
} | } |