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 }