1
2
3
4
5
6
7
8
9
10 package org.eclipse.jgit.api;
11
12 import static java.nio.charset.StandardCharsets.UTF_8;
13 import static org.junit.Assert.assertEquals;
14 import static org.junit.Assert.assertNull;
15 import static org.junit.Assert.assertTrue;
16
17 import java.beans.Statement;
18 import java.io.BufferedInputStream;
19 import java.io.File;
20 import java.io.FileNotFoundException;
21 import java.io.FileOutputStream;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.OutputStream;
25 import java.nio.file.Files;
26 import java.util.Arrays;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Map;
31
32 import java.util.Random;
33 import org.apache.commons.compress.archivers.ArchiveEntry;
34 import org.apache.commons.compress.archivers.ArchiveInputStream;
35 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
36 import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
37 import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
38 import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
39 import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
40 import org.eclipse.jgit.api.errors.AbortedByHookException;
41 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
42 import org.eclipse.jgit.api.errors.GitAPIException;
43 import org.eclipse.jgit.api.errors.NoFilepatternException;
44 import org.eclipse.jgit.api.errors.NoHeadException;
45 import org.eclipse.jgit.api.errors.NoMessageException;
46 import org.eclipse.jgit.api.errors.UnmergedPathsException;
47 import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
48 import org.eclipse.jgit.archive.ArchiveFormats;
49 import org.eclipse.jgit.errors.AmbiguousObjectException;
50 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
51 import org.eclipse.jgit.junit.RepositoryTestCase;
52 import org.eclipse.jgit.lib.FileMode;
53 import org.eclipse.jgit.lib.ObjectId;
54 import org.eclipse.jgit.lib.ObjectLoader;
55 import org.eclipse.jgit.revwalk.RevCommit;
56 import org.eclipse.jgit.util.IO;
57 import org.eclipse.jgit.util.StringUtils;
58 import org.junit.After;
59 import org.junit.Before;
60 import org.junit.Ignore;
61 import org.junit.Test;
62
63 public class ArchiveCommandTest extends RepositoryTestCase {
64
65
66 private static final int WAIT = 2000;
67 private static final String UNEXPECTED_ARCHIVE_SIZE = "Unexpected archive size";
68 private static final String UNEXPECTED_FILE_CONTENTS = "Unexpected file contents";
69 private static final String UNEXPECTED_TREE_CONTENTS = "Unexpected tree contents";
70 private static final String UNEXPECTED_LAST_MODIFIED =
71 "Unexpected lastModified mocked by MockSystemReader, truncated to 1 second";
72 private static final String UNEXPECTED_DIFFERENT_HASH = "Unexpected different hash";
73
74 private MockFormat format = null;
75
76 @Before
77 public void setup() {
78 format = new MockFormat();
79 ArchiveCommand.registerFormat(format.SUFFIXES.get(0), format);
80 ArchiveFormats.registerAll();
81 }
82
83 @Override
84 @After
85 public void tearDown() {
86 ArchiveCommand.unregisterFormat(format.SUFFIXES.get(0));
87 ArchiveFormats.unregisterAll();
88 }
89
90 @Test
91 public void archiveHeadAllFiles() throws IOException, GitAPIException {
92 try (Git git = new Git(db)) {
93 createTestContent(git);
94
95 git.archive().setOutputStream(new MockOutputStream())
96 .setFormat(format.SUFFIXES.get(0))
97 .setTree(git.getRepository().resolve("HEAD")).call();
98
99 assertEquals(UNEXPECTED_ARCHIVE_SIZE, 2, format.size());
100 assertEquals(UNEXPECTED_FILE_CONTENTS, "content_1_2", format.getByPath("file_1.txt"));
101 assertEquals(UNEXPECTED_FILE_CONTENTS, "content_2_2", format.getByPath("file_2.txt"));
102 }
103 }
104
105 @Test
106 public void archiveHeadSpecificPath() throws IOException, GitAPIException {
107 try (Git git = new Git(db)) {
108 writeTrashFile("file_1.txt", "content_1_1");
109 git.add().addFilepattern("file_1.txt").call();
110 git.commit().setMessage("create file").call();
111
112 writeTrashFile("file_1.txt", "content_1_2");
113 String expectedFilePath = "some_directory/file_2.txt";
114 writeTrashFile(expectedFilePath, "content_2_2");
115 git.add().addFilepattern(".").call();
116 git.commit().setMessage("updated file").call();
117
118 git.archive().setOutputStream(new MockOutputStream())
119 .setFormat(format.SUFFIXES.get(0))
120 .setTree(git.getRepository().resolve("HEAD"))
121 .setPaths(expectedFilePath).call();
122
123 assertEquals(UNEXPECTED_ARCHIVE_SIZE, 2, format.size());
124 assertEquals(UNEXPECTED_FILE_CONTENTS, "content_2_2", format.getByPath(expectedFilePath));
125 assertNull(UNEXPECTED_TREE_CONTENTS, format.getByPath("some_directory"));
126 }
127 }
128
129 @Test
130 public void archiveByIdSpecificFile() throws IOException, GitAPIException {
131 try (Git git = new Git(db)) {
132 writeTrashFile("file_1.txt", "content_1_1");
133 git.add().addFilepattern("file_1.txt").call();
134 RevCommit first = git.commit().setMessage("create file").call();
135
136 writeTrashFile("file_1.txt", "content_1_2");
137 String expectedFilePath = "some_directory/file_2.txt";
138 writeTrashFile(expectedFilePath, "content_2_2");
139 git.add().addFilepattern(".").call();
140 git.commit().setMessage("updated file").call();
141
142 Map<String, Object> options = new HashMap<>();
143 Integer opt = Integer.valueOf(42);
144 options.put("foo", opt);
145 MockOutputStream out = new MockOutputStream();
146 git.archive().setOutputStream(out)
147 .setFormat(format.SUFFIXES.get(0))
148 .setFormatOptions(options)
149 .setTree(first)
150 .setPaths("file_1.txt").call();
151
152 assertEquals(opt.intValue(), out.getFoo());
153 assertEquals(UNEXPECTED_ARCHIVE_SIZE, 1, format.size());
154 assertEquals(UNEXPECTED_FILE_CONTENTS, "content_1_1", format.getByPath("file_1.txt"));
155 }
156 }
157
158 @Test
159 public void archiveByDirectoryPath() throws GitAPIException, IOException {
160 try (Git git = new Git(db)) {
161 writeTrashFile("file_0.txt", "content_0_1");
162 git.add().addFilepattern("file_0.txt").call();
163 git.commit().setMessage("commit_1").call();
164
165 writeTrashFile("file_0.txt", "content_0_2");
166 String expectedFilePath1 = "some_directory/file_1.txt";
167 writeTrashFile(expectedFilePath1, "content_1_2");
168 String expectedFilePath2 = "some_directory/file_2.txt";
169 writeTrashFile(expectedFilePath2, "content_2_2");
170 String expectedFilePath3 = "some_directory/nested_directory/file_3.txt";
171 writeTrashFile(expectedFilePath3, "content_3_2");
172 git.add().addFilepattern(".").call();
173 git.commit().setMessage("commit_2").call();
174 git.archive().setOutputStream(new MockOutputStream())
175 .setFormat(format.SUFFIXES.get(0))
176 .setTree(git.getRepository().resolve("HEAD"))
177 .setPaths("some_directory/").call();
178
179 assertEquals(UNEXPECTED_ARCHIVE_SIZE, 5, format.size());
180 assertEquals(UNEXPECTED_FILE_CONTENTS, "content_1_2", format.getByPath(expectedFilePath1));
181 assertEquals(UNEXPECTED_FILE_CONTENTS, "content_2_2", format.getByPath(expectedFilePath2));
182 assertEquals(UNEXPECTED_FILE_CONTENTS, "content_3_2", format.getByPath(expectedFilePath3));
183 assertNull(UNEXPECTED_TREE_CONTENTS, format.getByPath("some_directory"));
184 assertNull(UNEXPECTED_TREE_CONTENTS, format.getByPath("some_directory/nested_directory"));
185 }
186 }
187
188 @Test
189 public void archiveHeadAllFilesTarTimestamps() throws Exception {
190 archiveHeadAllFiles("tar");
191 }
192
193 @Test
194 public void archiveHeadAllFilesTgzTimestamps() throws Exception {
195 archiveHeadAllFiles("tgz");
196 }
197
198 @Test
199 public void archiveHeadAllFilesTbz2Timestamps() throws Exception {
200 archiveHeadAllFiles("tbz2");
201 }
202
203 @Test
204 public void archiveHeadAllFilesTxzTimestamps() throws Exception {
205 archiveHeadAllFiles("txz");
206 }
207
208 @Test
209 public void archiveHeadAllFilesZipTimestamps() throws Exception {
210 archiveHeadAllFiles("zip");
211 }
212
213 @Test
214 public void archiveHeadAllFilesTgzWithCompressionReducesArchiveSize() throws Exception {
215 archiveHeadAllFilesWithCompression("tgz");
216 }
217
218 @Test
219 public void archiveHeadAllFilesTbz2WithCompressionReducesArchiveSize() throws Exception {
220 archiveHeadAllFilesWithCompression("tbz2");
221 }
222
223 @Test
224 @Ignore
225 public void archiveHeadAllFilesTxzWithCompressionReducesArchiveSize() throws Exception {
226
227
228 archiveHeadAllFilesWithCompression("txz");
229 }
230
231 @Test
232 public void archiveHeadAllFilesZipWithCompressionReducesArchiveSize() throws Exception {
233 archiveHeadAllFilesWithCompression("zip");
234 }
235
236 private void archiveHeadAllFiles(String fmt) throws Exception {
237 try (Git git = new Git(db)) {
238 createTestContent(git);
239 File archive = new File(getTemporaryDirectory(),
240 "archive." + format);
241 archive(git, archive, fmt);
242 ObjectId hash1 = ObjectId.fromRaw(IO.readFully(archive));
243
244 try (InputStream fi = Files.newInputStream(archive.toPath());
245 InputStream bi = new BufferedInputStream(fi);
246 ArchiveInputStream o = createArchiveInputStream(fmt, bi)) {
247 assertEntries(o);
248 }
249
250 Thread.sleep(WAIT);
251 archive(git, archive, fmt);
252 assertEquals(UNEXPECTED_DIFFERENT_HASH, hash1,
253 ObjectId.fromRaw(IO.readFully(archive)));
254 }
255 }
256
257 @SuppressWarnings({ "serial", "boxing" })
258 private void archiveHeadAllFilesWithCompression(String fmt) throws Exception {
259 try (Git git = new Git(db)) {
260 createLargeTestContent(git);
261 File archive = new File(getTemporaryDirectory(),
262 "archive." + format);
263
264 archive(git, archive, fmt, new HashMap<String, Object>() {{
265 put("compression-level", 1);
266 }});
267 int sizeCompression1 = getNumBytes(archive);
268
269 archive(git, archive, fmt, new HashMap<String, Object>() {{
270 put("compression-level", 9);
271 }});
272 int sizeCompression9 = getNumBytes(archive);
273
274 assertTrue(sizeCompression1 > sizeCompression9);
275 }
276 }
277
278 private static ArchiveInputStream createArchiveInputStream (String fmt, InputStream bi)
279 throws IOException {
280 switch (fmt) {
281 case "tar":
282 return new TarArchiveInputStream(bi);
283 case "tgz":
284 return new TarArchiveInputStream(new GzipCompressorInputStream(bi));
285 case "tbz2":
286 return new TarArchiveInputStream(new BZip2CompressorInputStream(bi));
287 case "txz":
288 return new TarArchiveInputStream(new XZCompressorInputStream(bi));
289 case "zip":
290 return new ZipArchiveInputStream(new BufferedInputStream(bi));
291 }
292 throw new IllegalArgumentException("Format " + fmt + " is not supported.");
293 }
294
295 private void createTestContent(Git git) throws IOException, GitAPIException,
296 NoFilepatternException, NoHeadException, NoMessageException,
297 UnmergedPathsException, ConcurrentRefUpdateException,
298 WrongRepositoryStateException, AbortedByHookException {
299 writeTrashFile("file_1.txt", "content_1_1");
300 git.add().addFilepattern("file_1.txt").call();
301 git.commit().setMessage("create file").call();
302
303 writeTrashFile("file_1.txt", "content_1_2");
304 writeTrashFile("file_2.txt", "content_2_2");
305 git.add().addFilepattern(".").call();
306 git.commit().setMessage("updated file").call();
307 }
308
309 private void createLargeTestContent(Git git) throws IOException, GitAPIException,
310 NoFilepatternException, NoHeadException, NoMessageException,
311 UnmergedPathsException, ConcurrentRefUpdateException,
312 WrongRepositoryStateException, AbortedByHookException {
313 StringBuilder largeContent = new StringBuilder();
314 Random r = new Random();
315 for (int i = 0; i < 2000; i++) {
316 for (int j = 0; j < 80; j++) {
317 largeContent.append((char)(r.nextInt(26) + 'a'));
318 }
319 largeContent.append("\n");
320 }
321 writeTrashFile("large_file.txt", largeContent.toString());
322 git.add().addFilepattern("large_file.txt").call();
323 git.commit().setMessage("create file").call();
324 }
325
326 private static void archive(Git git, File archive, String fmt)
327 throws GitAPIException,
328 FileNotFoundException, AmbiguousObjectException,
329 IncorrectObjectTypeException, IOException {
330 archive(git, archive, fmt, new HashMap<>());
331 }
332
333 private static void archive(Git git, File archive, String fmt, Map<String,
334 Object> options)
335 throws GitAPIException,
336 FileNotFoundException, AmbiguousObjectException,
337 IncorrectObjectTypeException, IOException {
338 git.archive().setOutputStream(new FileOutputStream(archive))
339 .setFormat(fmt)
340 .setTree(git.getRepository().resolve("HEAD"))
341 .setFormatOptions(options)
342 .call();
343 }
344
345 private static void assertEntries(ArchiveInputStream o) throws IOException {
346 ArchiveEntry e;
347 int n = 0;
348 while ((e = o.getNextEntry()) != null) {
349 n++;
350 assertEquals(UNEXPECTED_LAST_MODIFIED,
351 (1250379778668L / 1000L) * 1000L,
352 e.getLastModifiedDate().getTime());
353 }
354 assertEquals(UNEXPECTED_ARCHIVE_SIZE, 2, n);
355 }
356
357 private static int getNumBytes(File archive) throws Exception {
358 try (InputStream fi = Files.newInputStream(archive.toPath());
359 InputStream bi = new BufferedInputStream(fi)) {
360 return bi.available();
361 }
362 }
363
364 private static class MockFormat
365 implements ArchiveCommand.Format<MockOutputStream> {
366
367 private Map<String, String> entries = new HashMap<>();
368
369 private int size() {
370 return entries.size();
371 }
372
373 private String getByPath(String path) {
374 return entries.get(path);
375 }
376
377 private final List<String> SUFFIXES = Collections
378 .unmodifiableList(Arrays.asList(".mck"));
379
380 @Override
381 public MockOutputStream createArchiveOutputStream(OutputStream s)
382 throws IOException {
383 return createArchiveOutputStream(s,
384 Collections.<String, Object> emptyMap());
385 }
386
387 @Override
388 public MockOutputStream createArchiveOutputStream(OutputStream s,
389 Map<String, Object> o) throws IOException {
390 for (Map.Entry<String, Object> p : o.entrySet()) {
391 try {
392 String methodName = "set"
393 + StringUtils.capitalize(p.getKey());
394 new Statement(s, methodName, new Object[] { p.getValue() })
395 .execute();
396 } catch (Exception e) {
397 throw new IOException("cannot set option: " + p.getKey(), e);
398 }
399 }
400 return new MockOutputStream();
401 }
402
403 @Override
404 public void putEntry(MockOutputStream out, ObjectId tree, String path, FileMode mode, ObjectLoader loader) {
405 String content = mode != FileMode.TREE
406 ? new String(loader.getBytes(), UTF_8)
407 : null;
408 entries.put(path, content);
409 }
410
411 @Override
412 public Iterable<String> suffixes() {
413 return SUFFIXES;
414 }
415 }
416
417 public static class MockOutputStream extends OutputStream {
418
419 private int foo;
420
421 public void setFoo(int foo) {
422 this.foo = foo;
423 }
424
425 public int getFoo() {
426 return foo;
427 }
428
429 @Override
430 public void write(int b) throws IOException {
431
432 }
433 }
434 }