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