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