ArchiveCommand.java

  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. import java.io.Closeable;
  45. import java.io.IOException;
  46. import java.io.OutputStream;
  47. import java.text.MessageFormat;
  48. import java.util.ArrayList;
  49. import java.util.Arrays;
  50. import java.util.HashMap;
  51. import java.util.List;
  52. import java.util.Map;
  53. import java.util.concurrent.ConcurrentHashMap;
  54. import java.util.concurrent.ConcurrentMap;

  55. import org.eclipse.jgit.api.errors.GitAPIException;
  56. import org.eclipse.jgit.api.errors.JGitInternalException;
  57. import org.eclipse.jgit.errors.IncorrectObjectTypeException;
  58. import org.eclipse.jgit.internal.JGitText;
  59. import org.eclipse.jgit.lib.Constants;
  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.RevCommit;
  67. import org.eclipse.jgit.revwalk.RevObject;
  68. import org.eclipse.jgit.revwalk.RevTree;
  69. import org.eclipse.jgit.revwalk.RevWalk;
  70. import org.eclipse.jgit.treewalk.TreeWalk;
  71. import org.eclipse.jgit.treewalk.filter.PathFilterGroup;

  72. /**
  73.  * Create an archive of files from a named tree.
  74.  * <p>
  75.  * Examples (<code>git</code> is a {@link org.eclipse.jgit.api.Git} instance):
  76.  * <p>
  77.  * Create a tarball from HEAD:
  78.  *
  79.  * <pre>
  80.  * ArchiveCommand.registerFormat("tar", new TarFormat());
  81.  * try {
  82.  *  git.archive().setTree(db.resolve(&quot;HEAD&quot;)).setOutputStream(out).call();
  83.  * } finally {
  84.  *  ArchiveCommand.unregisterFormat("tar");
  85.  * }
  86.  * </pre>
  87.  * <p>
  88.  * Create a ZIP file from master:
  89.  *
  90.  * <pre>
  91.  * ArchiveCommand.registerFormat("zip", new ZipFormat());
  92.  * try {
  93.  *  git.archive().
  94.  *      .setTree(db.resolve(&quot;master&quot;))
  95.  *      .setFormat("zip")
  96.  *      .setOutputStream(out)
  97.  *      .call();
  98.  * } finally {
  99.  *  ArchiveCommand.unregisterFormat("zip");
  100.  * }
  101.  * </pre>
  102.  *
  103.  * @see <a href="http://git-htmldocs.googlecode.com/git/git-archive.html" >Git
  104.  *      documentation about archive</a>
  105.  * @since 3.1
  106.  */
  107. public class ArchiveCommand extends GitCommand<OutputStream> {
  108.     /**
  109.      * Archival format.
  110.      *
  111.      * Usage:
  112.      *  Repository repo = git.getRepository();
  113.      *  T out = format.createArchiveOutputStream(System.out);
  114.      *  try {
  115.      *      for (...) {
  116.      *          format.putEntry(out, path, mode, repo.open(objectId));
  117.      *      }
  118.      *      out.close();
  119.      *  }
  120.      *
  121.      * @param <T>
  122.      *            type representing an archive being created.
  123.      */
  124.     public static interface Format<T extends Closeable> {
  125.         /**
  126.          * Start a new archive. Entries can be included in the archive using the
  127.          * putEntry method, and then the archive should be closed using its
  128.          * close method.
  129.          *
  130.          * @param s
  131.          *            underlying output stream to which to write the archive.
  132.          * @return new archive object for use in putEntry
  133.          * @throws IOException
  134.          *             thrown by the underlying output stream for I/O errors
  135.          */
  136.         T createArchiveOutputStream(OutputStream s) throws IOException;

  137.         /**
  138.          * Start a new archive. Entries can be included in the archive using the
  139.          * putEntry method, and then the archive should be closed using its
  140.          * close method. In addition options can be applied to the underlying
  141.          * stream. E.g. compression level.
  142.          *
  143.          * @param s
  144.          *            underlying output stream to which to write the archive.
  145.          * @param o
  146.          *            options to apply to the underlying output stream. Keys are
  147.          *            option names and values are option values.
  148.          * @return new archive object for use in putEntry
  149.          * @throws IOException
  150.          *             thrown by the underlying output stream for I/O errors
  151.          * @since 4.0
  152.          */
  153.         T createArchiveOutputStream(OutputStream s, Map<String, Object> o)
  154.                 throws IOException;

  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.          * Filename suffixes representing this format (e.g.,
  179.          * { ".tar.gz", ".tgz" }).
  180.          *
  181.          * The behavior is undefined when suffixes overlap (if
  182.          * one format claims suffix ".7z", no other format should
  183.          * take ".tar.7z").
  184.          *
  185.          * @return this format's suffixes
  186.          */
  187.         Iterable<String> suffixes();
  188.     }

  189.     /**
  190.      * Signals an attempt to use an archival format that ArchiveCommand
  191.      * doesn't know about (for example due to a typo).
  192.      */
  193.     public static class UnsupportedFormatException extends GitAPIException {
  194.         private static final long serialVersionUID = 1L;

  195.         private final String format;

  196.         /**
  197.          * @param format the problematic format name
  198.          */
  199.         public UnsupportedFormatException(String format) {
  200.             super(MessageFormat.format(JGitText.get().unsupportedArchiveFormat, format));
  201.             this.format = format;
  202.         }

  203.         /**
  204.          * @return the problematic format name
  205.          */
  206.         public String getFormat() {
  207.             return format;
  208.         }
  209.     }

  210.     private static class FormatEntry {
  211.         final Format<?> format;
  212.         /** Number of times this format has been registered. */
  213.         final int refcnt;

  214.         public FormatEntry(Format<?> format, int refcnt) {
  215.             if (format == null)
  216.                 throw new NullPointerException();
  217.             this.format = format;
  218.             this.refcnt = refcnt;
  219.         }
  220.     }

  221.     /**
  222.      * Available archival formats (corresponding to values for
  223.      * the --format= option)
  224.      */
  225.     private static final ConcurrentMap<String, FormatEntry> formats =
  226.             new ConcurrentHashMap<>();

  227.     /**
  228.      * Replaces the entry for a key only if currently mapped to a given
  229.      * value.
  230.      *
  231.      * @param map a map
  232.      * @param key key with which the specified value is associated
  233.      * @param oldValue expected value for the key (null if should be absent).
  234.      * @param newValue value to be associated with the key (null to remove).
  235.      * @return true if the value was replaced
  236.      */
  237.     private static <K, V> boolean replace(ConcurrentMap<K, V> map,
  238.             K key, V oldValue, V newValue) {
  239.         if (oldValue == null && newValue == null) // Nothing to do.
  240.             return true;

  241.         if (oldValue == null)
  242.             return map.putIfAbsent(key, newValue) == null;
  243.         else if (newValue == null)
  244.             return map.remove(key, oldValue);
  245.         else
  246.             return map.replace(key, oldValue, newValue);
  247.     }

  248.     /**
  249.      * Adds support for an additional archival format.  To avoid
  250.      * unnecessary dependencies, ArchiveCommand does not have support
  251.      * for any formats built in; use this function to add them.
  252.      * <p>
  253.      * OSGi plugins providing formats should call this function at
  254.      * bundle activation time.
  255.      * <p>
  256.      * It is okay to register the same archive format with the same
  257.      * name multiple times, but don't forget to unregister it that
  258.      * same number of times, too.
  259.      * <p>
  260.      * Registering multiple formats with different names and the
  261.      * same or overlapping suffixes results in undefined behavior.
  262.      * TODO: check that suffixes don't overlap.
  263.      *
  264.      * @param name name of a format (e.g., "tar" or "zip").
  265.      * @param fmt archiver for that format
  266.      * @throws JGitInternalException
  267.      *              A different archival format with that name was
  268.      *              already registered.
  269.      */
  270.     public static void registerFormat(String name, Format<?> fmt) {
  271.         if (fmt == null)
  272.             throw new NullPointerException();

  273.         FormatEntry old, entry;
  274.         do {
  275.             old = formats.get(name);
  276.             if (old == null) {
  277.                 entry = new FormatEntry(fmt, 1);
  278.                 continue;
  279.             }
  280.             if (!old.format.equals(fmt))
  281.                 throw new JGitInternalException(MessageFormat.format(
  282.                         JGitText.get().archiveFormatAlreadyRegistered,
  283.                         name));
  284.             entry = new FormatEntry(old.format, old.refcnt + 1);
  285.         } while (!replace(formats, name, old, entry));
  286.     }

  287.     /**
  288.      * Marks support for an archival format as no longer needed so its
  289.      * Format can be garbage collected if no one else is using it either.
  290.      * <p>
  291.      * In other words, this decrements the reference count for an
  292.      * archival format.  If the reference count becomes zero, removes
  293.      * support for that format.
  294.      *
  295.      * @param name name of format (e.g., "tar" or "zip").
  296.      * @throws JGitInternalException
  297.      *              No such archival format was registered.
  298.      */
  299.     public static void unregisterFormat(String name) {
  300.         FormatEntry old, entry;
  301.         do {
  302.             old = formats.get(name);
  303.             if (old == null)
  304.                 throw new JGitInternalException(MessageFormat.format(
  305.                         JGitText.get().archiveFormatAlreadyAbsent,
  306.                         name));
  307.             if (old.refcnt == 1) {
  308.                 entry = null;
  309.                 continue;
  310.             }
  311.             entry = new FormatEntry(old.format, old.refcnt - 1);
  312.         } while (!replace(formats, name, old, entry));
  313.     }

  314.     private static Format<?> formatBySuffix(String filenameSuffix)
  315.             throws UnsupportedFormatException {
  316.         if (filenameSuffix != null)
  317.             for (FormatEntry entry : formats.values()) {
  318.                 Format<?> fmt = entry.format;
  319.                 for (String sfx : fmt.suffixes())
  320.                     if (filenameSuffix.endsWith(sfx))
  321.                         return fmt;
  322.             }
  323.         return lookupFormat("tar"); //$NON-NLS-1$
  324.     }

  325.     private static Format<?> lookupFormat(String formatName) throws UnsupportedFormatException {
  326.         FormatEntry entry = formats.get(formatName);
  327.         if (entry == null)
  328.             throw new UnsupportedFormatException(formatName);
  329.         return entry.format;
  330.     }

  331.     private OutputStream out;
  332.     private ObjectId tree;
  333.     private String prefix;
  334.     private String format;
  335.     private Map<String, Object> formatOptions = new HashMap<>();
  336.     private List<String> paths = new ArrayList<>();

  337.     /** Filename suffix, for automatically choosing a format. */
  338.     private String suffix;

  339.     /**
  340.      * Constructor for ArchiveCommand
  341.      *
  342.      * @param repo
  343.      *            the {@link org.eclipse.jgit.lib.Repository}
  344.      */
  345.     public ArchiveCommand(Repository repo) {
  346.         super(repo);
  347.         setCallable(false);
  348.     }

  349.     private <T extends Closeable> OutputStream writeArchive(Format<T> fmt) {
  350.         try {
  351.             try (TreeWalk walk = new TreeWalk(repo);
  352.                     RevWalk rw = new RevWalk(walk.getObjectReader());
  353.                     T outa = fmt.createArchiveOutputStream(out,
  354.                             formatOptions)) {
  355.                 String pfx = prefix == null ? "" : prefix; //$NON-NLS-1$
  356.                 MutableObjectId idBuf = new MutableObjectId();
  357.                 ObjectReader reader = walk.getObjectReader();

  358.                 RevObject o = rw.peel(rw.parseAny(tree));
  359.                 walk.reset(getTree(o));
  360.                 if (!paths.isEmpty()) {
  361.                     walk.setFilter(PathFilterGroup.createFromStrings(paths));
  362.                 }

  363.                 // Put base directory into archive
  364.                 if (pfx.endsWith("/")) { //$NON-NLS-1$
  365.                     fmt.putEntry(outa, o, pfx.replaceAll("[/]+$", "/"), //$NON-NLS-1$ //$NON-NLS-2$
  366.                             FileMode.TREE, null);
  367.                 }

  368.                 while (walk.next()) {
  369.                     String name = pfx + walk.getPathString();
  370.                     FileMode mode = walk.getFileMode(0);

  371.                     if (walk.isSubtree())
  372.                         walk.enterSubtree();

  373.                     if (mode == FileMode.GITLINK) {
  374.                         // TODO(jrn): Take a callback to recurse
  375.                         // into submodules.
  376.                         mode = FileMode.TREE;
  377.                     }

  378.                     if (mode == FileMode.TREE) {
  379.                         fmt.putEntry(outa, o, name + "/", mode, null); //$NON-NLS-1$
  380.                         continue;
  381.                     }
  382.                     walk.getObjectId(idBuf, 0);
  383.                     fmt.putEntry(outa, o, name, mode, reader.open(idBuf));
  384.                 }
  385.                 return out;
  386.             } finally {
  387.                 out.close();
  388.             }
  389.         } catch (IOException e) {
  390.             // TODO(jrn): Throw finer-grained errors.
  391.             throw new JGitInternalException(
  392.                     JGitText.get().exceptionCaughtDuringExecutionOfArchiveCommand, e);
  393.         }
  394.     }

  395.     /** {@inheritDoc} */
  396.     @Override
  397.     public OutputStream call() throws GitAPIException {
  398.         checkCallable();

  399.         Format<?> fmt;
  400.         if (format == null)
  401.             fmt = formatBySuffix(suffix);
  402.         else
  403.             fmt = lookupFormat(format);
  404.         return writeArchive(fmt);
  405.     }

  406.     /**
  407.      * Set the tag, commit, or tree object to produce an archive for
  408.      *
  409.      * @param tree
  410.      *            the tag, commit, or tree object to produce an archive for
  411.      * @return this
  412.      */
  413.     public ArchiveCommand setTree(ObjectId tree) {
  414.         if (tree == null)
  415.             throw new IllegalArgumentException();

  416.         this.tree = tree;
  417.         setCallable(true);
  418.         return this;
  419.     }

  420.     /**
  421.      * Set string prefixed to filenames in archive
  422.      *
  423.      * @param prefix
  424.      *            string prefixed to filenames in archive (e.g., "master/").
  425.      *            null means to not use any leading prefix.
  426.      * @return this
  427.      * @since 3.3
  428.      */
  429.     public ArchiveCommand setPrefix(String prefix) {
  430.         this.prefix = prefix;
  431.         return this;
  432.     }

  433.     /**
  434.      * Set the intended filename for the produced archive. Currently the only
  435.      * effect is to determine the default archive format when none is specified
  436.      * with {@link #setFormat(String)}.
  437.      *
  438.      * @param filename
  439.      *            intended filename for the archive
  440.      * @return this
  441.      */
  442.     public ArchiveCommand setFilename(String filename) {
  443.         int slash = filename.lastIndexOf('/');
  444.         int dot = filename.indexOf('.', slash + 1);

  445.         if (dot == -1)
  446.             this.suffix = ""; //$NON-NLS-1$
  447.         else
  448.             this.suffix = filename.substring(dot);
  449.         return this;
  450.     }

  451.     /**
  452.      * Set output stream
  453.      *
  454.      * @param out
  455.      *            the stream to which to write the archive
  456.      * @return this
  457.      */
  458.     public ArchiveCommand setOutputStream(OutputStream out) {
  459.         this.out = out;
  460.         return this;
  461.     }

  462.     /**
  463.      * Set archive format
  464.      *
  465.      * @param fmt
  466.      *            archive format (e.g., "tar" or "zip"). null means to choose
  467.      *            automatically based on the archive filename.
  468.      * @return this
  469.      */
  470.     public ArchiveCommand setFormat(String fmt) {
  471.         this.format = fmt;
  472.         return this;
  473.     }

  474.     /**
  475.      * Set archive format options
  476.      *
  477.      * @param options
  478.      *            archive format options (e.g., level=9 for zip compression).
  479.      * @return this
  480.      * @since 4.0
  481.      */
  482.     public ArchiveCommand setFormatOptions(Map<String, Object> options) {
  483.         this.formatOptions = options;
  484.         return this;
  485.     }

  486.     /**
  487.      * Set an optional parameter path. without an optional path parameter, all
  488.      * files and subdirectories of the current working directory are included in
  489.      * the archive. If one or more paths are specified, only these are included.
  490.      *
  491.      * @param paths
  492.      *            file names (e.g <code>file1.c</code>) or directory names (e.g.
  493.      *            <code>dir</code> to add <code>dir/file1</code> and
  494.      *            <code>dir/file2</code>) can also be given to add all files in
  495.      *            the directory, recursively. Fileglobs (e.g. *.c) are not yet
  496.      *            supported.
  497.      * @return this
  498.      * @since 3.4
  499.      */
  500.     public ArchiveCommand setPaths(String... paths) {
  501.         this.paths = Arrays.asList(paths);
  502.         return this;
  503.     }

  504.     private RevTree getTree(RevObject o)
  505.             throws IncorrectObjectTypeException {
  506.         final RevTree t;
  507.         if (o instanceof RevCommit) {
  508.             t = ((RevCommit) o).getTree();
  509.         } else if (!(o instanceof RevTree)) {
  510.             throw new IncorrectObjectTypeException(tree.toObjectId(),
  511.                     Constants.TYPE_TREE);
  512.         } else {
  513.             t = (RevTree) o;
  514.         }
  515.         return t;
  516.     }

  517. }