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