Download commands: Add JGit archive

Since JGit 3.1 archive command was implemented. Add it to download
drop down as new line.

The following libraries are introduced in this change:

* jgit-archive (Apache 2)
* commons-compress (Apache 2)
* tukaani-xz (Public domain)

Change-Id: I5f61aac8c434414c73585a9320e84f4430dd111d
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
index 29be66b..6fc678b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DownloadBox.java
@@ -40,9 +40,13 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 
+import java.util.ArrayList;
 import java.util.EnumSet;
+import java.util.Iterator;
+import java.util.List;
 
 class DownloadBox extends VerticalPanel {
+  private final static String ARCHIVE[] = {"tar", "tbz2", "tgz", "txz"};
   private final ChangeInfo change;
   private final String revision;
   private final PatchSet.Id psId;
@@ -109,6 +113,7 @@
       }
     }
     insertPatch();
+    insertArchive();
     insertCommand(null, scheme);
   }
 
@@ -141,6 +146,34 @@
     insertCommand("Patch-File", p);
   }
 
+  private void insertArchive() {
+    List<Anchor> formats = new ArrayList<>(ARCHIVE.length);
+    for (String f : ARCHIVE) {
+      Anchor archive = new Anchor(f);
+      archive.setHref(new RestApi("/changes/")
+          .id(psId.getParentKey().get())
+          .view("revisions")
+          .id(revision)
+          .view("archive")
+          .addParameter("format", f)
+          .url());
+      formats.add(archive);
+    }
+
+    HorizontalPanel p = new HorizontalPanel();
+    Iterator<Anchor> it = formats.iterator();
+    while (it.hasNext()) {
+      Anchor a = it.next();
+      p.add(a);
+      if (it.hasNext()) {
+        InlineLabel spacer = new InlineLabel("|");
+        spacer.setStyleName(Gerrit.RESOURCES.css().downloadBoxSpacer());
+        p.add(spacer);
+      }
+    }
+    insertCommand("Archive", p);
+  }
+
   private void insertCommand(String commandName, Widget w) {
     int row = commandTable.getRowCount();
     commandTable.insertRow(row);
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index 6ca5493..12dd162 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -57,6 +57,7 @@
     '//lib/guice:guice-assistedinject',
     '//lib/guice:guice-servlet',
     '//lib/jgit:jgit',
+    '//lib/jgit:jgit-archive',
     '//lib/joda:joda-time',
     '//lib/log:api',
     '//lib/prolog:prolog-cafe',
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java
new file mode 100644
index 0000000..e7d05df
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ArchiveFormat.java
@@ -0,0 +1,84 @@
+// Copyright 2013 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.collect.Maps;
+
+import org.eclipse.jgit.api.ArchiveCommand;
+import org.eclipse.jgit.archive.TarFormat;
+import org.eclipse.jgit.archive.Tbz2Format;
+import org.eclipse.jgit.archive.TgzFormat;
+import org.eclipse.jgit.archive.TxzFormat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.Map;
+
+enum ArchiveFormat {
+  TGZ("application/x-gzip", new TgzFormat()),
+  TAR("application/x-tar", new TarFormat()),
+  TBZ2("application/x-bzip2", new Tbz2Format()),
+  TXZ("application/x-xz", new TxzFormat());
+  // Zip is not supported because it may be interpreted by a Java plugin as a
+  // valid JAR file, whose code would have access to cookies on the domain.
+
+  static final Logger log = LoggerFactory.getLogger(ArchiveFormat.class);
+
+  private final ArchiveCommand.Format<?> format;
+  private final String mimeType;
+
+  private ArchiveFormat(String mimeType, ArchiveCommand.Format<?> format) {
+    this.format = format;
+    this.mimeType = mimeType;
+    ArchiveCommand.registerFormat(name(), format);
+  }
+
+  String getShortName() {
+    return name().toLowerCase();
+  }
+
+  String getMimeType() {
+    return mimeType;
+  }
+
+  String getDefaultSuffix() {
+    return getSuffixes().iterator().next();
+  }
+
+  Iterable<String> getSuffixes() {
+    return format.suffixes();
+  }
+
+  static Map<String, ArchiveFormat> init() {
+    String[] formats = new String[values().length];
+    for (int i = 0; i < values().length; i++) {
+      formats[i] = values()[i].name();
+    }
+
+    Map<String, ArchiveFormat> exts = Maps.newLinkedHashMap();
+    for (String name : formats) {
+      try {
+        ArchiveFormat format = valueOf(name.toUpperCase());
+        for (String ext : format.getSuffixes()) {
+          exts.put(ext, format);
+        }
+      } catch (IllegalArgumentException e) {
+        log.warn("Invalid archive.format {}", name);
+      }
+    }
+    return Collections.unmodifiableMap(exts);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
new file mode 100644
index 0000000..9a4ab21
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetArchive.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.ArchiveCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Map;
+
+class GetArchive implements RestReadView<RevisionResource> {
+  private static final Map<String, ArchiveFormat> formats = ArchiveFormat.init();
+  private final GitRepositoryManager repoManager;
+
+  @Option(name = "--format")
+  private String format;
+
+  @Inject
+  GetArchive(GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public BinaryResult apply(RevisionResource rsrc)
+      throws BadRequestException, IOException {
+    if (Strings.isNullOrEmpty(format)) {
+      throw new BadRequestException("format is not specified");
+    }
+    final ArchiveFormat f = formats.get("." + format);
+    if (f == null) {
+      throw new BadRequestException("unknown archive format");
+    }
+    boolean close = true;
+    final Repository repo = repoManager
+        .openRepository(rsrc.getControl().getProject().getNameKey());
+    try {
+      final RevWalk rw = new RevWalk(repo);
+      try {
+        final RevCommit commit =
+            rw.parseCommit(ObjectId.fromString(rsrc.getPatchSet()
+                .getRevision().get()));
+        BinaryResult bin = new BinaryResult() {
+          @Override
+          public void writeTo(OutputStream out) throws IOException {
+            try {
+              new ArchiveCommand(repo)
+                  .setFormat(f.name())
+                  .setTree(commit.getTree())
+                  .setOutputStream(out).call();
+            } catch (GitAPIException e) {
+              throw new IOException(e);
+            }
+          }
+
+          @Override
+          public void close() throws IOException {
+            rw.release();
+            repo.close();
+          }
+        };
+
+        bin.disableGzip()
+            .setContentType(f.getMimeType())
+            .setAttachmentName(name(f, rw, commit));
+
+        close = false;
+        return bin;
+      } finally {
+        if (close) {
+          rw.release();
+        }
+      }
+    } finally {
+      if (close) {
+        repo.close();
+      }
+    }
+  }
+
+  private static String name(ArchiveFormat format, RevWalk rw, RevCommit commit)
+      throws IOException {
+    return String.format("%s%s",
+        rw.getObjectReader().abbreviate(commit,7).name(),
+        format.getDefaultSuffix());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index 9ed2bee..6880ca2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -82,6 +82,7 @@
     get(REVISION_KIND, "submit_type").to(TestSubmitType.Get.class);
     post(REVISION_KIND, "test.submit_rule").to(TestSubmitRule.class);
     post(REVISION_KIND, "test.submit_type").to(TestSubmitType.class);
+    get(REVISION_KIND, "archive").to(GetArchive.class);
 
     child(REVISION_KIND, "drafts").to(Drafts.class);
     put(REVISION_KIND, "drafts").to(CreateDraft.class);
diff --git a/lib/BUCK b/lib/BUCK
index b7ba064..290ed92 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -20,6 +20,7 @@
 define_license(name = 'prologcafe')
 define_license(name = 'protobuf')
 define_license(name = 'slf4j')
+define_license(name = 'xz')
 define_license(name = 'DO_NOT_DISTRIBUTE')
 
 maven_jar(
@@ -248,3 +249,12 @@
   visibility = ['//lib:easymock'],
   attach_source = False,
 )
+
+maven_jar(
+  name = 'tukaani-xz',
+  id = 'org.tukaani:xz:1.4',
+  sha1 = '18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3',
+  license = 'xz',
+  attach_source = False,
+  visibility = ['//lib/jgit:jgit-archive'],
+)
diff --git a/lib/LICENSE-xz b/lib/LICENSE-xz
new file mode 100644
index 0000000..420556e
--- /dev/null
+++ b/lib/LICENSE-xz
@@ -0,0 +1,4 @@
+All the files in this package have been written by Lasse Collin
+and/or Igor Pavlov. All these files have been put into the
+public domain. You can do whatever you want with these files.
+This software is provided "as is", without any warranty.
diff --git a/lib/commons/BUCK b/lib/commons/BUCK
index 6f412e4..aed2c68 100644
--- a/lib/commons/BUCK
+++ b/lib/commons/BUCK
@@ -22,6 +22,15 @@
 )
 
 maven_jar(
+  name = 'compress',
+  id = 'org.apache.commons:commons-compress:1.7',
+  sha1 = 'ab365c96ee9bc88adcc6fa40d185c8e15a31410d',
+  license = 'Apache2.0',
+  exclude = ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt'],
+  visibility = ['//lib/jgit:jgit-archive'],
+)
+
+maven_jar(
   name = 'dbcp',
   id = 'commons-dbcp:commons-dbcp:1.4',
   sha1 = '30be73c965cc990b153a100aaaaafcf239f82d39',
diff --git a/lib/jgit/BUCK b/lib/jgit/BUCK
index ee5c4d0..026fa40 100644
--- a/lib/jgit/BUCK
+++ b/lib/jgit/BUCK
@@ -34,6 +34,23 @@
 )
 
 maven_jar(
+  name = 'jgit-archive',
+  id = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + VERS,
+  sha1 = 'c645b284344ec9791404f6fd0e04f6dbedb58b7d',
+  license = 'jgit',
+  repository = REPO,
+  deps = [':jgit',
+    '//lib/commons:compress',
+    '//lib:tukaani-xz',
+  ],
+  unsign = True,
+  exclude = [
+    'about.html',
+    'plugin.properties',
+  ],
+)
+
+maven_jar(
   name = 'junit',
   id = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + VERS,
   sha1 = '6ba0c7bf8f9577067c4338976385828e3ad774eb',