ArchiveCommand.java

  1. /*
  2.  * Copyright (C) 2012 Google Inc. and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.api;

  11. import java.io.Closeable;
  12. import java.io.IOException;
  13. import java.io.OutputStream;
  14. import java.text.MessageFormat;
  15. import java.util.ArrayList;
  16. import java.util.Arrays;
  17. import java.util.HashMap;
  18. import java.util.List;
  19. import java.util.Map;
  20. import java.util.concurrent.ConcurrentHashMap;

  21. import org.eclipse.jgit.api.errors.GitAPIException;
  22. import org.eclipse.jgit.api.errors.JGitInternalException;
  23. import org.eclipse.jgit.errors.IncorrectObjectTypeException;
  24. import org.eclipse.jgit.internal.JGitText;
  25. import org.eclipse.jgit.lib.Constants;
  26. import org.eclipse.jgit.lib.FileMode;
  27. import org.eclipse.jgit.lib.MutableObjectId;
  28. import org.eclipse.jgit.lib.ObjectId;
  29. import org.eclipse.jgit.lib.ObjectLoader;
  30. import org.eclipse.jgit.lib.ObjectReader;
  31. import org.eclipse.jgit.lib.Repository;
  32. import org.eclipse.jgit.revwalk.RevCommit;
  33. import org.eclipse.jgit.revwalk.RevObject;
  34. import org.eclipse.jgit.revwalk.RevTree;
  35. import org.eclipse.jgit.revwalk.RevWalk;
  36. import org.eclipse.jgit.treewalk.TreeWalk;
  37. import org.eclipse.jgit.treewalk.filter.PathFilterGroup;

  38. /**
  39.  * Create an archive of files from a named tree.
  40.  * <p>
  41.  * Examples (<code>git</code> is a {@link org.eclipse.jgit.api.Git} instance):
  42.  * <p>
  43.  * Create a tarball from HEAD:
  44.  *
  45.  * <pre>
  46.  * ArchiveCommand.registerFormat("tar", new TarFormat());
  47.  * try {
  48.  *  git.archive().setTree(db.resolve(&quot;HEAD&quot;)).setOutputStream(out).call();
  49.  * } finally {
  50.  *  ArchiveCommand.unregisterFormat("tar");
  51.  * }
  52.  * </pre>
  53.  * <p>
  54.  * Create a ZIP file from master:
  55.  *
  56.  * <pre>
  57.  * ArchiveCommand.registerFormat("zip", new ZipFormat());
  58.  * try {
  59.  *  git.archive().
  60.  *      .setTree(db.resolve(&quot;master&quot;))
  61.  *      .setFormat("zip")
  62.  *      .setOutputStream(out)
  63.  *      .call();
  64.  * } finally {
  65.  *  ArchiveCommand.unregisterFormat("zip");
  66.  * }
  67.  * </pre>
  68.  *
  69.  * @see <a href="http://git-htmldocs.googlecode.com/git/git-archive.html" >Git
  70.  *      documentation about archive</a>
  71.  * @since 3.1
  72.  */
  73. public class ArchiveCommand extends GitCommand<OutputStream> {
  74.     /**
  75.      * Archival format.
  76.      *
  77.      * Usage:
  78.      *  Repository repo = git.getRepository();
  79.      *  T out = format.createArchiveOutputStream(System.out);
  80.      *  try {
  81.      *      for (...) {
  82.      *          format.putEntry(out, path, mode, repo.open(objectId));
  83.      *      }
  84.      *      out.close();
  85.      *  }
  86.      *
  87.      * @param <T>
  88.      *            type representing an archive being created.
  89.      */
  90.     public static interface Format<T extends Closeable> {
  91.         /**
  92.          * Start a new archive. Entries can be included in the archive using the
  93.          * putEntry method, and then the archive should be closed using its
  94.          * close method.
  95.          *
  96.          * @param s
  97.          *            underlying output stream to which to write the archive.
  98.          * @return new archive object for use in putEntry
  99.          * @throws IOException
  100.          *             thrown by the underlying output stream for I/O errors
  101.          */
  102.         T createArchiveOutputStream(OutputStream s) throws IOException;

  103.         /**
  104.          * Start a new archive. Entries can be included in the archive using the
  105.          * putEntry method, and then the archive should be closed using its
  106.          * close method. In addition options can be applied to the underlying
  107.          * stream. E.g. compression level.
  108.          *
  109.          * @param s
  110.          *            underlying output stream to which to write the archive.
  111.          * @param o
  112.          *            options to apply to the underlying output stream. Keys are
  113.          *            option names and values are option values.
  114.          * @return new archive object for use in putEntry
  115.          * @throws IOException
  116.          *             thrown by the underlying output stream for I/O errors
  117.          * @since 4.0
  118.          */
  119.         T createArchiveOutputStream(OutputStream s, Map<String, Object> o)
  120.                 throws IOException;

  121.         /**
  122.          * Write an entry to an archive.
  123.          *
  124.          * @param out
  125.          *            archive object from createArchiveOutputStream
  126.          * @param tree
  127.          *            the tag, commit, or tree object to produce an archive for
  128.          * @param path
  129.          *            full filename relative to the root of the archive (with
  130.          *            trailing '/' for directories)
  131.          * @param mode
  132.          *            mode (for example FileMode.REGULAR_FILE or
  133.          *            FileMode.SYMLINK)
  134.          * @param loader
  135.          *            blob object with data for this entry (null for
  136.          *            directories)
  137.          * @throws IOException
  138.          *             thrown by the underlying output stream for I/O errors
  139.          * @since 4.7
  140.          */
  141.         void putEntry(T out, ObjectId tree, String path, FileMode mode,
  142.                 ObjectLoader loader) throws IOException;

  143.         /**
  144.          * Filename suffixes representing this format (e.g.,
  145.          * { ".tar.gz", ".tgz" }).
  146.          *
  147.          * The behavior is undefined when suffixes overlap (if
  148.          * one format claims suffix ".7z", no other format should
  149.          * take ".tar.7z").
  150.          *
  151.          * @return this format's suffixes
  152.          */
  153.         Iterable<String> suffixes();
  154.     }

  155.     /**
  156.      * Signals an attempt to use an archival format that ArchiveCommand
  157.      * doesn't know about (for example due to a typo).
  158.      */
  159.     public static class UnsupportedFormatException extends GitAPIException {
  160.         private static final long serialVersionUID = 1L;

  161.         private final String format;

  162.         /**
  163.          * @param format the problematic format name
  164.          */
  165.         public UnsupportedFormatException(String format) {
  166.             super(MessageFormat.format(JGitText.get().unsupportedArchiveFormat, format));
  167.             this.format = format;
  168.         }

  169.         /**
  170.          * @return the problematic format name
  171.          */
  172.         public String getFormat() {
  173.             return format;
  174.         }
  175.     }

  176.     private static class FormatEntry {
  177.         final Format<?> format;
  178.         /** Number of times this format has been registered. */
  179.         final int refcnt;

  180.         public FormatEntry(Format<?> format, int refcnt) {
  181.             if (format == null)
  182.                 throw new NullPointerException();
  183.             this.format = format;
  184.             this.refcnt = refcnt;
  185.         }
  186.     }

  187.     /**
  188.      * Available archival formats (corresponding to values for
  189.      * the --format= option)
  190.      */
  191.     private static final Map<String, FormatEntry> formats =
  192.             new ConcurrentHashMap<>();

  193.     /**
  194.      * Replaces the entry for a key only if currently mapped to a given
  195.      * value.
  196.      *
  197.      * @param map a map
  198.      * @param key key with which the specified value is associated
  199.      * @param oldValue expected value for the key (null if should be absent).
  200.      * @param newValue value to be associated with the key (null to remove).
  201.      * @return true if the value was replaced
  202.      */
  203.     private static <K, V> boolean replace(Map<K, V> map,
  204.             K key, V oldValue, V newValue) {
  205.         if (oldValue == null && newValue == null) // Nothing to do.
  206.             return true;

  207.         if (oldValue == null)
  208.             return map.putIfAbsent(key, newValue) == null;
  209.         else if (newValue == null)
  210.             return map.remove(key, oldValue);
  211.         else
  212.             return map.replace(key, oldValue, newValue);
  213.     }

  214.     /**
  215.      * Adds support for an additional archival format.  To avoid
  216.      * unnecessary dependencies, ArchiveCommand does not have support
  217.      * for any formats built in; use this function to add them.
  218.      * <p>
  219.      * OSGi plugins providing formats should call this function at
  220.      * bundle activation time.
  221.      * <p>
  222.      * It is okay to register the same archive format with the same
  223.      * name multiple times, but don't forget to unregister it that
  224.      * same number of times, too.
  225.      * <p>
  226.      * Registering multiple formats with different names and the
  227.      * same or overlapping suffixes results in undefined behavior.
  228.      * TODO: check that suffixes don't overlap.
  229.      *
  230.      * @param name name of a format (e.g., "tar" or "zip").
  231.      * @param fmt archiver for that format
  232.      * @throws JGitInternalException
  233.      *              A different archival format with that name was
  234.      *              already registered.
  235.      */
  236.     public static void registerFormat(String name, Format<?> fmt) {
  237.         if (fmt == null)
  238.             throw new NullPointerException();

  239.         FormatEntry old, entry;
  240.         do {
  241.             old = formats.get(name);
  242.             if (old == null) {
  243.                 entry = new FormatEntry(fmt, 1);
  244.                 continue;
  245.             }
  246.             if (!old.format.equals(fmt))
  247.                 throw new JGitInternalException(MessageFormat.format(
  248.                         JGitText.get().archiveFormatAlreadyRegistered,
  249.                         name));
  250.             entry = new FormatEntry(old.format, old.refcnt + 1);
  251.         } while (!replace(formats, name, old, entry));
  252.     }

  253.     /**
  254.      * Marks support for an archival format as no longer needed so its
  255.      * Format can be garbage collected if no one else is using it either.
  256.      * <p>
  257.      * In other words, this decrements the reference count for an
  258.      * archival format.  If the reference count becomes zero, removes
  259.      * support for that format.
  260.      *
  261.      * @param name name of format (e.g., "tar" or "zip").
  262.      * @throws JGitInternalException
  263.      *              No such archival format was registered.
  264.      */
  265.     public static void unregisterFormat(String name) {
  266.         FormatEntry old, entry;
  267.         do {
  268.             old = formats.get(name);
  269.             if (old == null)
  270.                 throw new JGitInternalException(MessageFormat.format(
  271.                         JGitText.get().archiveFormatAlreadyAbsent,
  272.                         name));
  273.             if (old.refcnt == 1) {
  274.                 entry = null;
  275.                 continue;
  276.             }
  277.             entry = new FormatEntry(old.format, old.refcnt - 1);
  278.         } while (!replace(formats, name, old, entry));
  279.     }

  280.     private static Format<?> formatBySuffix(String filenameSuffix)
  281.             throws UnsupportedFormatException {
  282.         if (filenameSuffix != null)
  283.             for (FormatEntry entry : formats.values()) {
  284.                 Format<?> fmt = entry.format;
  285.                 for (String sfx : fmt.suffixes())
  286.                     if (filenameSuffix.endsWith(sfx))
  287.                         return fmt;
  288.             }
  289.         return lookupFormat("tar"); //$NON-NLS-1$
  290.     }

  291.     private static Format<?> lookupFormat(String formatName) throws UnsupportedFormatException {
  292.         FormatEntry entry = formats.get(formatName);
  293.         if (entry == null)
  294.             throw new UnsupportedFormatException(formatName);
  295.         return entry.format;
  296.     }

  297.     private OutputStream out;
  298.     private ObjectId tree;
  299.     private String prefix;
  300.     private String format;
  301.     private Map<String, Object> formatOptions = new HashMap<>();
  302.     private List<String> paths = new ArrayList<>();

  303.     /** Filename suffix, for automatically choosing a format. */
  304.     private String suffix;

  305.     /**
  306.      * Constructor for ArchiveCommand
  307.      *
  308.      * @param repo
  309.      *            the {@link org.eclipse.jgit.lib.Repository}
  310.      */
  311.     public ArchiveCommand(Repository repo) {
  312.         super(repo);
  313.         setCallable(false);
  314.     }

  315.     private <T extends Closeable> OutputStream writeArchive(Format<T> fmt) {
  316.         try {
  317.             try (TreeWalk walk = new TreeWalk(repo);
  318.                     RevWalk rw = new RevWalk(walk.getObjectReader());
  319.                     T outa = fmt.createArchiveOutputStream(out,
  320.                             formatOptions)) {
  321.                 String pfx = prefix == null ? "" : prefix; //$NON-NLS-1$
  322.                 MutableObjectId idBuf = new MutableObjectId();
  323.                 ObjectReader reader = walk.getObjectReader();

  324.                 RevObject o = rw.peel(rw.parseAny(tree));
  325.                 walk.reset(getTree(o));
  326.                 if (!paths.isEmpty()) {
  327.                     walk.setFilter(PathFilterGroup.createFromStrings(paths));
  328.                 }

  329.                 // Put base directory into archive
  330.                 if (pfx.endsWith("/")) { //$NON-NLS-1$
  331.                     fmt.putEntry(outa, o, pfx.replaceAll("[/]+$", "/"), //$NON-NLS-1$ //$NON-NLS-2$
  332.                             FileMode.TREE, null);
  333.                 }

  334.                 while (walk.next()) {
  335.                     String name = pfx + walk.getPathString();
  336.                     FileMode mode = walk.getFileMode(0);

  337.                     if (walk.isSubtree())
  338.                         walk.enterSubtree();

  339.                     if (mode == FileMode.GITLINK) {
  340.                         // TODO(jrn): Take a callback to recurse
  341.                         // into submodules.
  342.                         mode = FileMode.TREE;
  343.                     }

  344.                     if (mode == FileMode.TREE) {
  345.                         fmt.putEntry(outa, o, name + "/", mode, null); //$NON-NLS-1$
  346.                         continue;
  347.                     }
  348.                     walk.getObjectId(idBuf, 0);
  349.                     fmt.putEntry(outa, o, name, mode, reader.open(idBuf));
  350.                 }
  351.                 return out;
  352.             } finally {
  353.                 out.close();
  354.             }
  355.         } catch (IOException e) {
  356.             // TODO(jrn): Throw finer-grained errors.
  357.             throw new JGitInternalException(
  358.                     JGitText.get().exceptionCaughtDuringExecutionOfArchiveCommand, e);
  359.         }
  360.     }

  361.     /** {@inheritDoc} */
  362.     @Override
  363.     public OutputStream call() throws GitAPIException {
  364.         checkCallable();

  365.         Format<?> fmt;
  366.         if (format == null)
  367.             fmt = formatBySuffix(suffix);
  368.         else
  369.             fmt = lookupFormat(format);
  370.         return writeArchive(fmt);
  371.     }

  372.     /**
  373.      * Set the tag, commit, or tree object to produce an archive for
  374.      *
  375.      * @param tree
  376.      *            the tag, commit, or tree object to produce an archive for
  377.      * @return this
  378.      */
  379.     public ArchiveCommand setTree(ObjectId tree) {
  380.         if (tree == null)
  381.             throw new IllegalArgumentException();

  382.         this.tree = tree;
  383.         setCallable(true);
  384.         return this;
  385.     }

  386.     /**
  387.      * Set string prefixed to filenames in archive
  388.      *
  389.      * @param prefix
  390.      *            string prefixed to filenames in archive (e.g., "master/").
  391.      *            null means to not use any leading prefix.
  392.      * @return this
  393.      * @since 3.3
  394.      */
  395.     public ArchiveCommand setPrefix(String prefix) {
  396.         this.prefix = prefix;
  397.         return this;
  398.     }

  399.     /**
  400.      * Set the intended filename for the produced archive. Currently the only
  401.      * effect is to determine the default archive format when none is specified
  402.      * with {@link #setFormat(String)}.
  403.      *
  404.      * @param filename
  405.      *            intended filename for the archive
  406.      * @return this
  407.      */
  408.     public ArchiveCommand setFilename(String filename) {
  409.         int slash = filename.lastIndexOf('/');
  410.         int dot = filename.indexOf('.', slash + 1);

  411.         if (dot == -1)
  412.             this.suffix = ""; //$NON-NLS-1$
  413.         else
  414.             this.suffix = filename.substring(dot);
  415.         return this;
  416.     }

  417.     /**
  418.      * Set output stream
  419.      *
  420.      * @param out
  421.      *            the stream to which to write the archive
  422.      * @return this
  423.      */
  424.     public ArchiveCommand setOutputStream(OutputStream out) {
  425.         this.out = out;
  426.         return this;
  427.     }

  428.     /**
  429.      * Set archive format
  430.      *
  431.      * @param fmt
  432.      *            archive format (e.g., "tar" or "zip"). null means to choose
  433.      *            automatically based on the archive filename.
  434.      * @return this
  435.      */
  436.     public ArchiveCommand setFormat(String fmt) {
  437.         this.format = fmt;
  438.         return this;
  439.     }

  440.     /**
  441.      * Set archive format options
  442.      *
  443.      * @param options
  444.      *            archive format options (e.g., level=9 for zip compression).
  445.      * @return this
  446.      * @since 4.0
  447.      */
  448.     public ArchiveCommand setFormatOptions(Map<String, Object> options) {
  449.         this.formatOptions = options;
  450.         return this;
  451.     }

  452.     /**
  453.      * Set an optional parameter path. without an optional path parameter, all
  454.      * files and subdirectories of the current working directory are included in
  455.      * the archive. If one or more paths are specified, only these are included.
  456.      *
  457.      * @param paths
  458.      *            file names (e.g <code>file1.c</code>) or directory names (e.g.
  459.      *            <code>dir</code> to add <code>dir/file1</code> and
  460.      *            <code>dir/file2</code>) can also be given to add all files in
  461.      *            the directory, recursively. Fileglobs (e.g. *.c) are not yet
  462.      *            supported.
  463.      * @return this
  464.      * @since 3.4
  465.      */
  466.     public ArchiveCommand setPaths(String... paths) {
  467.         this.paths = Arrays.asList(paths);
  468.         return this;
  469.     }

  470.     private RevTree getTree(RevObject o)
  471.             throws IncorrectObjectTypeException {
  472.         final RevTree t;
  473.         if (o instanceof RevCommit) {
  474.             t = ((RevCommit) o).getTree();
  475.         } else if (!(o instanceof RevTree)) {
  476.             throw new IncorrectObjectTypeException(tree.toObjectId(),
  477.                     Constants.TYPE_TREE);
  478.         } else {
  479.             t = (RevTree) o;
  480.         }
  481.         return t;
  482.     }

  483. }