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