]> source.dussan.org Git - gitblit.git/commitdiff
Implement simple JSON-based plugin registry and install command
authorJames Moger <james.moger@gitblit.com>
Tue, 1 Apr 2014 04:10:43 +0000 (00:10 -0400)
committerJames Moger <james.moger@gitblit.com>
Thu, 10 Apr 2014 23:00:52 +0000 (19:00 -0400)
releases.moxie
src/main/distrib/data/gitblit.properties
src/main/java/com/gitblit/manager/GitblitManager.java
src/main/java/com/gitblit/manager/IPluginManager.java
src/main/java/com/gitblit/manager/PluginManager.java
src/main/java/com/gitblit/models/PluginRegistry.java [new file with mode: 0644]
src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java

index 0f37bf2b03c172505128fcf246764aaf15bcb27c..89a7a5f329740d281185aa121c70b9c6721d06b2 100644 (file)
@@ -48,6 +48,7 @@ r22: {
     - { name: 'git.sshBackend', defaultValue: 'NIO2' }
     - { name: 'git.sshCommandStartThreads', defaultValue: '2' }
     - { name: 'plugins.folder', defaultValue: '${baseFolder}/plugins' }
+    - { name: 'plugins.registry', defaultValue: 'http://gitblit.github.io/gitblit-registry/plugins.json' }
 }
 
 #
index 1a613e25da760fd6c4a15fcc390b4e10a0efc677..c52423b9d929e48536634a52199117d1674ff528 100644 (file)
@@ -548,6 +548,18 @@ tickets.redis.url =
 # SINCE 1.4.0\r
 tickets.perPage = 25\r
 \r
+# The folder where plugins are loaded from.\r
+#\r
+# SINCE 1.5.0\r
+# RESTART REQUIRED\r
+# BASEFOLDER\r
+plugins.folder = ${baseFolder}/plugins\r
+\r
+# The registry of available plugins.\r
+#\r
+# SINCE 1.5.0\r
+plugins.registry = http://gitblit.github.io/gitblit-registry/plugins.json\r
+\r
 #\r
 # Groovy Integration\r
 #\r
@@ -1850,11 +1862,3 @@ server.requireClientCertificates = false
 # SINCE 0.5.0\r
 # RESTART REQUIRED\r
 server.shutdownPort = 8081\r
-\r
-# Base folder for plugins.\r
-# This folder may contain Gitblit plugins\r
-#\r
-# SINCE 1.6.0\r
-# RESTART REQUIRED\r
-# BASEFOLDER\r
-plugins.folder = ${baseFolder}/plugins\r
index 6b1cc8a547290a60d1b75fa088abaf920fb6be65..5a7d15ae484907062f67edda114590e1bd6d9648 100644 (file)
@@ -61,6 +61,8 @@ import com.gitblit.models.ForkModel;
 import com.gitblit.models.GitClientApplication;
 import com.gitblit.models.Mailing;
 import com.gitblit.models.Metric;
+import com.gitblit.models.PluginRegistry.PluginRegistration;
+import com.gitblit.models.PluginRegistry.PluginRelease;
 import com.gitblit.models.ProjectModel;
 import com.gitblit.models.RegistrantAccessPermission;
 import com.gitblit.models.RepositoryModel;
@@ -1180,6 +1182,10 @@ public class GitblitManager implements IGitblit {
                return repositoryManager.isIdle(repository);
        }
 
+       /*
+        * PLUGIN MANAGER
+        */
+
        @Override
        public <T> List<T> getExtensions(Class<T> clazz) {
                return pluginManager.getExtensions(clazz);
@@ -1195,6 +1201,36 @@ public class GitblitManager implements IGitblit {
                return pluginManager.deletePlugin(wrapper);
        }
 
+       @Override
+       public boolean refreshRegistry() {
+               return pluginManager.refreshRegistry();
+       }
+
+       @Override
+       public boolean installPlugin(String url) {
+               return pluginManager.installPlugin(url);
+       }
+
+       @Override
+       public boolean installPlugin(PluginRelease pv) {
+               return pluginManager.installPlugin(pv);
+       }
+
+       @Override
+       public List<PluginRegistration> getRegisteredPlugins() {
+               return pluginManager.getRegisteredPlugins();
+       }
+
+       @Override
+       public PluginRegistration lookupPlugin(String idOrName) {
+               return pluginManager.lookupPlugin(idOrName);
+       }
+
+       @Override
+       public PluginRelease lookupRelease(String idOrName, String version) {
+               return pluginManager.lookupRelease(idOrName, version);
+       }
+
        @Override
        public List<PluginWrapper> getPlugins() {
                return pluginManager.getPlugins();
index 11b81ea3b2c6d1054d194ef72f10a2b9db1bfe12..1f7f85eec209f009ac9a5a14d22fce4af805a1e9 100644 (file)
  */
 package com.gitblit.manager;
 
+import java.util.List;
+
 import ro.fortsoft.pf4j.PluginManager;
 import ro.fortsoft.pf4j.PluginWrapper;
 
+import com.gitblit.models.PluginRegistry.PluginRegistration;
+import com.gitblit.models.PluginRegistry.PluginRelease;
+
 public interface IPluginManager extends IManager, PluginManager {
 
        /**
@@ -27,12 +32,51 @@ public interface IPluginManager extends IManager, PluginManager {
      * @return PluginWrapper that loaded the given class
      */
     PluginWrapper whichPlugin(Class<?> clazz);
-    
+
     /**
      * Delete the plugin represented by {@link PluginWrapper}.
-     * 
+     *
      * @param wrapper
      * @return true if successful
      */
     boolean deletePlugin(PluginWrapper wrapper);
+
+    /**
+     * Refresh the plugin registry.
+     */
+    boolean refreshRegistry();
+
+    /**
+     * Install the plugin from the specified url.
+     */
+    boolean installPlugin(String url);
+
+    /**
+     * Install the plugin.
+     */
+    boolean installPlugin(PluginRelease pr);
+
+    /**
+     * The list of all registered plugins.
+     *
+     * @return a list of registered plugins
+     */
+    List<PluginRegistration> getRegisteredPlugins();
+
+    /**
+     * Lookup a plugin registration from the plugin registries.
+     *
+     * @param idOrName
+     * @return a plugin registration or null
+     */
+    PluginRegistration lookupPlugin(String idOrName);
+
+    /**
+     * Lookup a plugin release.
+     *
+     * @param idOrName
+     * @param version (use null for the current version)
+     * @return the identified plugin version or null
+     */
+    PluginRelease lookupRelease(String idOrName, String version);
 }
index e23aaec00ac4257527a87c7096be379184f69ff7..7b03f50dd30c57e1d9eccc7a7067bee92a22f9a3 100644 (file)
  */
 package com.gitblit.manager;
 
+import java.io.BufferedInputStream;
 import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import ro.fortsoft.pf4j.DefaultPluginManager;
+import ro.fortsoft.pf4j.PluginVersion;
 import ro.fortsoft.pf4j.PluginWrapper;
 
 import com.gitblit.Keys;
+import com.gitblit.models.PluginRegistry;
+import com.gitblit.models.PluginRegistry.PluginRegistration;
+import com.gitblit.models.PluginRegistry.PluginRelease;
+import com.gitblit.utils.Base64;
 import com.gitblit.utils.FileUtils;
+import com.gitblit.utils.JsonUtils;
+import com.gitblit.utils.StringUtils;
+import com.google.common.io.Files;
+import com.google.common.io.InputSupplier;
 
 /**
  * The plugin manager maintains the lifecycle of plugins. It is exposed as
  * Dagger bean. The extension consumers supposed to retrieve plugin  manager
  * from the Dagger DI and retrieve extensions provided by active plugins.
- * 
+ *
  * @author David Ostrovsky
- * 
+ *
  */
 public class PluginManager extends DefaultPluginManager implements IPluginManager {
 
        private final Logger logger = LoggerFactory.getLogger(getClass());
-       
+
        private final IRuntimeManager runtimeManager;
 
+       // timeout defaults of Maven 3.0.4 in seconds
+       private int connectTimeout = 20;
+
+       private int readTimeout = 12800;
+
        public PluginManager(IRuntimeManager runtimeManager) {
                super(runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"));
                this.runtimeManager = runtimeManager;
@@ -60,13 +86,13 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage
                stopPlugins();
                return null;
        }
-       
+
        @Override
        public boolean deletePlugin(PluginWrapper pw) {
                File folder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
                File pluginFolder = new File(folder, pw.getPluginPath());
                File pluginZip = new File(folder, pw.getPluginPath() + ".zip");
-               
+
                if (pluginFolder.exists()) {
                        FileUtils.delete(pluginFolder);
                }
@@ -75,4 +101,218 @@ public class PluginManager extends DefaultPluginManager implements IPluginManage
                }
                return true;
        }
+
+       @Override
+       public boolean refreshRegistry() {
+               String dr = "http://gitblit.github.io/gitblit-registry/plugins.json";
+               String url = runtimeManager.getSettings().getString(Keys.plugins.registry, dr);
+               try {
+                       return download(url);
+               } catch (Exception e) {
+                       logger.error(String.format("Failed to retrieve plugins.json from %s", url), e);
+               }
+               return false;
+       }
+
+       protected List<PluginRegistry> getRegistries() {
+               List<PluginRegistry> list = new ArrayList<PluginRegistry>();
+               File folder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
+               FileFilter jsonFilter = new FileFilter() {
+                       @Override
+                       public boolean accept(File file) {
+                               return !file.isDirectory() && file.getName().toLowerCase().endsWith(".json");
+                       }
+               };
+
+               File [] files = folder.listFiles(jsonFilter);
+               if (files == null || files.length == 0) {
+                       // automatically retrieve the registry if we don't have a local copy
+                       refreshRegistry();
+                       files = folder.listFiles(jsonFilter);
+               }
+
+               if (files == null || files.length == 0) {
+                       return list;
+               }
+
+               for (File file : files) {
+                       PluginRegistry registry = null;
+                       try {
+                               String json = FileUtils.readContent(file, "\n");
+                               registry = JsonUtils.fromJsonString(json, PluginRegistry.class);
+                       } catch (Exception e) {
+                               logger.error("Failed to deserialize " + file, e);
+                       }
+                       if (registry != null) {
+                               list.add(registry);
+                       }
+               }
+               return list;
+       }
+
+       @Override
+       public List<PluginRegistration> getRegisteredPlugins() {
+               List<PluginRegistration> list = new ArrayList<PluginRegistration>();
+               Map<String, PluginRegistration> map = new TreeMap<String, PluginRegistration>();
+               for (PluginRegistry registry : getRegistries()) {
+                       List<PluginRegistration> registrations = registry.registrations;
+                       list.addAll(registrations);
+                       for (PluginRegistration reg : registrations) {
+                               reg.installedRelease = null;
+                               map.put(reg.id, reg);
+                       }
+               }
+               for (PluginWrapper pw : getPlugins()) {
+                       String id = pw.getDescriptor().getPluginId();
+                       PluginVersion pv = pw.getDescriptor().getVersion();
+                       PluginRegistration reg = map.get(id);
+                       if (reg != null) {
+                               reg.installedRelease = pv.toString();
+                       }
+               }
+               return list;
+       }
+
+       @Override
+       public PluginRegistration lookupPlugin(String idOrName) {
+               for (PluginRegistry registry : getRegistries()) {
+                       PluginRegistration reg = registry.lookup(idOrName);
+                       if (reg != null) {
+                               return reg;
+                       }
+               }
+               return null;
+       }
+
+       @Override
+       public PluginRelease lookupRelease(String idOrName, String version) {
+               for (PluginRegistry registry : getRegistries()) {
+                       PluginRegistration reg = registry.lookup(idOrName);
+                       if (reg != null) {
+                               PluginRelease pv;
+                               if (StringUtils.isEmpty(version)) {
+                                       pv = reg.getCurrentRelease();
+                               } else {
+                                       pv = reg.getRelease(version);
+                               }
+                               if (pv != null) {
+                                       return pv;
+                               }
+                       }
+               }
+               return null;
+       }
+
+
+       /**
+        * Installs the plugin from the plugin version.
+        *
+        * @param pv
+        * @throws IOException
+        * @return true if successful
+        */
+       @Override
+       public boolean installPlugin(PluginRelease pv) {
+               return installPlugin(pv.url);
+       }
+
+       /**
+        * Installs the plugin from the url.
+        *
+        * @param url
+        * @return true if successful
+        */
+       @Override
+       public boolean installPlugin(String url) {
+               try {
+                       if (!download(url)) {
+                               return false;
+                       }
+                       // TODO stop, unload, load
+               } catch (IOException e) {
+                       logger.error("Failed to install plugin from " + url, e);
+               }
+               return true;
+       }
+
+       /**
+        * Download a file to the plugins folder.
+        *
+        * @param url
+        * @return
+        * @throws IOException
+        */
+       protected boolean download(String url) throws IOException {
+               File pFolder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
+               File tmpFile = new File(pFolder, StringUtils.getSHA1(url) + ".tmp");
+               if (tmpFile.exists()) {
+                       tmpFile.delete();
+               }
+
+               URL u = new URL(url);
+               final URLConnection conn = getConnection(u);
+
+               // try to get the server-specified last-modified date of this artifact
+               long lastModified = conn.getHeaderFieldDate("Last-Modified", System.currentTimeMillis());
+
+               Files.copy(new InputSupplier<InputStream>() {
+                        @Override
+                       public InputStream getInput() throws IOException {
+                                return new BufferedInputStream(conn.getInputStream());
+                       }
+               }, tmpFile);
+
+               File destFile = new File(pFolder, StringUtils.getLastPathElement(u.getPath()));
+               if (destFile.exists()) {
+                       destFile.delete();
+               }
+               tmpFile.renameTo(destFile);
+               destFile.setLastModified(lastModified);
+
+               return true;
+       }
+
+       protected URLConnection getConnection(URL url) throws IOException {
+               java.net.Proxy proxy = getProxy(url);
+               HttpURLConnection conn = (HttpURLConnection) url.openConnection(proxy);
+               if (java.net.Proxy.Type.DIRECT != proxy.type()) {
+                       String auth = getProxyAuthorization(url);
+                       conn.setRequestProperty("Proxy-Authorization", auth);
+               }
+
+               String username = null;
+               String password = null;
+               if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
+                       // set basic authentication header
+                       String auth = Base64.encodeBytes((username + ":" + password).getBytes());
+                       conn.setRequestProperty("Authorization", "Basic " + auth);
+               }
+
+               // configure timeouts
+               conn.setConnectTimeout(connectTimeout * 1000);
+               conn.setReadTimeout(readTimeout * 1000);
+
+               switch (conn.getResponseCode()) {
+               case HttpURLConnection.HTTP_MOVED_TEMP:
+               case HttpURLConnection.HTTP_MOVED_PERM:
+                       // handle redirects by closing this connection and opening a new
+                       // one to the new location of the requested resource
+                       String newLocation = conn.getHeaderField("Location");
+                       if (!StringUtils.isEmpty(newLocation)) {
+                               logger.info("following redirect to {0}", newLocation);
+                               conn.disconnect();
+                               return getConnection(new URL(newLocation));
+                       }
+               }
+
+               return conn;
+       }
+
+       protected Proxy getProxy(URL url) {
+               return java.net.Proxy.NO_PROXY;
+       }
+
+       protected String getProxyAuthorization(URL url) {
+               return "";
+       }
 }
diff --git a/src/main/java/com/gitblit/models/PluginRegistry.java b/src/main/java/com/gitblit/models/PluginRegistry.java
new file mode 100644 (file)
index 0000000..c81a0f2
--- /dev/null
@@ -0,0 +1,143 @@
+/*\r
+ * Copyright 2014 gitblit.com.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *     http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package com.gitblit.models;\r
+\r
+import java.io.Serializable;\r
+import java.util.ArrayList;\r
+import java.util.Date;\r
+import java.util.List;\r
+\r
+import org.parboiled.common.StringUtils;\r
+\r
+import ro.fortsoft.pf4j.PluginVersion;\r
+\r
+/**\r
+ * Represents a list of plugin registrations.\r
+ */\r
+public class PluginRegistry implements Serializable {\r
+\r
+       private static final long serialVersionUID = 1L;\r
+\r
+       public final String name;\r
+\r
+       public final List<PluginRegistration> registrations;\r
+\r
+       public PluginRegistry(String name) {\r
+               this.name = name;\r
+               registrations = new ArrayList<PluginRegistration>();\r
+       }\r
+\r
+       public PluginRegistration lookup(String idOrName) {\r
+               for (PluginRegistration registration : registrations) {\r
+                       if (registration.id.equalsIgnoreCase(idOrName)\r
+                                       || registration.name.equalsIgnoreCase(idOrName)) {\r
+                               return registration;\r
+                       }\r
+               }\r
+               return null;\r
+       }\r
+\r
+       @Override\r
+       public String toString() {\r
+               return getClass().getSimpleName();\r
+       }\r
+\r
+       public static enum InstallState {\r
+               NOT_INSTALLED, INSTALLED, CAN_UPDATE, UNKNOWN\r
+       }\r
+\r
+       /**\r
+        * Represents a plugin registration.\r
+        */\r
+       public static class PluginRegistration implements Serializable {\r
+\r
+               private static final long serialVersionUID = 1L;\r
+\r
+               public final String id;\r
+\r
+               public String name;\r
+\r
+               public String description;\r
+\r
+               public String provider;\r
+\r
+               public String projectUrl;\r
+\r
+               public String currentRelease;\r
+\r
+               public transient String installedRelease;\r
+\r
+               public List<PluginRelease> releases;\r
+\r
+               public PluginRegistration(String id) {\r
+                       this.id = id;\r
+                       this.releases = new ArrayList<PluginRelease>();\r
+               }\r
+\r
+               public PluginRelease getCurrentRelease() {\r
+                       PluginRelease current = null;\r
+                       if (!StringUtils.isEmpty(currentRelease)) {\r
+                               current = getRelease(currentRelease);\r
+                       }\r
+\r
+                       if (current == null) {\r
+                               Date date = new Date(0);\r
+                               for (PluginRelease pv : releases) {\r
+                                       if (pv.date.after(date)) {\r
+                                               current = pv;\r
+                                       }\r
+                               }\r
+                       }\r
+                       return current;\r
+               }\r
+\r
+               public PluginRelease getRelease(String version) {\r
+                       for (PluginRelease pv : releases) {\r
+                               if (pv.version.equalsIgnoreCase(version)) {\r
+                                       return pv;\r
+                               }\r
+                       }\r
+                       return null;\r
+               }\r
+\r
+               public InstallState getInstallState() {\r
+                       if (StringUtils.isEmpty(installedRelease)) {\r
+                               return InstallState.NOT_INSTALLED;\r
+                       }\r
+                       PluginVersion ir = PluginVersion.createVersion(installedRelease);\r
+                       PluginVersion cr = PluginVersion.createVersion(currentRelease);\r
+                       switch (ir.compareTo(cr)) {\r
+                       case -1:\r
+                               return InstallState.UNKNOWN;\r
+                       case 1:\r
+                               return InstallState.CAN_UPDATE;\r
+                       default:\r
+                               return InstallState.INSTALLED;\r
+                       }\r
+               }\r
+\r
+               @Override\r
+               public String toString() {\r
+                       return id;\r
+               }\r
+       }\r
+\r
+       public static class PluginRelease {\r
+               public String version;\r
+               public Date date;\r
+               public String url;\r
+       }\r
+}\r
index 5c413db2a85414152b7d2de2adeefa5c6f79d496..ba6f30d69e1044993215cdfa6c1399fdf115a9b1 100644 (file)
@@ -19,6 +19,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
 
 import ro.fortsoft.pf4j.PluginDependency;
 import ro.fortsoft.pf4j.PluginDescriptor;
@@ -26,6 +27,8 @@ import ro.fortsoft.pf4j.PluginState;
 import ro.fortsoft.pf4j.PluginWrapper;
 
 import com.gitblit.manager.IGitblit;
+import com.gitblit.models.PluginRegistry.PluginRegistration;
+import com.gitblit.models.PluginRegistry.PluginRelease;
 import com.gitblit.models.UserModel;
 import com.gitblit.utils.FlipTable;
 import com.gitblit.utils.FlipTable.Borders;
@@ -46,7 +49,8 @@ public class PluginDispatcher extends DispatchCommand {
                register(user, StopPlugin.class);
                register(user, ShowPlugin.class);
                register(user, RemovePlugin.class);
-               register(user, UploadPlugin.class);
+               register(user, InstallPlugin.class);
+               register(user, AvailablePlugins.class);
        }
 
        @CommandMetaData(name = "list", aliases = { "ls" }, description = "List the loaded plugins")
@@ -82,7 +86,7 @@ public class PluginDispatcher extends DispatchCommand {
 
                        stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS));
                }
-               
+
                @Override
                protected void asTabbed(List<PluginWrapper> list) {
                        for (PluginWrapper pw : list) {
@@ -95,7 +99,7 @@ public class PluginDispatcher extends DispatchCommand {
                        }
                }
        }
-       
+
        @CommandMetaData(name = "start", description = "Start a plugin")
        public static class StartPlugin extends SshCommand {
 
@@ -128,7 +132,7 @@ public class PluginDispatcher extends DispatchCommand {
                                }
                        }
                }
-               
+
                protected void start(PluginWrapper pw) throws UnloggedFailure {
                        String id = pw.getDescriptor().getPluginId();
                        if (pw.getPluginState() == PluginState.STARTED) {
@@ -143,7 +147,7 @@ public class PluginDispatcher extends DispatchCommand {
                        }
                }
        }
-       
+
 
        @CommandMetaData(name = "stop", description = "Stop a plugin")
        public static class StopPlugin extends SshCommand {
@@ -177,7 +181,7 @@ public class PluginDispatcher extends DispatchCommand {
                        }
                        }
                }
-               
+
                protected void stop(PluginWrapper pw) throws UnloggedFailure {
                        String id = pw.getDescriptor().getPluginId();
                        if (pw.getPluginState() == PluginState.STOPPED) {
@@ -192,7 +196,7 @@ public class PluginDispatcher extends DispatchCommand {
                        }
                }
        }
-       
+
        @CommandMetaData(name = "show", description = "Show the details of a plugin")
        public static class ShowPlugin extends SshCommand {
 
@@ -230,7 +234,7 @@ public class PluginDispatcher extends DispatchCommand {
                                        String ext = exts.get(i);
                                        data[0] = new Object[] { ext.toString(), ext.toString() };
                                }
-                               extensions = FlipTable.of(headers, data, Borders.COLS);         
+                               extensions = FlipTable.of(headers, data, Borders.COLS);
                        }
 
                        // DEPENDENCIES
@@ -246,9 +250,9 @@ public class PluginDispatcher extends DispatchCommand {
                                        PluginDependency dep = deps.get(i);
                                        data[0] = new Object[] { dep.getPluginId(), dep.getPluginVersion() };
                                }
-                               dependencies = FlipTable.of(headers, data, Borders.COLS);               
+                               dependencies = FlipTable.of(headers, data, Borders.COLS);
                        }
-                       
+
                        String[] headers = { d.getPluginId() };
                        Object[][] data = new Object[5][];
                        data[0] = new Object[] { fields };
@@ -256,10 +260,10 @@ public class PluginDispatcher extends DispatchCommand {
                        data[2] = new Object[] { extensions };
                        data[3] = new Object[] { "DEPENDENCIES" };
                        data[4] = new Object[] { dependencies };
-                       stdout.println(FlipTable.of(headers, data));            
+                       stdout.println(FlipTable.of(headers, data));
                }
        }
-       
+
        @CommandMetaData(name = "remove", aliases= { "rm", "del" }, description = "Remove a plugin", hidden = true)
        public static class RemovePlugin extends SshCommand {
 
@@ -282,12 +286,98 @@ public class PluginDispatcher extends DispatchCommand {
                        }
                }
        }
-       
-       @CommandMetaData(name = "receive", aliases= { "upload" }, description = "Upload a plugin to the server", hidden = true)
-       public static class UploadPlugin extends SshCommand {
+
+       @CommandMetaData(name = "install", description = "Download and installs a plugin", hidden = true)
+       public static class InstallPlugin extends SshCommand {
+
+               @Argument(index = 0, required = true, metaVar = "<URL>|<ID>|<NAME>", usage = "the id, name, or the url of the plugin to download and install")
+               protected String urlOrIdOrName;
+
+               @Option(name = "--version", usage = "The specific version to install")
+               private String version;
 
                @Override
                public void run() throws UnloggedFailure {
+                       IGitblit gitblit = getContext().getGitblit();
+                       try {
+                               String ulc = urlOrIdOrName.toLowerCase();
+                               if (ulc.startsWith("http://") || ulc.startsWith("https://")) {
+                                       if (gitblit.installPlugin(urlOrIdOrName)) {
+                                               stdout.println(String.format("Installed %s", urlOrIdOrName));
+                                       } else {
+                                               new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName));
+                                       }
+                               } else {
+                                       PluginRelease pv = gitblit.lookupRelease(urlOrIdOrName, version);
+                                       if (pv == null) {
+                                               throw new UnloggedFailure(1,  String.format("Plugin \"%s\" is not in the registry!", urlOrIdOrName));
+                                       }
+                                       if (gitblit.installPlugin(pv)) {
+                                               stdout.println(String.format("Installed %s", urlOrIdOrName));
+                                       } else {
+                                               throw new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName));
+                                       }
+                               }
+                       } catch (Exception e) {
+                               log.error("Failed to install " + urlOrIdOrName, e);
+                               throw new UnloggedFailure(1, String.format("Failed to install %s", urlOrIdOrName), e);
+                       }
+               }
+       }
+
+       @CommandMetaData(name = "available", description = "List the available plugins")
+       public static class AvailablePlugins extends ListFilterCommand<PluginRegistration> {
+
+               @Option(name = "--refresh", aliases = { "-r" }, usage = "refresh the plugin registry")
+               protected boolean refresh;
+
+               @Override
+               protected List<PluginRegistration> getItems() throws UnloggedFailure {
+                       IGitblit gitblit = getContext().getGitblit();
+                       if (refresh) {
+                               gitblit.refreshRegistry();
+                       }
+                       List<PluginRegistration> list = gitblit.getRegisteredPlugins();
+                       return list;
+               }
+
+               @Override
+               protected boolean matches(String filter, PluginRegistration t) {
+                       return t.id.matches(filter) || t.name.matches(filter);
+               }
+
+               @Override
+               protected void asTable(List<PluginRegistration> list) {
+                       String[] headers;
+                       if (verbose) {
+                               String [] h = { "Name", "Description", "Installed", "Release", "State", "Id", "Provider" };
+                               headers = h;
+                       } else {
+                               String [] h = { "Name", "Description", "Installed", "Release", "State" };
+                               headers = h;
+                       }
+                       Object[][] data = new Object[list.size()][];
+                       for (int i = 0; i < list.size(); i++) {
+                               PluginRegistration p = list.get(i);
+                               if (verbose) {
+                                       data[i] = new Object[] {p.name, p.description, p.installedRelease, p.currentRelease, p.getInstallState(), p.id, p.provider};
+                               } else {
+                                       data[i] = new Object[] {p.name, p.description, p.installedRelease, p.currentRelease, p.getInstallState()};
+                               }
+                       }
+
+                       stdout.println(FlipTable.of(headers, data, Borders.BODY_HCOLS));
+               }
+
+               @Override
+               protected void asTabbed(List<PluginRegistration> list) {
+                       for (PluginRegistration p : list) {
+                               if (verbose) {
+                                       outTabbed(p.name, p.description, p.currentRelease, p.getInstallState(), p.id, p.provider);
+                               } else {
+                                       outTabbed(p.name, p.description, p.currentRelease, p.getInstallState());
+                               }
+                       }
                }
        }
 }