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("HEAD")).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("master"))
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 (TreeWalkeeWalk.html#TreeWalk">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 }