View Javadoc
1   /*
2    * Copyright (C) 2012 Google Inc. 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 java.io.Closeable;
13  import java.io.IOException;
14  import java.io.OutputStream;
15  import java.text.MessageFormat;
16  import java.util.ArrayList;
17  import java.util.Arrays;
18  import java.util.HashMap;
19  import java.util.List;
20  import java.util.Map;
21  import java.util.concurrent.ConcurrentHashMap;
22  
23  import org.eclipse.jgit.api.errors.GitAPIException;
24  import org.eclipse.jgit.api.errors.JGitInternalException;
25  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
26  import org.eclipse.jgit.internal.JGitText;
27  import org.eclipse.jgit.lib.Constants;
28  import org.eclipse.jgit.lib.FileMode;
29  import org.eclipse.jgit.lib.MutableObjectId;
30  import org.eclipse.jgit.lib.ObjectId;
31  import org.eclipse.jgit.lib.ObjectLoader;
32  import org.eclipse.jgit.lib.ObjectReader;
33  import org.eclipse.jgit.lib.Repository;
34  import org.eclipse.jgit.revwalk.RevCommit;
35  import org.eclipse.jgit.revwalk.RevObject;
36  import org.eclipse.jgit.revwalk.RevTree;
37  import org.eclipse.jgit.revwalk.RevWalk;
38  import org.eclipse.jgit.treewalk.TreeWalk;
39  import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
40  
41  /**
42   * Create an archive of files from a named tree.
43   * <p>
44   * Examples (<code>git</code> is a {@link org.eclipse.jgit.api.Git} instance):
45   * <p>
46   * Create a tarball from HEAD:
47   *
48   * <pre>
49   * ArchiveCommand.registerFormat("tar", new TarFormat());
50   * try {
51   * 	git.archive().setTree(db.resolve(&quot;HEAD&quot;)).setOutputStream(out).call();
52   * } finally {
53   * 	ArchiveCommand.unregisterFormat("tar");
54   * }
55   * </pre>
56   * <p>
57   * Create a ZIP file from master:
58   *
59   * <pre>
60   * ArchiveCommand.registerFormat("zip", new ZipFormat());
61   * try {
62   *	git.archive().
63   *		.setTree(db.resolve(&quot;master&quot;))
64   *		.setFormat("zip")
65   *		.setOutputStream(out)
66   *		.call();
67   * } finally {
68   *	ArchiveCommand.unregisterFormat("zip");
69   * }
70   * </pre>
71   *
72   * @see <a href="http://git-htmldocs.googlecode.com/git/git-archive.html" >Git
73   *      documentation about archive</a>
74   * @since 3.1
75   */
76  public class ArchiveCommand extends GitCommand<OutputStream> {
77  	/**
78  	 * Archival format.
79  	 *
80  	 * Usage:
81  	 *	Repository repo = git.getRepository();
82  	 *	T out = format.createArchiveOutputStream(System.out);
83  	 *	try {
84  	 *		for (...) {
85  	 *			format.putEntry(out, path, mode, repo.open(objectId));
86  	 *		}
87  	 *		out.close();
88  	 *	}
89  	 *
90  	 * @param <T>
91  	 *            type representing an archive being created.
92  	 */
93  	public static interface Format<T extends Closeable> {
94  		/**
95  		 * Start a new archive. Entries can be included in the archive using the
96  		 * putEntry method, and then the archive should be closed using its
97  		 * close method.
98  		 *
99  		 * @param s
100 		 *            underlying output stream to which to write the archive.
101 		 * @return new archive object for use in putEntry
102 		 * @throws IOException
103 		 *             thrown by the underlying output stream for I/O errors
104 		 */
105 		T createArchiveOutputStream(OutputStream s) throws IOException;
106 
107 		/**
108 		 * Start a new archive. Entries can be included in the archive using the
109 		 * putEntry method, and then the archive should be closed using its
110 		 * close method. In addition options can be applied to the underlying
111 		 * stream. E.g. compression level.
112 		 *
113 		 * @param s
114 		 *            underlying output stream to which to write the archive.
115 		 * @param o
116 		 *            options to apply to the underlying output stream. Keys are
117 		 *            option names and values are option values.
118 		 * @return new archive object for use in putEntry
119 		 * @throws IOException
120 		 *             thrown by the underlying output stream for I/O errors
121 		 * @since 4.0
122 		 */
123 		T createArchiveOutputStream(OutputStream s, Map<String, Object> o)
124 				throws IOException;
125 
126 		/**
127 		 * Write an entry to an archive.
128 		 *
129 		 * @param out
130 		 *            archive object from createArchiveOutputStream
131 		 * @param tree
132 		 *            the tag, commit, or tree object to produce an archive for
133 		 * @param path
134 		 *            full filename relative to the root of the archive (with
135 		 *            trailing '/' for directories)
136 		 * @param mode
137 		 *            mode (for example FileMode.REGULAR_FILE or
138 		 *            FileMode.SYMLINK)
139 		 * @param loader
140 		 *            blob object with data for this entry (null for
141 		 *            directories)
142 		 * @throws IOException
143 		 *             thrown by the underlying output stream for I/O errors
144 		 * @since 4.7
145 		 */
146 		void putEntry(T out, ObjectId tree, String path, FileMode mode,
147 				ObjectLoader loader) throws IOException;
148 
149 		/**
150 		 * Filename suffixes representing this format (e.g.,
151 		 * { ".tar.gz", ".tgz" }).
152 		 *
153 		 * The behavior is undefined when suffixes overlap (if
154 		 * one format claims suffix ".7z", no other format should
155 		 * take ".tar.7z").
156 		 *
157 		 * @return this format's suffixes
158 		 */
159 		Iterable<String> suffixes();
160 	}
161 
162 	/**
163 	 * Signals an attempt to use an archival format that ArchiveCommand
164 	 * doesn't know about (for example due to a typo).
165 	 */
166 	public static class UnsupportedFormatException extends GitAPIException {
167 		private static final long serialVersionUID = 1L;
168 
169 		private final String format;
170 
171 		/**
172 		 * @param format the problematic format name
173 		 */
174 		public UnsupportedFormatException(String format) {
175 			super(MessageFormat.format(JGitText.get().unsupportedArchiveFormat, format));
176 			this.format = format;
177 		}
178 
179 		/**
180 		 * @return the problematic format name
181 		 */
182 		public String getFormat() {
183 			return format;
184 		}
185 	}
186 
187 	private static class FormatEntry {
188 		final Format<?> format;
189 		/** Number of times this format has been registered. */
190 		final int refcnt;
191 
192 		public FormatEntry(Format<?> format, int refcnt) {
193 			if (format == null)
194 				throw new NullPointerException();
195 			this.format = format;
196 			this.refcnt = refcnt;
197 		}
198 	}
199 
200 	/**
201 	 * Available archival formats (corresponding to values for
202 	 * the --format= option)
203 	 */
204 	private static final Map<String, FormatEntry> formats =
205 			new ConcurrentHashMap<>();
206 
207 	/**
208 	 * Replaces the entry for a key only if currently mapped to a given
209 	 * value.
210 	 *
211 	 * @param map a map
212 	 * @param key key with which the specified value is associated
213 	 * @param oldValue expected value for the key (null if should be absent).
214 	 * @param newValue value to be associated with the key (null to remove).
215 	 * @return true if the value was replaced
216 	 */
217 	private static <K, V> boolean replace(Map<K, V> map,
218 			K key, V oldValue, V newValue) {
219 		if (oldValue == null && newValue == null) // Nothing to do.
220 			return true;
221 
222 		if (oldValue == null)
223 			return map.putIfAbsent(key, newValue) == null;
224 		else if (newValue == null)
225 			return map.remove(key, oldValue);
226 		else
227 			return map.replace(key, oldValue, newValue);
228 	}
229 
230 	/**
231 	 * Adds support for an additional archival format.  To avoid
232 	 * unnecessary dependencies, ArchiveCommand does not have support
233 	 * for any formats built in; use this function to add them.
234 	 * <p>
235 	 * OSGi plugins providing formats should call this function at
236 	 * bundle activation time.
237 	 * <p>
238 	 * It is okay to register the same archive format with the same
239 	 * name multiple times, but don't forget to unregister it that
240 	 * same number of times, too.
241 	 * <p>
242 	 * Registering multiple formats with different names and the
243 	 * same or overlapping suffixes results in undefined behavior.
244 	 * TODO: check that suffixes don't overlap.
245 	 *
246 	 * @param name name of a format (e.g., "tar" or "zip").
247 	 * @param fmt archiver for that format
248 	 * @throws JGitInternalException
249 	 *              A different archival format with that name was
250 	 *              already registered.
251 	 */
252 	public static void registerFormat(String name, Format<?> fmt) {
253 		if (fmt == null)
254 			throw new NullPointerException();
255 
256 		FormatEntry old, entry;
257 		do {
258 			old = formats.get(name);
259 			if (old == null) {
260 				entry = new FormatEntry(fmt, 1);
261 				continue;
262 			}
263 			if (!old.format.equals(fmt))
264 				throw new JGitInternalException(MessageFormat.format(
265 						JGitText.get().archiveFormatAlreadyRegistered,
266 						name));
267 			entry = new FormatEntry(old.format, old.refcnt + 1);
268 		} while (!replace(formats, name, old, entry));
269 	}
270 
271 	/**
272 	 * Marks support for an archival format as no longer needed so its
273 	 * Format can be garbage collected if no one else is using it either.
274 	 * <p>
275 	 * In other words, this decrements the reference count for an
276 	 * archival format.  If the reference count becomes zero, removes
277 	 * support for that format.
278 	 *
279 	 * @param name name of format (e.g., "tar" or "zip").
280 	 * @throws JGitInternalException
281 	 *              No such archival format was registered.
282 	 */
283 	public static void unregisterFormat(String name) {
284 		FormatEntry old, entry;
285 		do {
286 			old = formats.get(name);
287 			if (old == null)
288 				throw new JGitInternalException(MessageFormat.format(
289 						JGitText.get().archiveFormatAlreadyAbsent,
290 						name));
291 			if (old.refcnt == 1) {
292 				entry = null;
293 				continue;
294 			}
295 			entry = new FormatEntry(old.format, old.refcnt - 1);
296 		} while (!replace(formats, name, old, entry));
297 	}
298 
299 	private static Format<?> formatBySuffix(String filenameSuffix)
300 			throws UnsupportedFormatException {
301 		if (filenameSuffix != null)
302 			for (FormatEntry entry : formats.values()) {
303 				Format<?> fmt = entry.format;
304 				for (String sfx : fmt.suffixes())
305 					if (filenameSuffix.endsWith(sfx))
306 						return fmt;
307 			}
308 		return lookupFormat("tar"); //$NON-NLS-1$
309 	}
310 
311 	private static Format<?> lookupFormat(String formatName) throws UnsupportedFormatException {
312 		FormatEntry entry = formats.get(formatName);
313 		if (entry == null)
314 			throw new UnsupportedFormatException(formatName);
315 		return entry.format;
316 	}
317 
318 	private OutputStream out;
319 	private ObjectId tree;
320 	private String prefix;
321 	private String format;
322 	private Map<String, Object> formatOptions = new HashMap<>();
323 	private List<String> paths = new ArrayList<>();
324 
325 	/** Filename suffix, for automatically choosing a format. */
326 	private String suffix;
327 
328 	/**
329 	 * Constructor for ArchiveCommand
330 	 *
331 	 * @param repo
332 	 *            the {@link org.eclipse.jgit.lib.Repository}
333 	 */
334 	public ArchiveCommand(Repository repo) {
335 		super(repo);
336 		setCallable(false);
337 	}
338 
339 	private <T extends Closeable> OutputStream writeArchive(Format<T> fmt) {
340 		try {
341 			try (TreeWalk walk = new TreeWalk(repo);
342 					RevWalk rw = new RevWalk(walk.getObjectReader());
343 					T outa = fmt.createArchiveOutputStream(out,
344 							formatOptions)) {
345 				String pfx = prefix == null ? "" : prefix; //$NON-NLS-1$
346 				MutableObjectId idBuf = new MutableObjectId();
347 				ObjectReader reader = walk.getObjectReader();
348 
349 				RevObject o = rw.peel(rw.parseAny(tree));
350 				walk.reset(getTree(o));
351 				if (!paths.isEmpty()) {
352 					walk.setFilter(PathFilterGroup.createFromStrings(paths));
353 				}
354 
355 				// Put base directory into archive
356 				if (pfx.endsWith("/")) { //$NON-NLS-1$
357 					fmt.putEntry(outa, o, pfx.replaceAll("[/]+$", "/"), //$NON-NLS-1$ //$NON-NLS-2$
358 							FileMode.TREE, null);
359 				}
360 
361 				while (walk.next()) {
362 					String name = pfx + walk.getPathString();
363 					FileMode mode = walk.getFileMode(0);
364 
365 					if (walk.isSubtree())
366 						walk.enterSubtree();
367 
368 					if (mode == FileMode.GITLINK) {
369 						// TODO(jrn): Take a callback to recurse
370 						// into submodules.
371 						mode = FileMode.TREE;
372 					}
373 
374 					if (mode == FileMode.TREE) {
375 						fmt.putEntry(outa, o, name + "/", mode, null); //$NON-NLS-1$
376 						continue;
377 					}
378 					walk.getObjectId(idBuf, 0);
379 					fmt.putEntry(outa, o, name, mode, reader.open(idBuf));
380 				}
381 				return out;
382 			} finally {
383 				out.close();
384 			}
385 		} catch (IOException e) {
386 			// TODO(jrn): Throw finer-grained errors.
387 			throw new JGitInternalException(
388 					JGitText.get().exceptionCaughtDuringExecutionOfArchiveCommand, e);
389 		}
390 	}
391 
392 	/** {@inheritDoc} */
393 	@Override
394 	public OutputStream call() throws GitAPIException {
395 		checkCallable();
396 
397 		Format<?> fmt;
398 		if (format == null)
399 			fmt = formatBySuffix(suffix);
400 		else
401 			fmt = lookupFormat(format);
402 		return writeArchive(fmt);
403 	}
404 
405 	/**
406 	 * Set the tag, commit, or tree object to produce an archive for
407 	 *
408 	 * @param tree
409 	 *            the tag, commit, or tree object to produce an archive for
410 	 * @return this
411 	 */
412 	public ArchiveCommand setTree(ObjectId tree) {
413 		if (tree == null)
414 			throw new IllegalArgumentException();
415 
416 		this.tree = tree;
417 		setCallable(true);
418 		return this;
419 	}
420 
421 	/**
422 	 * Set string prefixed to filenames in archive
423 	 *
424 	 * @param prefix
425 	 *            string prefixed to filenames in archive (e.g., "master/").
426 	 *            null means to not use any leading prefix.
427 	 * @return this
428 	 * @since 3.3
429 	 */
430 	public ArchiveCommand setPrefix(String prefix) {
431 		this.prefix = prefix;
432 		return this;
433 	}
434 
435 	/**
436 	 * Set the intended filename for the produced archive. Currently the only
437 	 * effect is to determine the default archive format when none is specified
438 	 * with {@link #setFormat(String)}.
439 	 *
440 	 * @param filename
441 	 *            intended filename for the archive
442 	 * @return this
443 	 */
444 	public ArchiveCommand setFilename(String filename) {
445 		int slash = filename.lastIndexOf('/');
446 		int dot = filename.indexOf('.', slash + 1);
447 
448 		if (dot == -1)
449 			this.suffix = ""; //$NON-NLS-1$
450 		else
451 			this.suffix = filename.substring(dot);
452 		return this;
453 	}
454 
455 	/**
456 	 * Set output stream
457 	 *
458 	 * @param out
459 	 *            the stream to which to write the archive
460 	 * @return this
461 	 */
462 	public ArchiveCommand setOutputStream(OutputStream out) {
463 		this.out = out;
464 		return this;
465 	}
466 
467 	/**
468 	 * Set archive format
469 	 *
470 	 * @param fmt
471 	 *            archive format (e.g., "tar" or "zip"). null means to choose
472 	 *            automatically based on the archive filename.
473 	 * @return this
474 	 */
475 	public ArchiveCommand setFormat(String fmt) {
476 		this.format = fmt;
477 		return this;
478 	}
479 
480 	/**
481 	 * Set archive format options
482 	 *
483 	 * @param options
484 	 *            archive format options (e.g., level=9 for zip compression).
485 	 * @return this
486 	 * @since 4.0
487 	 */
488 	public ArchiveCommand setFormatOptions(Map<String, Object> options) {
489 		this.formatOptions = options;
490 		return this;
491 	}
492 
493 	/**
494 	 * Set an optional parameter path. without an optional path parameter, all
495 	 * files and subdirectories of the current working directory are included in
496 	 * the archive. If one or more paths are specified, only these are included.
497 	 *
498 	 * @param paths
499 	 *            file names (e.g <code>file1.c</code>) or directory names (e.g.
500 	 *            <code>dir</code> to add <code>dir/file1</code> and
501 	 *            <code>dir/file2</code>) can also be given to add all files in
502 	 *            the directory, recursively. Fileglobs (e.g. *.c) are not yet
503 	 *            supported.
504 	 * @return this
505 	 * @since 3.4
506 	 */
507 	public ArchiveCommand setPaths(String... paths) {
508 		this.paths = Arrays.asList(paths);
509 		return this;
510 	}
511 
512 	private RevTree getTree(RevObject o)
513 			throws IncorrectObjectTypeException {
514 		final RevTree t;
515 		if (o instanceof RevCommit) {
516 			t = ((RevCommit) o).getTree();
517 		} else if (!(o instanceof RevTree)) {
518 			throw new IncorrectObjectTypeException(tree.toObjectId(),
519 					Constants.TYPE_TREE);
520 		} else {
521 			t = (RevTree) o;
522 		}
523 		return t;
524 	}
525 
526 }