Merge branch 'stable-2.14'

* stable-2.14:
  Fix computation of the target branch on CI
  Align button width to labels
  Bring navigation back to Gerrit
  Align action buttons with the rest of plugin info
  Display plugin description
  Define new annotation to get the plugin URL path
  Add intro page after first admin login into Gerrit
  Redirect using path-based URL

Change-Id: I8939e170e1de6b8282b4a1b2f9e2e36c2c761ac8
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/FirstWebLoginListener.java b/src/main/java/com/googlesource/gerrit/plugins/manager/FirstWebLoginListener.java
index 2746bff..722d406 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/FirstWebLoginListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/FirstWebLoginListener.java
@@ -14,7 +14,6 @@
 
 package com.googlesource.gerrit.plugins.manager;
 
-import com.google.gerrit.extensions.annotations.PluginCanonicalWebUrl;
 import com.google.gerrit.extensions.annotations.PluginData;
 import com.google.gerrit.httpd.WebLoginListener;
 import com.google.gerrit.server.IdentifiedUser;
@@ -31,18 +30,18 @@
 public class FirstWebLoginListener implements WebLoginListener {
   private final Path pluginData;
   private final PluginLoader pluginLoader;
-  private final String pluginUrl;
   private final PluginManagerConfig config;
+  private final String pluginUrlPath;
 
   @Inject
   public FirstWebLoginListener(
       PluginLoader pluginLoader,
       @PluginData Path pluginData,
-      @PluginCanonicalWebUrl String pluginUrl,
+      @PluginCanonicalWebUrlPath String pluginUrlPath,
       PluginManagerConfig config) {
     this.pluginData = pluginData;
     this.pluginLoader = pluginLoader;
-    this.pluginUrl = pluginUrl;
+    this.pluginUrlPath = pluginUrlPath;
     this.config = config;
   }
 
@@ -52,7 +51,7 @@
     if (pluginLoader.isRemoteAdminEnabled() && config.canAdministerPlugins()) {
       Path firstLoginFile = pluginData.resolve("firstLogin." + user.getAccountId().get());
       if (!firstLoginFile.toFile().exists()) {
-        response.sendRedirect(pluginUrl + "static/index.html");
+        response.sendRedirect(pluginUrlPath + "static/intro.html");
 
         Files.write(firstLoginFile, new Date().toString().getBytes(), StandardOpenOption.CREATE);
       }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/GerritVersionBranch.java b/src/main/java/com/googlesource/gerrit/plugins/manager/GerritVersionBranch.java
index 02aeea3..ccf3e69 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/GerritVersionBranch.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/GerritVersionBranch.java
@@ -15,7 +15,7 @@
 package com.googlesource.gerrit.plugins.manager;
 
 public class GerritVersionBranch {
-  private static final String GERRIT_NEXT_VERSION = "2.13";
+  private static final String GERRIT_NEXT_VERSION = "2.15";
 
   public static String getBranch(String gerritVersion) {
     if (gerritVersion == null
@@ -25,6 +25,12 @@
       return "master";
     }
     String[] versionNumbers = gerritVersion.split("\\.");
+    String major = versionNumbers[0];
+    String minor = versionNumbers[1];
+
+    if (minor.contains("-")) {
+      minor = minor.split("-")[0];
+    }
 
     if (versionNumbers.length > 2) {
       String fixVersionNumber = versionNumbers[2];
@@ -36,6 +42,7 @@
         }
       }
     }
-    return "stable-" + versionNumbers[0] + "." + versionNumbers[1];
+
+    return "stable-" + major + "." + minor;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/Module.java b/src/main/java/com/googlesource/gerrit/plugins/manager/Module.java
index 2f231be..8a6b387 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/Module.java
@@ -27,6 +27,10 @@
 
   @Override
   protected void configure() {
+    bind(String.class)
+        .annotatedWith(PluginCanonicalWebUrlPath.class)
+        .toProvider(PluginCanonicalWebUrlPathProvider.class);
+
     DynamicSet.bind(binder(), TopMenu.class).to(PluginManagerTopMenu.class);
 
     DynamicSet.setOf(binder(), PluginsRepository.class);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/PluginCanonicalWebUrlPath.java b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginCanonicalWebUrlPath.java
new file mode 100644
index 0000000..3f17da8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginCanonicalWebUrlPath.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2017 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.googlesource.gerrit.plugins.manager;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import com.google.inject.BindingAnnotation;
+
+/**
+ * Annotation applied to a String containing the plugin canonical web URL path.
+ *
+ * <p>A plugin or extension may receive this string by Guice injection to discover the canonical web
+ * URL path under which the plugin is available:
+ *
+ * <pre>
+ *  {@literal @Inject}
+ *  MyType(@PluginCanonicalWebUrlPath String myUrl) {
+ *  ...
+ *  }
+ * </pre>
+ */
+@Target({ElementType.PARAMETER, ElementType.FIELD})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface PluginCanonicalWebUrlPath {}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/PluginCanonicalWebUrlPathProvider.java b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginCanonicalWebUrlPathProvider.java
new file mode 100644
index 0000000..6581f87
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginCanonicalWebUrlPathProvider.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2017 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.googlesource.gerrit.plugins.manager;
+
+import java.net.URI;
+
+import com.google.gerrit.extensions.annotations.PluginCanonicalWebUrl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class PluginCanonicalWebUrlPathProvider implements Provider<String> {
+  private final URI pluginCanonicalUri;
+
+  @Inject
+  PluginCanonicalWebUrlPathProvider(@PluginCanonicalWebUrl String pluginCanonicalUrl) {
+    this.pluginCanonicalUri = URI.create(pluginCanonicalUrl);
+  }
+
+  @Override
+  public String get() {
+    return pluginCanonicalUri.getPath();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/PluginManagerTopMenu.java b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginManagerTopMenu.java
index c79a818..378ff83 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/PluginManagerTopMenu.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginManagerTopMenu.java
@@ -14,7 +14,6 @@
 
 package com.googlesource.gerrit.plugins.manager;
 
-import com.google.gerrit.extensions.annotations.PluginCanonicalWebUrl;
 import com.google.gerrit.extensions.client.MenuItem;
 import com.google.gerrit.extensions.webui.TopMenu;
 import com.google.gerrit.server.plugins.PluginLoader;
@@ -33,14 +32,14 @@
 
   @Inject
   public PluginManagerTopMenu(
-      @PluginCanonicalWebUrl String myUrl, PluginLoader loader, PluginManagerConfig config) {
+      @PluginCanonicalWebUrlPath String myUrl, PluginLoader loader, PluginManagerConfig config) {
     this.loader = loader;
     this.config = config;
     this.menuEntries =
         Arrays.asList(
             new MenuEntry(
                 "Plugins",
-                Arrays.asList(new MenuItem("Manage", myUrl + "static/index.html", "_blank"))));
+                Arrays.asList(new MenuItem("Manage", myUrl + "static/index.html", "_self"))));
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartGson.java b/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartGson.java
index b091ca6..f8516e2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartGson.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartGson.java
@@ -35,6 +35,10 @@
     return SmartJson.of(gson.fromJson(getReader(url), JsonObject.class));
   }
 
+  public SmartJson of(String jsonText) {
+    return SmartJson.of(gson.fromJson(jsonText, JsonObject.class));
+  }
+
   public <T> T get(String url, Class<T> classOfT) throws IOException {
     try (Reader reader = getReader(url)) {
       return gson.fromJson(reader, classOfT);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartJson.java b/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartJson.java
index abfc879..3ea2af0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartJson.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartJson.java
@@ -50,7 +50,9 @@
   }
 
   public Optional<SmartJson> getOptional(String fieldName) {
-    if (jsonElem != null && jsonElem.getAsJsonObject().get(fieldName) != null) {
+    if (jsonElem != null
+        && !jsonElem.isJsonNull()
+        && jsonElem.getAsJsonObject().get(fieldName) != null) {
       return Optional.of(SmartJson.of(jsonElem.getAsJsonObject().get(fieldName)));
     }
     return Optional.empty();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsDescriptions.java b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsDescriptions.java
new file mode 100644
index 0000000..3474b19
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsDescriptions.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2017 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.googlesource.gerrit.plugins.manager.repository;
+
+import java.util.HashMap;
+import java.util.Optional;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class CorePluginsDescriptions {
+  private final HashMap<String, String> pluginsDescriptions;
+
+  @Inject
+  public CorePluginsDescriptions() {
+    pluginsDescriptions = new HashMap<>();
+    pluginsDescriptions.put("commit-message-length-validator",
+        "Plugin to validate that commit messages conform to length limits");
+    pluginsDescriptions.put("download-commands", "Adds the standard download schemes and commands");
+    pluginsDescriptions.put("hooks", "Old-style fork+exec hooks");
+    pluginsDescriptions.put("replication", "Copies to other servers using the Git protocol");
+    pluginsDescriptions.put("reviewnotes", "Annotates merged commits using notes on refs/notes/review");
+    pluginsDescriptions.put("singleusergroup", "GroupBackend enabling users to be directly added to access rules");
+  }
+
+  public Optional<String> get(String plugin) {
+    return Optional.ofNullable(pluginsDescriptions.get(plugin));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsRepository.java b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsRepository.java
index 1f5b055..8e96e6a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsRepository.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsRepository.java
@@ -39,11 +39,14 @@
 public class CorePluginsRepository implements PluginsRepository {
   private static final Logger log = LoggerFactory.getLogger(CorePluginsRepository.class);
   private static final String GERRIT_VERSION = Version.getVersion();
+
   private final SitePaths site;
+  private final CorePluginsDescriptions pluginsDescriptions;
 
   @Inject
-  public CorePluginsRepository(SitePaths site) {
+  public CorePluginsRepository(SitePaths site, CorePluginsDescriptions pd) {
     this.site = site;
+    this.pluginsDescriptions = pd;
   }
 
   static class SelectPluginsFromJar implements Predicate<JarEntry> {
@@ -54,7 +57,7 @@
     }
   }
 
-  static class ExtractPluginInfoFromJarEntry implements Function<JarEntry, PluginInfo> {
+  class ExtractPluginInfoFromJarEntry implements Function<JarEntry, PluginInfo> {
     private String gerritWarFilename;
 
     public ExtractPluginInfoFromJarEntry(String gerritWarFilename) {
@@ -70,13 +73,16 @@
           Manifest manifestJarEntry = getManifestEntry(pluginJar);
           if (manifestJarEntry != null) {
             Attributes pluginAttributes = manifestJarEntry.getMainAttributes();
+            String pluginName = pluginAttributes.getValue("Gerrit-PluginName");
             return new PluginInfo(
-                pluginAttributes.getValue("Gerrit-PluginName"),
+                pluginName,
+                pluginsDescriptions.get(pluginName).orElse(""),
                 pluginAttributes.getValue("Implementation-Version"),
                 "",
                 pluginUrl.toString());
           }
-          return new PluginInfo(entryName.getFileName().toString(), "", "", pluginUrl.toString());
+          return new PluginInfo(
+              entryName.getFileName().toString(), "", "", "", pluginUrl.toString());
         } catch (IOException e) {
           log.error("Unable to open plugin " + pluginUrl, e);
           return null;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java
index a01555a..6eb29a3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java
@@ -42,8 +42,6 @@
 
   private static final Logger log = LoggerFactory.getLogger(JenkinsCiPluginsRepository.class);
 
-  private static final Optional<PluginInfo> noPluginInfo = Optional.empty();
-
   private final PluginManagerConfig config;
 
   private HashMap<String, List<PluginInfo>> cache = new HashMap<>();
@@ -105,32 +103,25 @@
     SmartJson jobDetails = gson.get(url + "/api/json");
     Optional<SmartJson> lastSuccessfulBuild = jobDetails.getOptional("lastSuccessfulBuild");
 
-    return lastSuccessfulBuild
-        .map(
-            new Function<SmartJson, Optional<PluginInfo>>() {
-              @Override
-              public Optional<PluginInfo> apply(SmartJson build) {
-                String buildUrl = build.getString("url");
-                try {
-                  return getPluginArtifactInfo(gson, buildUrl);
-                } catch (IOException e) {
-                  log.warn("Cannot retrieve plugin artifact info from {}", buildUrl);
-                  return noPluginInfo;
-                }
-              }
-            })
-        .orElse(noPluginInfo);
+    return lastSuccessfulBuild.flatMap(
+        new Function<SmartJson, Optional<PluginInfo>>() {
+          @Override
+          public Optional<PluginInfo> apply(SmartJson build) {
+            String buildUrl = build.getString("url");
+            return getPluginArtifactInfo(buildUrl);
+          }
+        });
   }
 
-  private Optional<PluginInfo> getPluginArtifactInfo(SmartGson gson, String url)
-      throws IOException {
-    SmartJson buildExecution = gson.get(url + "/api/json");
-    JsonArray artifacts = buildExecution.get("artifacts").get().getAsJsonArray();
-    if (artifacts.size() == 0) {
+  private Optional<PluginInfo> getPluginArtifactInfo(String url) {
+    Optional<SmartJson> buildExecution = tryGetJson(url + "/api/json");
+    Optional<JsonArray> artifacts =
+        buildExecution.map(json -> json.get("artifacts").get().getAsJsonArray());
+    if (artifacts.orElse(new JsonArray()).size() == 0) {
       return Optional.empty();
     }
 
-    Optional<SmartJson> artifactJson = findArtifact(artifacts, ".jar");
+    Optional<SmartJson> artifactJson = artifacts.flatMap(a -> findArtifact(a, ".jar"));
     if (!artifactJson.isPresent()) {
       return Optional.empty();
     }
@@ -141,12 +132,36 @@
     String pluginName =
         isMavenBuild(pluginPathParts)
             ? fixPluginNameForMavenBuilds(pluginPathParts)
-            : pluginPathParts[pluginPathParts.length - 2];
+            : pluginNameOfJar(pluginPathParts);
 
-    String pluginUrl = String.format("%s/artifact/%s", buildExecution.getString("url"), pluginPath);
+    String pluginUrl =
+        String.format("%s/artifact/%s", buildExecution.get().getString("url"), pluginPath);
 
-    String pluginVersion = "";
-    Optional<SmartJson> verArtifactJson = findArtifact(artifacts, ".jar-version");
+    Optional<String> pluginVersion =
+        fetchArtifact(buildExecution.get(), artifacts.get(), ".jar-version");
+    Optional<String> pluginDescription =
+        fetchArtifactJson(buildExecution.get(), artifacts.get(), ".json")
+            .flatMap(json -> json.getOptionalString("description"));
+
+    for (JsonElement elem : buildExecution.get().get("actions").get().getAsJsonArray()) {
+      SmartJson elemJson = SmartJson.of(elem);
+      Optional<SmartJson> lastBuildRevision = elemJson.getOptional("lastBuiltRevision");
+
+      if (lastBuildRevision.isPresent()) {
+        String sha1 = lastBuildRevision.get().getString("SHA1").substring(0, 8);
+        return pluginVersion.map(
+            version ->
+                new PluginInfo(pluginName, pluginDescription.orElse(""), version, sha1, pluginUrl));
+      }
+    }
+
+    return Optional.empty();
+  }
+
+  private Optional<String> fetchArtifact(
+      SmartJson buildExecution, JsonArray artifacts, String artifactSuffix) {
+    StringBuilder artifactBody = new StringBuilder();
+    Optional<SmartJson> verArtifactJson = findArtifact(artifacts, artifactSuffix);
     if (verArtifactJson.isPresent()) {
       String versionUrl =
           String.format(
@@ -154,21 +169,40 @@
               buildExecution.getString("url"), verArtifactJson.get().getString("relativePath"));
       try (BufferedReader reader =
           new BufferedReader(new InputStreamReader(new URL(versionUrl).openStream()), 4096)) {
-        pluginVersion = reader.readLine();
+        String line;
+        while ((line = reader.readLine()) != null) {
+          if (artifactBody.length() > 0) {
+            artifactBody.append("\n");
+          }
+          artifactBody.append(line);
+        }
+      } catch (Exception e) {
+        log.error("Unable to fetch artifact from " + versionUrl);
+        return Optional.empty();
       }
     }
+    return Optional.of(artifactBody.toString());
+  }
 
-    String sha1 = "";
-    for (JsonElement elem : buildExecution.get("actions").get().getAsJsonArray()) {
-      SmartJson elemJson = SmartJson.of(elem);
-      Optional<SmartJson> lastBuildRevision = elemJson.getOptional("lastBuiltRevision");
+  private Optional<SmartJson> fetchArtifactJson(
+      SmartJson buildExecution, JsonArray artifacts, String artifactSuffix) {
+    Optional<SmartJson> jsonArtifact = findArtifact(artifacts, artifactSuffix);
+    return jsonArtifact.flatMap(
+        artifactJson ->
+            tryGetJson(
+                String.format(
+                    "%s/artifact/%s",
+                    buildExecution.getString("url"),
+                    jsonArtifact.get().getString("relativePath"))));
+  }
 
-      if (lastBuildRevision.isPresent()) {
-        sha1 = lastBuildRevision.get().getString("SHA1").substring(0, 8);
-      }
+  private Optional<SmartJson> tryGetJson(String url) {
+    try {
+      return Optional.of(gsonProvider.get().get(url));
+    } catch (IOException e) {
+      log.error("Cannot get JSON from " + url, e);
+      return Optional.empty();
     }
-
-    return Optional.of(new PluginInfo(pluginName, pluginVersion, sha1, pluginUrl));
   }
 
   private String fixPluginNameForMavenBuilds(String[] pluginPathParts) {
@@ -178,6 +212,18 @@
     return versionDelim > 0 ? mavenPluginFilename.substring(0, versionDelim) : mavenPluginFilename;
   }
 
+  private String pluginNameOfJar(String[] pluginJarParts) {
+    int filePos = pluginJarParts.length - 1;
+    int pathPos = filePos - 1;
+
+    if (pluginJarParts[filePos].startsWith(pluginJarParts[pathPos])) {
+      return pluginJarParts[pathPos];
+    }
+
+    int jarExtPos = pluginJarParts[filePos].indexOf(".jar");
+    return pluginJarParts[filePos].substring(0, jarExtPos);
+  }
+
   private boolean isMavenBuild(String[] pluginPathParts) {
     return pluginPathParts[pluginPathParts.length - 2].equals("target");
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginInfo.java b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginInfo.java
index c6af444..ac30a64 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginInfo.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginInfo.java
@@ -19,13 +19,15 @@
 public class PluginInfo {
   public final String id;
   public final String name;
+  public final String description;
   public final String version;
   public final String sha1;
   public final String url;
 
-  public PluginInfo(String name, String version, String sha1, String url) {
+  public PluginInfo(String name, String description, String version, String sha1, String url) {
     this.id = Url.encode(name);
     this.name = name;
+    this.description = description;
     this.version = version;
     this.sha1 = sha1;
     this.url = url;
diff --git a/src/main/resources/static/css/ie10-viewport-bug-workaround.css b/src/main/resources/static/css/ie10-viewport-bug-workaround.css
new file mode 100644
index 0000000..4b9518e
--- /dev/null
+++ b/src/main/resources/static/css/ie10-viewport-bug-workaround.css
@@ -0,0 +1,13 @@
+/*!
+ * IE10 viewport hack for Surface/desktop Windows 8 bug
+ * Copyright 2014-2015 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+/*
+ * See the Getting Started docs for more information:
+ * http://getbootstrap.com/getting-started/#support-ie10-width
+ */
+@-ms-viewport     { width: device-width; }
+@-o-viewport      { width: device-width; }
+@viewport         { width: device-width; }
diff --git a/src/main/resources/static/css/jumbotron.css b/src/main/resources/static/css/jumbotron.css
new file mode 100644
index 0000000..179d408
--- /dev/null
+++ b/src/main/resources/static/css/jumbotron.css
@@ -0,0 +1,5 @@
+/* Move down content because we have a fixed navbar that is 50px tall */
+body {
+  padding-top: 50px;
+  padding-bottom: 20px;
+}
diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css
index ddcd2b2..e187458 100644
--- a/src/main/resources/static/css/style.css
+++ b/src/main/resources/static/css/style.css
@@ -24,6 +24,14 @@
   width: 65px;
 }
 
+button.btn-sm {
+  width: 83px;
+}
+
+button.btn-100 {
+  width: 100%;
+}
+
 .label-success {
   background-color: #888888;
 }
diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html
index 9ca7d04..977ddac 100644
--- a/src/main/resources/static/index.html
+++ b/src/main/resources/static/index.html
@@ -27,7 +27,13 @@
           </div>
         </form>
         <ul class="nav navbar-nav navbar-right">
-          <li><a href="/">&gt; Go to Gerrit</a></li>
+          <li>
+            <div class="navbar-form navbar-right">
+              <button type="button"
+                  class="btn btn-sm btn-primary btn-100"
+                  ng-click="plugins.goToGerrit()">Done &raquo; Go To Gerrit</button>
+            </div>
+          </li>
         </ul>
       </div>
     </div>
@@ -39,19 +45,20 @@
           <tr>
             <th>Plugin Name</th>
             <th>Version</th>
-            <th>Upgrade / New Version Available</th>
-            <th>Latest Commit</th>
             <th>Actions</th>
           </tr>
         </thead>
         <tbody>
           <tr ng-repeat="prop in plugins.list | filter:searchPlugin">
-            <td>{{prop.id}}</td>
-            <td>{{prop.version}}</td>
-            <td>{{prop.update_version}}</td>
-            <td>{{prop.sha1}}</td>
+            <td><h4>{{prop.id}}<br/><small>{{prop.description.split('.')[0]}}</small></h4></td>
             <td>
-              <h5>
+              <p>{{prop.version}}</p>
+              <div ng-if="prop.update_version">
+              <span class="glyphicon glyphicon-save"></span><b>&nbsp;{{prop.update_version}}</b>
+              </div>
+            </td>
+            <td>
+              <h4>
                 <span id="installing-{{prop.id}}"
                   class="label label-default hidden">Installing</span>
                 <span
@@ -60,12 +67,12 @@
                 <span
                   id="failed-{{prop.id}}" class="label label-warning hidden">Failed</span>
                 <button id="{{prop.id}}" type="button"
-                  class="btn btn-xs btn-primary {{ (prop.version == '' && prop.update_version != undefined) ? '':'hidden' }}"
+                  class="btn btn-sm btn-primary {{ (prop.version == '' && prop.update_version != undefined) ? '':'hidden' }}"
                   ng-click="install(prop.id,prop.url)">Install</button>
                 <button id="{{prop.id}}" type="button"
-                  class="btn btn-xs btn-primary {{ (prop.version != '' && prop.update_version != '') ? '':'hidden' }}"
+                  class="btn btn-sm btn-primary {{ (prop.version != '' && prop.update_version != '') ? '':'hidden' }}"
                   ng-click="install(prop.id,prop.url)">Upgrade</button>
-              </h5>
+              </h4>
             </td>
           </tr>
         </tbody>
diff --git a/src/main/resources/static/intro.html b/src/main/resources/static/intro.html
new file mode 100644
index 0000000..2b1d215
--- /dev/null
+++ b/src/main/resources/static/intro.html
@@ -0,0 +1,146 @@
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
+<meta name="description" content="Gerrit Plugin Manager">
+<meta name="author" content="Gerrit Code Review">
+<link rel="icon" href="/favicon.ico">
+
+<title>Gerrit Plugin Manager - Introduction</title>
+
+<!-- Bootstrap core CSS -->
+<link
+  href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"
+  rel="stylesheet"
+  integrity="sha256-MfvZlkHCEqatNoGiOXveE8FIwMzZg4W85qfrfIFBfYc= sha512-dTfge/zgoMYpP7QbHy4gWMEGsbsdZeCXz7irItjcC3sPUFtf0kuFbDz/ixG7ArTxmDjLXDmezHubeNikyKGVyQ=="
+  crossorigin="anonymous">
+<link href="css/style.css" rel="stylesheet">
+
+<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
+<link href="css/ie10-viewport-bug-workaround.css" rel="stylesheet">
+
+<!-- Custom styles for this template -->
+<link href="css/jumbotron.css" rel="stylesheet">
+
+<!-- Just for debugging purposes. Don't actually copy these 2 lines! -->
+<!--[if lt IE 9]><script src="../../assets/js/ie8-responsive-file-warning.js"></script><![endif]-->
+<script src="js/ie-emulation-modes-warning.js"></script>
+
+<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
+<!--[if lt IE 9]>
+      <script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
+      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
+    <![endif]-->
+</head>
+
+<body>
+
+  <nav class="navbar navbar-inverse navbar-fixed-top">
+    <div class="container">
+      <div class="navbar-header">
+        <button type="button" class="navbar-toggle collapsed"
+          data-toggle="collapse" data-target="#navbar"
+          aria-expanded="false" aria-controls="navbar">
+          <span class="sr-only">Toggle navigation</span> <span
+            class="icon-bar"></span> <span class="icon-bar"></span> <span
+            class="icon-bar"></span>
+        </button>
+        <a class="navbar-brand" href="../../../">&raquo; Skip intro</a>
+      </div>
+    </div>
+  </nav>
+
+  <!-- Main jumbotron for a primary marketing message or call to action -->
+  <div class="jumbotron">
+    <div class="container">
+      <h1>Welcome!</h1>
+      <p><b><a href="https://www.gerritcodereview.com" target="_blank">Gerrit Code Review</a></b> provides web-based code review and repository
+        management for the Git version control system.</p>
+      <p>
+        Before starting using it, you may want to consider <strong>installing
+          some extra plugins</strong> to extend its functionality, including <i>replication
+          to remote locations</i>, integration with <i>Atlassian Jira</i>
+        and other issue trackers workflow, <i>OAuth 2.0
+          authentication</i> with external providers such as <i>Google
+          Account</i>, <i>GitHub</i> and <i>GitLab</i> and many others.
+      </p>
+      <p>
+      <p>
+        Once you're done with your plugins selection, click on the
+        top-right <strong>"&gt; Go to Gerrit"</strong> link on the
+        top-right corner to access the Gerrit Code Review main screen.
+      </p>
+      <a class="btn btn-primary btn-lg" href="index.html" role="button">Install
+        plugins &raquo;</a>
+      </p>
+    </div>
+  </div>
+
+  <div class="container">
+    <!-- Example row of columns -->
+    <div class="row">
+      <div class="col-md-4">
+        <h2><span class="glyphicon glyphicon-film" aria-hidden="true"></span>&nbsp;Learn more</h2>
+        <p>Before starting using Gerrit Code Review, you may want to
+          spend a few minutes learning the basic concepts and how its
+          workflow works.</p>
+        <p>Get information from the source, with an introduction
+          video starring Shawn Pearce, the Gerrit Code Review project
+          founder, at the EclipseCon 2013.</p>
+        <p>
+          <a class="btn btn-default"
+            href="https://www.youtube.com/watch?v=Wxx8XndqZ7A&t=15s"
+            target="_blank" role="button">Watch Video &raquo;</a>
+        </p>
+      </div>
+      <div class="col-md-4">
+        <h2><span class="glyphicon glyphicon-envelope" aria-hidden="true"></span>&nbsp;Get involved</h2>
+        <p>Gerrit Code Review is backed by a thriving Community of
+          users and contributors. The main channel to get in touch is
+          through the "repo-discuss" mailing list.</p>
+        <p>Join the discussions and feel free to ask questions.
+          Share your thoughts on how to improve Gerrit workflow and user
+          experience.
+        <p>
+          <a class="btn btn-default"
+            href="https://groups.google.com/forum/#!forum/repo-discuss"
+            target="_blank" role="button">Repo-discuss mailing list
+            &raquo;</a>
+        </p>
+      </div>
+      <div class="col-md-4">
+        <h2><span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span>&nbsp;Keep in touch</h2>
+        <p>
+          Get the latest news about Gerrit Code Review releases and
+          events by following the official social network channels of
+          the project, including <a
+            href="https://plus.google.com/communities/111271594706618791655"
+            target="_blank">Google+</a> and <a
+            href="https://twitter.com/gerritreview">Twitter</a>.
+        </p>
+        <p>Follow Gerrit and tweet about your experience to share
+          your thoughts and achievements with your daily code reviews.
+        <p>
+          <a href="https://twitter.com/gerritreview"
+            class="twitter-follow-button" data-show-count="false">Follow
+            @gerritreview</a>
+          <script async src="//platform.twitter.com/widgets.js"
+            charset="utf-8"></script>
+        </p>
+      </div>
+    </div>
+
+    <hr>
+
+    <footer>
+      <p><a href="https://www.gerritcodereview.com" href="_blank">Gerrit Code Review</a> - Copyright &copy; 2017 The Android Open Source Project -
+        Licensed under the Apache License, Version 2.0.</p>
+    </footer>
+  </div>
+  <!-- /container -->
+</body>
+</html>
diff --git a/src/main/resources/static/js/ie-emulation-modes-warning.js b/src/main/resources/static/js/ie-emulation-modes-warning.js
new file mode 100644
index 0000000..3f97ba5
--- /dev/null
+++ b/src/main/resources/static/js/ie-emulation-modes-warning.js
@@ -0,0 +1,51 @@
+// NOTICE!! DO NOT USE ANY OF THIS JAVASCRIPT
+// IT'S JUST JUNK FOR OUR DOCS!
+// ++++++++++++++++++++++++++++++++++++++++++
+/*!
+ * Copyright 2014-2015 Twitter, Inc.
+ *
+ * Licensed under the Creative Commons Attribution 3.0 Unported License. For
+ * details, see https://creativecommons.org/licenses/by/3.0/.
+ */
+// Intended to prevent false-positive bug reports about Bootstrap not working properly in old versions of IE due to folks testing using IE's unreliable emulation modes.
+(function () {
+  'use strict';
+
+  function emulatedIEMajorVersion() {
+    var groups = /MSIE ([0-9.]+)/.exec(window.navigator.userAgent)
+    if (groups === null) {
+      return null
+    }
+    var ieVersionNum = parseInt(groups[1], 10)
+    var ieMajorVersion = Math.floor(ieVersionNum)
+    return ieMajorVersion
+  }
+
+  function actualNonEmulatedIEMajorVersion() {
+    // Detects the actual version of IE in use, even if it's in an older-IE emulation mode.
+    // IE JavaScript conditional compilation docs: https://msdn.microsoft.com/library/121hztk3%28v=vs.94%29.aspx
+    // @cc_on docs: https://msdn.microsoft.com/library/8ka90k2e%28v=vs.94%29.aspx
+    var jscriptVersion = new Function('/*@cc_on return @_jscript_version; @*/')() // jshint ignore:line
+    if (jscriptVersion === undefined) {
+      return 11 // IE11+ not in emulation mode
+    }
+    if (jscriptVersion < 9) {
+      return 8 // IE8 (or lower; haven't tested on IE<8)
+    }
+    return jscriptVersion // IE9 or IE10 in any mode, or IE11 in non-IE11 mode
+  }
+
+  var ua = window.navigator.userAgent
+  if (ua.indexOf('Opera') > -1 || ua.indexOf('Presto') > -1) {
+    return // Opera, which might pretend to be IE
+  }
+  var emulated = emulatedIEMajorVersion()
+  if (emulated === null) {
+    return // Not IE
+  }
+  var nonEmulated = actualNonEmulatedIEMajorVersion()
+
+  if (emulated !== nonEmulated) {
+    window.alert('WARNING: You appear to be using IE' + nonEmulated + ' in IE' + emulated + ' emulation mode.\nIE emulation modes can behave significantly differently from ACTUAL older versions of IE.\nPLEASE DON\'T FILE BOOTSTRAP BUGS based on testing in IE emulation modes!')
+  }
+})();
diff --git a/src/main/resources/static/js/plugin-manager.js b/src/main/resources/static/js/plugin-manager.js
index 5862fe4..0fb97a2 100644
--- a/src/main/resources/static/js/plugin-manager.js
+++ b/src/main/resources/static/js/plugin-manager.js
@@ -14,7 +14,7 @@
 
 var app = angular.module('PluginManager', []).controller(
     'LoadInstalledPlugins',
-    function($scope, $http) {
+    function($scope, $http, $location, $window) {
       var plugins = this;
 
       plugins.list = [];
@@ -47,6 +47,7 @@
                   if (currPluginIdx < 0) {
                     plugins.list.push({
                       id : plugin.id,
+                      description : plugin.description,
                       index_url : plugin.index_url,
                       version : plugin.version,
                       sha1 : '',
@@ -56,6 +57,7 @@
                   } else {
                     plugins.list[currPluginIdx] = {
                       id : plugin.id,
+                      description : plugin.description,
                       index_url : plugin.index_url,
                       version : plugin.version,
                       sha1 : '',
@@ -96,6 +98,7 @@
                       }
                       currPlugin.sha1 = plugin.sha1;
                       currPlugin.url = plugin.url;
+                      currPlugin.description = plugin.description;
 
                       if (currRow < 0) {
                         plugins.list.push(currPlugin);
@@ -126,6 +129,16 @@
             });
       }
 
+      plugins.goToGerrit = function () {
+        var currUrl = $location.absUrl();
+        var indexOfHash = currUrl.indexOf("#")
+        if(indexOfHash > 0) {
+          currUrl = currUrl.substring(0,indexOfHash)
+        }
+        var newUrl = currUrl + "/../../../.."
+        $window.location.href = newUrl
+      };
+
       $scope.refreshInstalled();
     });
 
@@ -133,4 +146,4 @@
   $httpProvider.defaults.headers.common = {
     'X-Gerrit-Auth' : '@X-Gerrit-Auth'
   };
-});
\ No newline at end of file
+});