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