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