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 }