Merge "Fix: Plugins implementing LifecycleListener cannot use auto registration"
diff --git a/Documentation/BUCK b/Documentation/BUCK
index 4dc1f3a..faa2553 100644
--- a/Documentation/BUCK
+++ b/Documentation/BUCK
@@ -1,4 +1,6 @@
 include_defs('//Documentation/asciidoc.defs')
+include_defs('//Documentation/config.defs')
+include_defs('//tools/git.defs')
 
 MAIN = ['//gerrit-pgm:pgm', '//gerrit-gwtui:ui_module']
 SRCS = glob(['*.txt'], excludes = ['licenses.txt'])
@@ -11,40 +13,39 @@
     'for s in $SRCS;do ln -s $s Documentation;done;' +
     'mv Documentation/*.{jpg,png} Documentation/images;' +
     'rm Documentation/licenses.txt;' +
-    'ln -s $SRCDIR/licenses.txt LICENSES.txt;' +
+    'cp $SRCDIR/licenses.txt LICENSES.txt;' +
     'zip -qr $OUT *',
   srcs = [genfile(d) for d in HTML] +
     glob([
       'images/*.jpg',
       'images/*.png',
     ]) + [
+    genfile('doc.css'),
     genfile('licenses.html'),
     genfile('licenses.txt'),
   ],
   deps = [':' + d for d in HTML] + [
     ':licenses.html',
     ':licenses.txt',
+    ':doc.css',
   ],
   out = 'html.zip',
   visibility = ['PUBLIC'],
 )
 
+genrule(
+  name = 'doc.css',
+  cmd = 'ln -s $SRCDIR/doc.css $OUT',
+  srcs = ['doc.css'],
+  out = 'doc.css',
+)
+
 genasciidoc(
-  name = 'generate_html',
   srcs = SRCS + [genfile('licenses.txt')],
   outs = HTML + ['licenses.html'],
-  deps = [':licenses.txt'],
-  attributes = [
-    'toc',
-    'newline="\\n"',
-    'asterisk="*"',
-    'plus="+"',
-    'caret="^"',
-    'startsb="["',
-    'endsb="]"',
-    'tilde="~"',
-  ],
-  backend = 'xhtml11',
+  deps = DOCUMENTATION_DEPS,
+  attributes = documentation_attributes(git_describe()),
+  backend = 'html5',
 )
 
 genrule(
diff --git a/Documentation/asciidoc.defs b/Documentation/asciidoc.defs
index 97bf287..8279847 100644
--- a/Documentation/asciidoc.defs
+++ b/Documentation/asciidoc.defs
@@ -12,50 +12,50 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-include_defs('//tools/git.defs')
-
 def genasciidoc(
-    name,
     srcs = [],
     outs = [],
-    deps = [],
+    deps = {},
     attributes = [],
     backend = None,
     visibility = []):
-  MACRO_SUFFIX = '.expanded'
+  EXPN = '.expn'
 
-  cmd = ['asciidoc', '-o', '$OUT']
+  asciidoc = ['$(exe //lib/asciidoctor:asciidoc)']
   if backend:
-    cmd.extend(['-b', backend])
+    asciidoc.extend(['-b', backend])
   for attribute in attributes:
-    cmd.extend(['-a', attribute])
-  cmd.extend(['-a', 'revision="%s"' % git_describe()])
-  cmd.append('$SRCS')
+    asciidoc.extend(['-a', attribute])
+  asciidoc.extend(['-o', '$OUT'])
 
   for p in zip(srcs, outs):
-    s, o = p
-    filename = s
-    if filename.startswith('BUCKGEN:') :
-      filename = s[8:]
+    src, out = p
+    dep = deps.get(src) or []
+
+    tx = []
+    fn = src
+    if fn.startswith('BUCKGEN:') :
+      fn = src[8:]
+      tx = [':' + fn]
+    ex = fn + EXPN
+
     genrule(
-      name = filename + MACRO_SUFFIX,
-      cmd = '$(exe :replace_macros) -s $SRCS -o $OUT --suffix=' + MACRO_SUFFIX,
-      srcs = [s],
-      deps = deps + [':replace_macros'],
-      out = filename + MACRO_SUFFIX,
+      name = ex,
+      cmd = '$(exe :replace_macros) --suffix=' + EXPN +
+            ' -s $SRCDIR/%s' % fn +
+            ' -o $OUT',
+      srcs = [src],
+      deps = tx + [':replace_macros'],
+      out = ex,
     )
     genrule(
-      name = o,
-      cmd = ' '.join(cmd),
-      srcs = [genfile(filename + MACRO_SUFFIX)],
-      deps = deps + [':' + filename + MACRO_SUFFIX],
-      out = o,
+      name = out,
+      cmd = ' '.join(asciidoc + ['$SRCDIR/' + ex]),
+      srcs = [genfile(ex)] + [genfile(n + EXPN) for n in dep],
+      deps = [':' + n + EXPN for n in dep] + [
+        ':' + ex,
+        '//lib/asciidoctor:asciidoc',
+      ],
+      out = out,
       visibility = visibility,
     )
-  genrule(
-    name = name,
-    cmd = ':>$OUT',
-    deps = [':' + o for o in outs],
-    out = name + '__done',
-    visibility = visibility,
-  )
diff --git a/Documentation/config.defs b/Documentation/config.defs
new file mode 100644
index 0000000..28dd2c8
--- /dev/null
+++ b/Documentation/config.defs
@@ -0,0 +1,19 @@
+DOCUMENTATION_DEPS = {
+  "install-quick.txt": ["config-login-register.txt"],
+  "install.txt": ["database-setup.txt"],
+}
+
+def documentation_attributes(revision):
+  return [
+    'toc',
+    'newline="\\n"',
+    'asterisk="*"',
+    'plus="+"',
+    'caret="^"',
+    'startsb="["',
+    'endsb="]"',
+    'tilde="~"',
+    'source-highlighter=prettify',
+    'stylesheet=doc.css',
+    'revnumber="%s"' % revision,
+  ]
diff --git a/Documentation/dev-buck.txt b/Documentation/dev-buck.txt
index 78edefd..7671767 100644
--- a/Documentation/dev-buck.txt
+++ b/Documentation/dev-buck.txt
@@ -178,6 +178,42 @@
 is not regenerated.
 
 
+[[documentation]]
+Documentation
+~~~~~~~~~~~~~
+
+To build the documentation:
+
+----
+  buck build docs
+----
+
+The generated html files will be placed in:
+
+----
+  buck-out/gen/Documentation
+----
+
+The html files will also be bundled into `html.zip` in the same location.
+
+
+[[release]]
+Gerrit Release WAR File
+~~~~~~~~~~~~~~~~~~~~~~~
+
+To build the release of the Gerrit web application, including documentation and
+all core plugins:
+
+----
+  buck build release
+----
+
+The output release WAR will be placed in:
+
+----
+  buck-out/gen/release.war
+----
+
 [[tests]]
 Running Unit Tests
 ------------------
@@ -270,67 +306,6 @@
   EOF
 ----
 
-
-Build Process Switch Exit Criteria
-----------------------------------
-
-The switch to Buck is an experimental process. Buck will become the
-primary build for Gerrit only when the following conditions are met.
-
-1. Windows support.
-+
-Facebook has an intern who will be working on this (summer 2013).
-
-2. Bootstrap and stable version support.
-+
-From a fresh Gerrit clone on a machine without Buck (but with some
-reasonable subset of Buck's dependencies, e.g. Python 2.7), a new
-Gerrit developer should be able to set up and start building with
-Buck by running approximately one command. There should also be some
-idea of a "stable" version of Buck, even if we just tie our build
-to specific known-good SHAs. Binary distributions are another plus,
-which I believe the Buck team has planned.
-
-3. Shawn's Buck fork merged upstream.
-+
-Shawn has a link:https://gerrit.googlesource.com/buck/+log/github-master..master[fork of Buck]
-with some patches necessary to build Gerrit and run its unit tests.
-These patches (or their equivalents) must be in the upstream Buck tree.
-
-4. Fix all incidental issues.
-+
-Things come up that don't work. Martin just ran out of file
-descriptors, which sounds like an upstream bug.
-+
-There should be a consensus that new bugs like this in upstream
-Buck are not constantly being introduced.
-
-5. Support development of custom plugins.
-+
-There are three different alternatives for custom plugins:
-+
-The first is to build with BUCK in tree; your plugin builds against
-whatever version of Gerrit you have the sources checked out for. This
-is the simplest method on master right now. The BUCK definition is
-only a few lines of code and the rest "just works". But you are
-working from a Gerrit source tree, which is maybe not the ideal way to
-develop.
-+
-Another is to continue to use Maven. We just have to package the
-archetypes to support creating a new plugin, but existing plugins can
-develop against the API JAR(s) if they are installed into a Maven
-repository. tools/deploy_api.sh is how we did this for release
-versions of Gerrit. Something similar probably still be used with BUCK
-to publish the precompiled JARs. (Note: this is partially done with the
-target: `buck build api_install`; after issuing this command the new API
-version can be consumed by Maven driven custom plugins).
-+
-The third option is to support a BUCK based plugin build outside of
-the Gerrit tree. This is harder because there is functionality
-developed as macros in the Gerrit tree that plugins would want to use
-(e.g. gerrit_plugin rule).
-
-
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/doc.css b/Documentation/doc.css
new file mode 100644
index 0000000..3e226fe
--- /dev/null
+++ b/Documentation/doc.css
@@ -0,0 +1,37 @@
+body {
+  margin: 1em;
+}
+
+#toctitle {
+  margin-top: 0.5em;
+  font-weight: bold;
+}
+
+h1, h2, h3, h4, h5, h6, #toctitle {
+  color: #527bbd;
+  font-family: sans-serif;
+}
+
+h1, h2, h3 {
+  border-bottom: 2px solid silver;
+}
+
+p {
+  margin: 0.5em 0 0.5em 0;
+}
+li p {
+  margin: 0.2em 0 0.2em 0;
+}
+
+pre {
+  border: 2px solid silver;
+  background: #ebebeb;
+  margin-left: 2em;
+  width: 100em;
+  color: darkgreen;
+  padding: 2px;
+}
+
+dl dt {
+  margin-top: 1em;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
index 2488537..e93973b 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -106,7 +106,7 @@
       JSchException {
     SshSession s = new SshSession(server, accounts.create("user", "user@example.com", "User"));
     s.exec("gerrit gc --all");
-    assertError("fatal: user does not have \"runGC\" capability.", s.getError());
+    assertError("Capability runGC is required to access this resource", s.getError());
   }
 
   @Test
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
index 970b317..09aef22 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen2.java
@@ -216,7 +216,7 @@
         Gerrit.displayLastChangeList();
       }
     });
-    keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReload()) {
+    keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadChange()) {
       @Override
       public void onKeyPress(final KeyPressEvent event) {
         reload.reload();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index 2580542..6273717 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -21,6 +21,8 @@
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.core.client.JsArray;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
 
 import java.util.Collections;
 import java.util.Comparator;
@@ -41,7 +43,16 @@
   @Override
   protected void onInitUI() {
     super.onInitUI();
-    table = new ChangeTable2();
+    table = new ChangeTable2() {
+      {
+        keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadSearch()) {
+          @Override
+          public void onKeyPress(final KeyPressEvent event) {
+            Gerrit.display(getToken());
+          }
+        });
+      }
+    };
     table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
 
     outgoing = new ChangeTable2.Section();
@@ -63,6 +74,7 @@
   @Override
   protected void onLoad() {
     super.onLoad();
+
     String who = mine ? "self" : ownerId.toString();
     ChangeList.query(
         new ScreenLoadCallback<JsArray<ChangeList>>(this) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 81d4cc3..197b466 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -56,7 +56,8 @@
   String expandCollapseDependencies();
   String previousPatchSet();
   String nextPatchSet();
-  String keyReload();
+  String keyReloadChange();
+  String keyReloadSearch();
   String keyPublishComments();
   String keyEditTopic();
   String keyEditMessage();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index 7bc6eb1..f51cff0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -36,7 +36,8 @@
 expandCollapseDependencies = Expands / Collapses dependencies section
 previousPatchSet = Previous patch set
 nextPatchSet = Next patch set
-keyReload = Reload change
+keyReloadChange = Reload change
+keyReloadSearch = Reload change list
 keyPublishComments = Review and publish comments
 keyEditTopic = Edit change topic
 keyEditMessage = Edit commit message
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
index 2310bf8..b2ccbcb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
@@ -76,6 +76,13 @@
             .changeTablePagePrev(), prev));
         keysNavigation.add(new DoLinkCommand(0, ']', Util.C
             .changeTablePageNext(), next));
+
+        keysNavigation.add(new KeyCommand(0, 'R', Util.C.keyReloadSearch()) {
+          @Override
+          public void onKeyPress(final KeyPressEvent event) {
+            Gerrit.display(getToken());
+          }
+        });
       }
     };
     section = new ChangeTable2.Section();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index 97b3696..508c41a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -43,6 +43,7 @@
 import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
+import org.w3c.dom.Node;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -80,6 +81,7 @@
   private final String noCacheName;
   private final PermutationSelector selector;
   private final boolean refreshHeaderFooter;
+  private final StaticServlet staticServlet;
   private volatile Page page;
 
   @Inject
@@ -87,7 +89,8 @@
       final SitePaths sp, final ThemeFactory themeFactory,
       final GerritConfig gc, final ServletContext servletContext,
       final DynamicSet<WebUiPlugin> webUiPlugins,
-      @GerritServerConfig final Config cfg)
+      @GerritServerConfig final Config cfg,
+      final StaticServlet ss)
       throws IOException, ServletException {
     currentUser = cu;
     session = w;
@@ -97,6 +100,7 @@
     signedInTheme = themeFactory.getSignedInTheme();
     site = sp;
     refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
+    staticServlet = ss;
     boolean checkUserAgent = cfg.getBoolean("site", "checkUserAgent", true);
 
     final String pageName = "HostPage.html";
@@ -247,6 +251,26 @@
     return pg.get(selector.select(req));
   }
 
+  private void insertETags(Element e) {
+    if ("img".equalsIgnoreCase(e.getTagName())
+        || "script".equalsIgnoreCase(e.getTagName())) {
+      String src = e.getAttribute("src");
+      if (src != null && src.startsWith("static/")) {
+        String name = src.substring("static/".length());
+        StaticServlet.Resource r = staticServlet.getResource(name);
+        if (r != null) {
+          e.setAttribute("src", src + "?e=" + r.etag);
+        }
+      }
+    }
+
+    for (Node n = e.getFirstChild(); n != null; n = n.getNextSibling()) {
+      if (n instanceof Element) {
+        insertETags((Element) n);
+      }
+    }
+  }
+
   private static class FileInfo {
     private final File path;
     private final long time;
@@ -378,7 +402,8 @@
         return info;
       }
 
-      final Element content = html.getDocumentElement();
+      Element content = html.getDocumentElement();
+      insertETags(content);
       banner.appendChild(hostDoc.importNode(content, true));
       return info;
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
index bc0a174..b74d1ac 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
@@ -14,37 +14,60 @@
 
 package com.google.gerrit.httpd.raw;
 
+import static com.google.common.net.HttpHeaders.CONTENT_ENCODING;
+import static com.google.common.net.HttpHeaders.ETAG;
+import static com.google.common.net.HttpHeaders.IF_NONE_MATCH;
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
 import com.google.common.collect.Maps;
+import com.google.common.hash.Hashing;
+import com.google.common.io.ByteStreams;
+import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
-import java.util.zip.GZIPOutputStream;
+import java.util.concurrent.ExecutionException;
 
+import javax.annotation.Nullable;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+
 /** Sends static content from the site 's <code>static/</code> subdirectory. */
 @SuppressWarnings("serial")
 @Singleton
 public class StaticServlet extends HttpServlet {
+  private static final Logger log = LoggerFactory.getLogger(StaticServlet.class);
+  private static final String JS = "application/x-javascript";
   private static final Map<String, String> MIME_TYPES = Maps.newHashMap();
   static {
     MIME_TYPES.put("html", "text/html");
     MIME_TYPES.put("htm", "text/html");
-    MIME_TYPES.put("js", "application/x-javascript");
+    MIME_TYPES.put("js", JS);
     MIME_TYPES.put("css", "text/css");
     MIME_TYPES.put("rtf", "text/rtf");
     MIME_TYPES.put("txt", "text/plain");
@@ -66,31 +89,13 @@
     return type != null ? type : "application/octet-stream";
   }
 
-  private static byte[] readFile(final File p) throws IOException {
-    final FileInputStream in = new FileInputStream(p);
-    try {
-      final byte[] r = new byte[(int) in.getChannel().size()];
-      IO.readFully(in, r, 0, r.length);
-      return r;
-    } finally {
-      in.close();
-    }
-  }
-
-  private static byte[] compress(final byte[] raw) throws IOException {
-    final ByteArrayOutputStream out = new ByteArrayOutputStream();
-    final GZIPOutputStream gz = new GZIPOutputStream(out);
-    gz.write(raw);
-    gz.finish();
-    gz.flush();
-    return out.toByteArray();
-  }
-
   private final File staticBase;
   private final String staticBasePath;
+  private final boolean refresh;
+  private final LoadingCache<String, Resource> cache;
 
   @Inject
-  StaticServlet(final SitePaths site) {
+  StaticServlet(@GerritServerConfig Config cfg, SitePaths site) {
     File f;
     try {
       f = site.static_dir.getCanonicalFile();
@@ -99,70 +104,101 @@
     }
     staticBase = f;
     staticBasePath = staticBase.getPath() + File.separator;
+    refresh = cfg.getBoolean("site", "refreshHeaderFooter", true);
+    cache = CacheBuilder.newBuilder()
+        .maximumWeight(1 << 20)
+        .weigher(new Weigher<String, Resource>() {
+          @Override
+          public int weigh(String name, Resource r) {
+            return 2 * name.length() + r.raw.length;
+          }
+        })
+        .build(new CacheLoader<String, Resource>() {
+          @Override
+          public Resource load(String name) throws Exception {
+            return loadResource(name);
+          }
+        });
   }
 
-  private File local(final HttpServletRequest req) {
-    final String name = req.getPathInfo();
-    if (name.length() < 2 || !name.startsWith("/") || isUnreasonableName(name)) {
-      // Too short to be a valid file name, or doesn't start with
-      // the path info separator like we expected.
-      //
+  @Nullable
+  Resource getResource(String name) {
+    try {
+      return cache.get(name);
+    } catch (ExecutionException e) {
+      log.warn(String.format("Cannot load static resource %s", name), e);
       return null;
     }
+  }
 
-    final File p = new File(staticBase, name.substring(1));
-
-    // Ensure that the requested file is *actually* within the static dir base.
-    try {
-      if (!p.getCanonicalFile().getPath().startsWith(staticBasePath))
-        return null;
-    } catch (IOException e) {
-        return null;
+  private Resource getResource(HttpServletRequest req) throws ExecutionException {
+    String name = CharMatcher.is('/').trimFrom(req.getPathInfo());
+    if (isUnreasonableName(name)) {
+      return Resource.NOT_FOUND;
     }
 
-    return p.isFile() ? p : null;
+    Resource r = cache.get(name);
+    if (r == Resource.NOT_FOUND) {
+      return Resource.NOT_FOUND;
+    }
+
+    if (refresh && r.isStale()) {
+      cache.invalidate(name);
+      r = cache.get(name);
+    }
+    return r;
   }
 
   private static boolean isUnreasonableName(String name) {
-    if (name.charAt(name.length() -1) == '/') return true; // no suffix
+    if (name.length() < 1) return true;
     if (name.indexOf('\\') >= 0) return true; // no windows/dos stlye paths
     if (name.startsWith("../")) return true; // no "../etc/passwd"
     if (name.contains("/../")) return true; // no "foo/../etc/passwd"
     if (name.contains("/./")) return true; // "foo/./foo" is insane to ask
     if (name.contains("//")) return true; // windows UNC path can be "//..."
-
     return false; // is a reasonable name
   }
 
   @Override
-  protected long getLastModified(final HttpServletRequest req) {
-    final File p = local(req);
-    return p != null ? p.lastModified() : -1;
-  }
-
-  @Override
   protected void doGet(final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
-    final File p = local(req);
-    if (p == null) {
+    Resource r;
+    try {
+      r = getResource(req);
+    } catch (ExecutionException e) {
+      log.warn(String.format(
+          "Cannot load static resource %s",
+          req.getPathInfo()), e);
       CacheHeaders.setNotCacheable(rsp);
-      rsp.setStatus(HttpServletResponse.SC_NOT_FOUND);
+      rsp.setStatus(SC_INTERNAL_SERVER_ERROR);
       return;
     }
 
-    final String type = contentType(p.getName());
-    final byte[] tosend;
-    if (!type.equals("application/x-javascript")
-        && RPCServletUtils.acceptsGzipEncoding(req)) {
-      rsp.setHeader("Content-Encoding", "gzip");
-      tosend = compress(readFile(p));
-    } else {
-      tosend = readFile(p);
+    String e = req.getParameter("e");
+    if (r == Resource.NOT_FOUND || (e != null && !r.etag.equals(e))) {
+      CacheHeaders.setNotCacheable(rsp);
+      rsp.setStatus(SC_NOT_FOUND);
+      return;
+    } else if (r.etag.equals(req.getHeader(IF_NONE_MATCH))) {
+      rsp.setStatus(SC_NOT_MODIFIED);
+      return;
     }
 
-    CacheHeaders.setCacheable(req, rsp, 12, TimeUnit.HOURS);
-    rsp.setDateHeader("Last-Modified", p.lastModified());
-    rsp.setContentType(type);
+    byte[] tosend = r.raw;
+    if (!r.contentType.equals(JS) && RPCServletUtils.acceptsGzipEncoding(req)) {
+      byte[] gz = HtmlDomUtil.compress(tosend);
+      if ((gz.length + 24) < tosend.length) {
+        rsp.setHeader(CONTENT_ENCODING, "gzip");
+        tosend = gz;
+      }
+    }
+    if (e != null && r.etag.equals(e)) {
+      CacheHeaders.setCacheable(req, rsp, 360, DAYS, false);
+    } else {
+      CacheHeaders.setCacheable(req, rsp, 15, MINUTES, refresh);
+    }
+    rsp.setHeader(ETAG, r.etag);
+    rsp.setContentType(r.contentType);
     rsp.setContentLength(tosend.length);
     final OutputStream out = rsp.getOutputStream();
     try {
@@ -171,4 +207,54 @@
       out.close();
     }
   }
+
+  private Resource loadResource(String name) throws IOException {
+    File p = new File(staticBase, name);
+    try {
+      p = p.getCanonicalFile();
+    } catch (IOException e) {
+      return Resource.NOT_FOUND;
+    }
+    if (!p.getPath().startsWith(staticBasePath)) {
+      return Resource.NOT_FOUND;
+    }
+
+    long ts = p.lastModified();
+    FileInputStream in;
+    try {
+      in = new FileInputStream(p);
+    } catch (FileNotFoundException e) {
+      return Resource.NOT_FOUND;
+    }
+
+    byte[] raw;
+    try {
+      raw = ByteStreams.toByteArray(in);
+    } finally {
+      in.close();
+    }
+    return new Resource(p, ts, contentType(name), raw);
+  }
+
+  static class Resource {
+    static final Resource NOT_FOUND = new Resource(null, -1, "", new byte[] {});
+
+    final File src;
+    final long lastModified;
+    final String contentType;
+    final String etag;
+    final byte[] raw;
+
+    Resource(File src, long lastModified, String contentType, byte[] raw) {
+      this.src = src;
+      this.lastModified = lastModified;
+      this.contentType = contentType;
+      this.etag = Hashing.md5().hashBytes(raw).toString();
+      this.raw = raw;
+    }
+
+    boolean isStale() {
+      return lastModified != src.lastModified();
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index c6a925e..e1d8627 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -216,10 +216,12 @@
     try {
       openSchema();
       openRepository();
-      openBranch();
+
+      RefUpdate branchUpdate = openBranch();
+      boolean reopen = false;
+
       final ListMultimap<SubmitType, Change> toSubmit =
           validateChangeList(db.changes().submitted(destBranch).toList());
-
       final ListMultimap<SubmitType, CodeReviewCommit> toMergeNextTurn =
           ArrayListMultimap.create();
       final List<CodeReviewCommit> potentiallyStillSubmittableOnNextRun =
@@ -229,10 +231,14 @@
         final Set<SubmitType> submitTypes =
             new HashSet<Project.SubmitType>(toMerge.keySet());
         for (final SubmitType submitType : submitTypes) {
-          final RefUpdate branchUpdate = openBranch();
+          if (reopen) {
+            branchUpdate = openBranch();
+          }
           final SubmitStrategy strategy = createStrategy(submitType);
           preMerge(strategy, toMerge.get(submitType));
           updateBranch(strategy, branchUpdate);
+          reopen = true;
+
           updateChangeStatus(toSubmit.get(submitType));
           updateSubscriptions(toSubmit.get(submitType));
 
@@ -368,27 +374,15 @@
       if (branchUpdate.getOldObjectId() != null) {
         branchTip =
             (CodeReviewCommit) rw.parseCommit(branchUpdate.getOldObjectId());
-      } else {
+      } else if (repo.getFullBranch().equals(destBranch.get())) {
         branchTip = null;
-      }
-
-      try {
-        final Ref destRef = repo.getRef(destBranch.get());
-        if (destRef != null) {
-          branchUpdate.setExpectedOldObjectId(destRef.getObjectId());
-        } else if (repo.getFullBranch().equals(destBranch.get())) {
-          branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
-        } else {
-          for (final Change c : db.changes().submitted(destBranch).toList()) {
-            setNew(c, message(c, "Your change could not be merged, "
-                + "because the destination branch does not exist anymore."));
-          }
+        branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
+      } else {
+        for (final Change c : db.changes().submitted(destBranch).toList()) {
+          setNew(c, message(c, "Your change could not be merged, "
+              + "because the destination branch does not exist anymore."));
         }
-      } catch (IOException e) {
-        throw new MergeException(
-            "Failed to check existence of destination branch", e);
       }
-
       return branchUpdate;
     } catch (IOException e) {
       throw new MergeException("Cannot open branch", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index c5e2739..e9704e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -189,18 +189,18 @@
 
     m.setBase(originalCommit.getParent(0));
     if (m.merge(mergeTip, originalCommit)) {
+      ObjectId tree = m.getResultTreeId();
+      if (tree.equals(mergeTip.getTree())) {
+        return null;
+      }
 
-      final CommitBuilder mergeCommit = new CommitBuilder();
-
-      mergeCommit.setTreeId(m.getResultTreeId());
+      CommitBuilder mergeCommit = new CommitBuilder();
+      mergeCommit.setTreeId(tree);
       mergeCommit.setParentId(mergeTip);
       mergeCommit.setAuthor(originalCommit.getAuthorIdent());
       mergeCommit.setCommitter(cherryPickCommitterIdent);
       mergeCommit.setMessage(commitMsg);
-
-      final ObjectId id = commit(inserter, mergeCommit);
-
-      return rw.parseCommit(id);
+      return rw.parseCommit(commit(inserter, mergeCommit));
     } else {
       return null;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
index f4a4f20..b67faa3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
@@ -89,8 +89,7 @@
    * Results may not be immediately visible to searchers, but should be visible
    * within a reasonable amount of time.
    *
-   * @param cd change document with all index fields prepopulated; see
-   *     {@link ChangeData#fillIndexFields}.
+   * @param cd change document
    *
    * @throws IOException if the change could not be inserted.
    */
@@ -103,8 +102,7 @@
    * new field values. Results may not be immediately visible to searchers, but
    * should be visible within a reasonable amount of time.
    *
-   * @param cd change document with all index fields prepopulated; see
-   *     {@link ChangeData#fillIndexFields}.
+   * @param cd change document
    *
    * @throws IOException
    */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
index 483ecaf..6647652 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
@@ -77,7 +77,7 @@
 
       boolean perUser = false;
       Map<AccessSection, Project.NameKey> sectionToProject = Maps.newLinkedHashMap();
-      for (SectionMatcher matcher : matcherList) {
+      for (SectionMatcher sm : matcherList) {
         // If the matcher has to expand parameters and its prefix matches the
         // reference there is a very good chance the reference is actually user
         // specific, even if the matcher does not match the reference. Since its
@@ -91,12 +91,12 @@
         // references are usually less frequent than the non-user references.
         //
         if (username != null && !perUser
-            && matcher instanceof SectionMatcher.ExpandParameters) {
-          perUser = ((SectionMatcher.ExpandParameters) matcher).matchPrefix(ref);
+            && sm.matcher instanceof RefPatternMatcher.ExpandParameters) {
+          perUser = ((RefPatternMatcher.ExpandParameters) sm.matcher).matchPrefix(ref);
         }
 
-        if (matcher.match(ref, username)) {
-          sectionToProject.put(matcher.section, matcher.project);
+        if (sm.match(ref, username)) {
+          sectionToProject.put(sm.section, sm.project);
         }
       }
       List<AccessSection> sections = Lists.newArrayList(sectionToProject.keySet());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPatternMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPatternMatcher.java
new file mode 100644
index 0000000..b71d194
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefPatternMatcher.java
@@ -0,0 +1,122 @@
+// Copyright (C) 2013 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.project;
+
+import static com.google.gerrit.server.project.RefControl.isRE;
+import com.google.gerrit.common.data.ParameterizedString;
+import dk.brics.automaton.Automaton;
+import java.util.Collections;
+import java.util.regex.Pattern;
+
+abstract class RefPatternMatcher {
+  public static RefPatternMatcher getMatcher(String pattern) {
+    if (pattern.contains("${")) {
+      return new ExpandParameters(pattern);
+    } else if (isRE(pattern)) {
+      return new Regexp(pattern);
+    } else if (pattern.endsWith("/*")) {
+      return new Prefix(pattern.substring(0, pattern.length() - 1));
+    } else {
+      return new Exact(pattern);
+    }
+  }
+
+  abstract boolean match(String ref, String username);
+
+  private static class Exact extends RefPatternMatcher {
+    private final String expect;
+
+    Exact(String name) {
+      expect = name;
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      return expect.equals(ref);
+    }
+  }
+
+  private static class Prefix extends RefPatternMatcher {
+    private final String prefix;
+
+    Prefix(String pfx) {
+      prefix = pfx;
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      return ref.startsWith(prefix);
+    }
+  }
+
+  private static class Regexp extends RefPatternMatcher {
+    private final Pattern pattern;
+
+    Regexp(String re) {
+      pattern = Pattern.compile(re);
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      return pattern.matcher(ref).matches();
+    }
+  }
+
+  static class ExpandParameters extends RefPatternMatcher {
+    private final ParameterizedString template;
+    private final String prefix;
+
+    ExpandParameters(String pattern) {
+      template = new ParameterizedString(pattern);
+
+      if (isRE(pattern)) {
+        // Replace ${username} with ":USERNAME:" as : is not legal
+        // in a reference and the string :USERNAME: is not likely to
+        // be a valid part of the regex. This later allows the pattern
+        // prefix to be clipped, saving time on evaluation.
+        Automaton am =
+            RefControl.toRegExp(
+                template.replace(Collections.singletonMap("username",
+                    ":USERNAME:"))).toAutomaton();
+        String rePrefix = am.getCommonPrefix();
+        prefix = rePrefix.substring(0, rePrefix.indexOf(":USERNAME:"));
+      } else {
+        prefix = pattern.substring(0, pattern.indexOf("${"));
+      }
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      if (!ref.startsWith(prefix) || username == null) {
+        return false;
+      }
+
+      String u;
+      if (isRE(template.getPattern())) {
+        u = username.replace(".", "\\.");
+      } else {
+        u = username;
+      }
+
+      RefPatternMatcher next =
+          getMatcher(template.replace(Collections.singletonMap("username", u)));
+      return next != null ? next.match(ref, username) : false;
+    }
+
+    boolean matchPrefix(String ref) {
+      return ref.startsWith(prefix);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
index 6f8af80..44c8b9a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
@@ -14,147 +14,38 @@
 
 package com.google.gerrit.server.project;
 
-import static com.google.gerrit.server.project.RefControl.isRE;
-
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.reviewdb.client.Project;
 
-import dk.brics.automaton.Automaton;
-
-import java.util.Collections;
-import java.util.regex.Pattern;
-
 /**
  * Matches an AccessSection against a reference name.
  * <p>
  * These matchers are "compiled" versions of the AccessSection name, supporting
  * faster selection of which sections are relevant to any given input reference.
  */
-abstract class SectionMatcher {
+class SectionMatcher extends RefPatternMatcher {
   static SectionMatcher wrap(Project.NameKey project, AccessSection section) {
     String ref = section.getName();
     if (AccessSection.isValid(ref)) {
-      return wrap(project, ref, section);
+      return new SectionMatcher(project, section, getMatcher(ref));
     } else {
       return null;
     }
   }
 
-  static SectionMatcher wrap(Project.NameKey project, String pattern,
-      AccessSection section) {
-    if (pattern.contains("${")) {
-      return new ExpandParameters(project, pattern, section);
-
-    } else if (isRE(pattern)) {
-      return new Regexp(project, pattern, section);
-
-    } else if (pattern.endsWith("/*")) {
-      return new Prefix(project, pattern.substring(0, pattern.length() - 1),
-          section);
-
-    } else {
-      return new Exact(project, pattern, section);
-    }
-  }
-
   final Project.NameKey project;
   final AccessSection section;
+  final RefPatternMatcher matcher;
 
-  SectionMatcher(Project.NameKey project, AccessSection section) {
+  SectionMatcher(Project.NameKey project, AccessSection section,
+      RefPatternMatcher matcher) {
     this.project = project;
     this.section = section;
+    this.matcher = matcher;
   }
 
-  abstract boolean match(String ref, String username);
-
-  private static class Exact extends SectionMatcher {
-    private final String expect;
-
-    Exact(Project.NameKey project, String name, AccessSection section) {
-      super(project, section);
-      expect = name;
-    }
-
-    @Override
-    boolean match(String ref, String username) {
-      return expect.equals(ref);
-    }
-  }
-
-  private static class Prefix extends SectionMatcher {
-    private final String prefix;
-
-    Prefix(Project.NameKey project, String pfx, AccessSection section) {
-      super(project, section);
-      prefix = pfx;
-    }
-
-    @Override
-    boolean match(String ref, String username) {
-      return ref.startsWith(prefix);
-    }
-  }
-
-  private static class Regexp extends SectionMatcher {
-    private final Pattern pattern;
-
-    Regexp(Project.NameKey project, String re, AccessSection section) {
-      super(project, section);
-      pattern = Pattern.compile(re);
-    }
-
-    @Override
-    boolean match(String ref, String username) {
-      return pattern.matcher(ref).matches();
-    }
-  }
-
-  static class ExpandParameters extends SectionMatcher {
-    private final ParameterizedString template;
-    private final String prefix;
-
-    ExpandParameters(Project.NameKey project, String pattern,
-        AccessSection section) {
-      super(project, section);
-      template = new ParameterizedString(pattern);
-
-      if (isRE(pattern)) {
-        // Replace ${username} with ":USERNAME:" as : is not legal
-        // in a reference and the string :USERNAME: is not likely to
-        // be a valid part of the regex. This later allows the pattern
-        // prefix to be clipped, saving time on evaluation.
-        Automaton am = RefControl.toRegExp(
-            template.replace(Collections.singletonMap("username", ":USERNAME:")))
-            .toAutomaton();
-        String rePrefix = am.getCommonPrefix();
-        prefix = rePrefix.substring(0, rePrefix.indexOf(":USERNAME:"));
-      } else {
-        prefix = pattern.substring(0, pattern.indexOf("${"));
-      }
-    }
-
-    @Override
-    boolean match(String ref, String username) {
-      if (!ref.startsWith(prefix) || username == null) {
-        return false;
-      }
-
-      String u;
-      if (isRE(template.getPattern())) {
-        u = username.replace(".", "\\.");
-      } else {
-        u = username;
-      }
-
-      SectionMatcher next = wrap(project,
-          template.replace(Collections.singletonMap("username", u)),
-          section);
-      return next != null ? next.match(ref, username) : false;
-    }
-
-   boolean matchPrefix(String ref) {
-     return ref.startsWith(prefix);
-    }
+  @Override
+  boolean match(String ref, String username) {
+    return this.matcher.match(ref, username);
   }
 }
diff --git a/lib/asciidoctor/BUCK b/lib/asciidoctor/BUCK
new file mode 100644
index 0000000..25b9eb8
--- /dev/null
+++ b/lib/asciidoctor/BUCK
@@ -0,0 +1,39 @@
+include_defs('//lib/maven.defs')
+
+java_binary(
+  name = 'asciidoc',
+  main_class = 'org.asciidoctor.cli.AsciidoctorInvoker',
+  deps = [':asciidoctor'],
+  visibility = ['PUBLIC'],
+)
+
+maven_jar(
+  name = 'asciidoctor',
+  id = 'org.asciidoctor:asciidoctor-java-integration:0.1.3',
+  sha1 = '5cf21b4331d737ef0f3b3f543a7e5a343c1f27ec',
+  license = 'Apache2.0',
+  visibility = [],
+  attach_source = False,
+  deps = [
+    ':jcommander',
+    ':jruby',
+  ],
+)
+
+maven_jar(
+  name = 'jcommander',
+  id = 'com.beust:jcommander:1.30',
+  sha1 = 'c440b30a944ba199751551aee393f8aa03b3c327',
+  license = 'Apache2.0',
+  visibility = [],
+  attach_source = False,
+)
+
+maven_jar(
+  name = 'jruby',
+  id = 'org.jruby:jruby-complete:1.7.4',
+  sha1 = '74984d84846523bd7da49064679ed1ccf199e1db',
+  license = 'DO_NOT_DISTRIBUTE',
+  visibility = [],
+  attach_source = False,
+)