View Javadoc
1   /*
2    * Copyright (C) 2014, Shaul Zorea <shaulzorea@gmail.com> and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
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  	// archives store timestamp with 1 second resolution
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 		// We ignore this test because the txz format consumes a lot of memory for high level
227 		// compressions.
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 			// Do nothing. for testing purposes.
432 		}
433 	}
434 }