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("HEAD")).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("master"))
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 }