From b2fec20f1f1081607b54b3e7dd20b12d03cef113 Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gitblit.com>
Date: Thu, 10 Apr 2014 19:01:30 -0400
Subject: [PATCH] Improve plugin manager based on upstreamed contributions to pf4j

---
 src/main/java/com/gitblit/manager/IPluginManager.java                  |  103 ++++
 src/main/java/com/gitblit/models/PluginRegistry.java                   |   21 +
 .classpath                                                             |    2 
 src/main/java/com/gitblit/manager/PluginManager.java                   |  335 +++++++++++++---
 src/main/java/com/gitblit/manager/GitblitManager.java                  |  152 ++++----
 src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java   |    7 
 src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java |  480 +++++++++++++++++--------
 build.moxie                                                            |    2 
 src/main/java/com/gitblit/utils/StringUtils.java                       |    2 
 gitblit.iml                                                            |    6 
 10 files changed, 782 insertions(+), 328 deletions(-)

diff --git a/.classpath b/.classpath
index 252a7c9..bcf2fbd 100644
--- a/.classpath
+++ b/.classpath
@@ -76,7 +76,7 @@
 	<classpathentry kind="lib" path="ext/args4j-2.0.26.jar" sourcepath="ext/src/args4j-2.0.26.jar" />
 	<classpathentry kind="lib" path="ext/jedis-2.3.1.jar" sourcepath="ext/src/jedis-2.3.1.jar" />
 	<classpathentry kind="lib" path="ext/commons-pool2-2.0.jar" sourcepath="ext/src/commons-pool2-2.0.jar" />
-	<classpathentry kind="lib" path="ext/pf4j-0.6.jar" sourcepath="ext/src/pf4j-0.6.jar" />
+	<classpathentry kind="lib" path="ext/pf4j-0.7.0.jar" sourcepath="ext/src/pf4j-0.7.0.jar" />
 	<classpathentry kind="lib" path="ext/junit-4.11.jar" sourcepath="ext/src/junit-4.11.jar" />
 	<classpathentry kind="lib" path="ext/hamcrest-core-1.3.jar" sourcepath="ext/src/hamcrest-core-1.3.jar" />
 	<classpathentry kind="lib" path="ext/selenium-java-2.28.0.jar" sourcepath="ext/src/selenium-java-2.28.0.jar" />
diff --git a/build.moxie b/build.moxie
index eb2878a..bc4d7d5 100644
--- a/build.moxie
+++ b/build.moxie
@@ -174,7 +174,7 @@
 - compile 'args4j:args4j:2.0.26' :war :fedclient :authority
 - compile 'commons-codec:commons-codec:1.7' :war
 - compile 'redis.clients:jedis:2.3.1' :war
-- compile 'ro.fortsoft.pf4j:pf4j:0.6' :war
+- compile 'ro.fortsoft.pf4j:pf4j:0.7.0' :war
 - test 'junit'
 # Dependencies for Selenium web page testing
 - test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar
diff --git a/gitblit.iml b/gitblit.iml
index ed067f2..0d5fb69 100644
--- a/gitblit.iml
+++ b/gitblit.iml
@@ -791,13 +791,13 @@
       </library>
     </orderEntry>
     <orderEntry type="module-library">
-      <library name="pf4j-0.6.jar">
+      <library name="pf4j-0.7.0.jar">
         <CLASSES>
-          <root url="jar://$MODULE_DIR$/ext/pf4j-0.6.jar!/" />
+          <root url="jar://$MODULE_DIR$/ext/pf4j-0.7.0.jar!/" />
         </CLASSES>
         <JAVADOC />
         <SOURCES>
-          <root url="jar://$MODULE_DIR$/ext/src/pf4j-0.6.jar!/" />
+          <root url="jar://$MODULE_DIR$/ext/src/pf4j-0.7.0.jar!/" />
         </SOURCES>
       </library>
     </orderEntry>
diff --git a/src/main/java/com/gitblit/manager/GitblitManager.java b/src/main/java/com/gitblit/manager/GitblitManager.java
index e3b6cf7..191d7cf 100644
--- a/src/main/java/com/gitblit/manager/GitblitManager.java
+++ b/src/main/java/com/gitblit/manager/GitblitManager.java
@@ -42,9 +42,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import ro.fortsoft.pf4j.PluginClassLoader;
+import ro.fortsoft.pf4j.PluginState;
 import ro.fortsoft.pf4j.PluginWrapper;
-import ro.fortsoft.pf4j.RuntimeMode;
 
 import com.gitblit.Constants;
 import com.gitblit.Constants.AccessPermission;
@@ -61,6 +60,7 @@
 import com.gitblit.models.GitClientApplication;
 import com.gitblit.models.Mailing;
 import com.gitblit.models.Metric;
+import com.gitblit.models.PluginRegistry.InstallState;
 import com.gitblit.models.PluginRegistry.PluginRegistration;
 import com.gitblit.models.PluginRegistry.PluginRelease;
 import com.gitblit.models.ProjectModel;
@@ -1190,76 +1190,6 @@
 	 */
 
 	@Override
-	public <T> List<T> getExtensions(Class<T> clazz) {
-		return pluginManager.getExtensions(clazz);
-	}
-
-	@Override
-	public PluginWrapper whichPlugin(Class<?> clazz) {
-		return pluginManager.whichPlugin(clazz);
-	}
-
-	@Override
-	public boolean deletePlugin(PluginWrapper wrapper) {
-		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();
-	}
-
-	@Override
-	public List<PluginWrapper> getResolvedPlugins() {
-		return pluginManager.getResolvedPlugins();
-	}
-
-	@Override
-	public List<PluginWrapper> getUnresolvedPlugins() {
-		return pluginManager.getUnresolvedPlugins();
-	}
-
-	@Override
-	public List<PluginWrapper> getStartedPlugins() {
-		return pluginManager.getStartedPlugins();
-	}
-
-	@Override
-	public void loadPlugins() {
-		pluginManager.loadPlugins();
-	}
-
-	@Override
 	public void startPlugins() {
 		pluginManager.startPlugins();
 	}
@@ -1270,12 +1200,82 @@
 	}
 
 	@Override
-	public PluginClassLoader getPluginClassLoader(String pluginId) {
-		return pluginManager.getPluginClassLoader(pluginId);
+	public List<PluginWrapper> getPlugins() {
+		return pluginManager.getPlugins();
 	}
 
 	@Override
-	public RuntimeMode getRuntimeMode() {
-		return pluginManager.getRuntimeMode();
+	public PluginWrapper getPlugin(String pluginId) {
+		return pluginManager.getPlugin(pluginId);
+	}
+
+	@Override
+	public List<Class<?>> getExtensionClasses(String pluginId) {
+		return pluginManager.getExtensionClasses(pluginId);
+	}
+
+	@Override
+	public <T> List<T> getExtensions(Class<T> clazz) {
+		return pluginManager.getExtensions(clazz);
+	}
+
+	@Override
+	public PluginWrapper whichPlugin(Class<?> clazz) {
+		return pluginManager.whichPlugin(clazz);
+	}
+
+	@Override
+	public PluginState startPlugin(String pluginId) {
+		return pluginManager.startPlugin(pluginId);
+	}
+
+	@Override
+	public PluginState stopPlugin(String pluginId) {
+		return pluginManager.stopPlugin(pluginId);
+	}
+
+	@Override
+	public boolean disablePlugin(String pluginId) {
+		return pluginManager.disablePlugin(pluginId);
+	}
+
+	@Override
+	public boolean enablePlugin(String pluginId) {
+		return pluginManager.enablePlugin(pluginId);
+	}
+
+	@Override
+	public boolean deletePlugin(String pluginId) {
+		return pluginManager.deletePlugin(pluginId);
+	}
+
+	@Override
+	public boolean refreshRegistry() {
+		return pluginManager.refreshRegistry();
+	}
+
+	@Override
+	public boolean installPlugin(String url, boolean verifyChecksum) throws IOException {
+		return pluginManager.installPlugin(url, verifyChecksum);
+	}
+
+	@Override
+	public List<PluginRegistration> getRegisteredPlugins() {
+		return pluginManager.getRegisteredPlugins();
+	}
+
+	@Override
+	public List<PluginRegistration> getRegisteredPlugins(InstallState state) {
+		return pluginManager.getRegisteredPlugins(state);
+	}
+
+	@Override
+	public PluginRegistration lookupPlugin(String idOrName) {
+		return pluginManager.lookupPlugin(idOrName);
+	}
+
+	@Override
+	public PluginRelease lookupRelease(String idOrName, String version) {
+		return pluginManager.lookupRelease(idOrName, version);
 	}
 }
diff --git a/src/main/java/com/gitblit/manager/IPluginManager.java b/src/main/java/com/gitblit/manager/IPluginManager.java
index 1f7f85e..33763aa 100644
--- a/src/main/java/com/gitblit/manager/IPluginManager.java
+++ b/src/main/java/com/gitblit/manager/IPluginManager.java
@@ -15,15 +15,74 @@
  */
 package com.gitblit.manager;
 
+import java.io.IOException;
 import java.util.List;
 
-import ro.fortsoft.pf4j.PluginManager;
+import ro.fortsoft.pf4j.PluginState;
 import ro.fortsoft.pf4j.PluginWrapper;
 
+import com.gitblit.models.PluginRegistry.InstallState;
 import com.gitblit.models.PluginRegistry.PluginRegistration;
 import com.gitblit.models.PluginRegistry.PluginRelease;
 
-public interface IPluginManager extends IManager, PluginManager {
+public interface IPluginManager extends IManager {
+
+	/**
+	 * Starts all plugins.
+	 */
+	void startPlugins();
+
+	/**
+	 * Stops all plugins.
+	 */
+	void stopPlugins();
+
+	/**
+	 * Starts the specified plugin.
+	 *
+	 * @param pluginId
+	 * @return the state of the plugin
+	 */
+	PluginState startPlugin(String pluginId);
+
+	/**
+	 * Stops the specified plugin.
+	 *
+	 * @param pluginId
+	 * @return the state of the plugin
+	 */
+	PluginState stopPlugin(String pluginId);
+
+	/**
+	 * Returns the list of extensions the plugin provides.
+	 *
+	 * @param type
+	 * @return a list of extensions the plugin provides
+	 */
+	List<Class<?>> getExtensionClasses(String pluginId);
+
+	/**
+	 * Returns the list of extension instances for a given extension point.
+	 *
+	 * @param type
+	 * @return a list of extension instances
+	 */
+	<T> List<T> getExtensions(Class<T> type);
+
+	/**
+	 * Returns the list of all resolved plugins.
+	 *
+	 * @return a list of resolved plugins
+	 */
+	List<PluginWrapper> getPlugins();
+
+	/**
+	 * Retrieves the {@link PluginWrapper} for the specified plugin id.
+	 *
+	 * @param pluginId
+	 * @return the plugin wrapper
+	 */
+	PluginWrapper getPlugin(String pluginId);
 
 	/**
      * Retrieves the {@link PluginWrapper} that loaded the given class 'clazz'.
@@ -34,12 +93,28 @@
     PluginWrapper whichPlugin(Class<?> clazz);
 
     /**
-     * Delete the plugin represented by {@link PluginWrapper}.
+     * Disable the plugin represented by pluginId.
      *
-     * @param wrapper
+     * @param pluginId
      * @return true if successful
      */
-    boolean deletePlugin(PluginWrapper wrapper);
+    boolean disablePlugin(String pluginId);
+
+    /**
+     * Enable the plugin represented by pluginId.
+     *
+     * @param pluginId
+     * @return true if successful
+     */
+    boolean enablePlugin(String pluginId);
+
+    /**
+     * Delete the plugin represented by pluginId.
+     *
+     * @param pluginId
+     * @return true if successful
+     */
+    boolean deletePlugin(String pluginId);
 
     /**
      * Refresh the plugin registry.
@@ -48,13 +123,11 @@
 
     /**
      * Install the plugin from the specified url.
+     *
+     * @param url
+     * @param verifyChecksum
      */
-    boolean installPlugin(String url);
-
-    /**
-     * Install the plugin.
-     */
-    boolean installPlugin(PluginRelease pr);
+    boolean installPlugin(String url, boolean verifyChecksum) throws IOException;
 
     /**
      * The list of all registered plugins.
@@ -64,6 +137,14 @@
     List<PluginRegistration> getRegisteredPlugins();
 
     /**
+     * Return a list of registered plugins that match the install state.
+     *
+     * @param state
+     * @return the list of plugins that match the install state
+     */
+    List<PluginRegistration> getRegisteredPlugins(InstallState state);
+
+    /**
      * Lookup a plugin registration from the plugin registries.
      *
      * @param idOrName
diff --git a/src/main/java/com/gitblit/manager/PluginManager.java b/src/main/java/com/gitblit/manager/PluginManager.java
index 47154b8..9cefc88 100644
--- a/src/main/java/com/gitblit/manager/PluginManager.java
+++ b/src/main/java/com/gitblit/manager/PluginManager.java
@@ -18,13 +18,18 @@
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileFilter;
+import java.io.FileInputStream;
 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.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
@@ -33,11 +38,16 @@
 import org.slf4j.LoggerFactory;
 
 import ro.fortsoft.pf4j.DefaultPluginManager;
+import ro.fortsoft.pf4j.PluginClassLoader;
+import ro.fortsoft.pf4j.PluginState;
+import ro.fortsoft.pf4j.PluginStateEvent;
+import ro.fortsoft.pf4j.PluginStateListener;
 import ro.fortsoft.pf4j.PluginVersion;
 import ro.fortsoft.pf4j.PluginWrapper;
 
 import com.gitblit.Keys;
 import com.gitblit.models.PluginRegistry;
+import com.gitblit.models.PluginRegistry.InstallState;
 import com.gitblit.models.PluginRegistry.PluginRegistration;
 import com.gitblit.models.PluginRegistry.PluginRelease;
 import com.gitblit.utils.Base64;
@@ -49,15 +59,17 @@
 
 /**
  * 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.
+ * 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 {
+public class PluginManager implements IPluginManager, PluginStateListener {
 
 	private final Logger logger = LoggerFactory.getLogger(getClass());
+
+	private final DefaultPluginManager pf4j;
 
 	private final IRuntimeManager runtimeManager;
 
@@ -67,47 +79,168 @@
 	private int readTimeout = 12800;
 
 	public PluginManager(IRuntimeManager runtimeManager) {
-		super(runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"));
+		File dir = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
 		this.runtimeManager = runtimeManager;
+		this.pf4j = new DefaultPluginManager(dir);
+	}
+
+	@Override
+	public void pluginStateChanged(PluginStateEvent event) {
+		logger.debug(event.toString());
 	}
 
 	@Override
 	public PluginManager start() {
-		logger.info("Loading plugins...");
-		loadPlugins();
-		logger.info("Starting loaded plugins...");
-		startPlugins();
+		pf4j.loadPlugins();
+		logger.debug("Starting plugins");
+		pf4j.startPlugins();
 		return this;
 	}
 
 	@Override
 	public PluginManager stop() {
-		logger.info("Stopping loaded plugins...");
-		stopPlugins();
+		logger.debug("Stopping plugins");
+		pf4j.stopPlugins();
 		return null;
 	}
 
+	/**
+	 * Installs the plugin from the url.
+	 *
+	 * @param url
+	 * @param verifyChecksum
+	 * @return true if successful
+	 */
 	@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");
+	public synchronized boolean installPlugin(String url, boolean verifyChecksum) throws IOException {
+		File file = download(url, verifyChecksum);
+		if (file == null || !file.exists()) {
+			logger.error("Failed to download plugin {}", url);
+			return false;
+		}
 
-		if (pluginFolder.exists()) {
-			FileUtils.delete(pluginFolder);
+		String pluginId = pf4j.loadPlugin(file);
+		if (StringUtils.isEmpty(pluginId)) {
+			logger.error("Failed to load plugin {}", file);
+			return false;
 		}
-		if (pluginZip.exists()) {
-			FileUtils.delete(pluginZip);
-		}
-		return true;
+
+		PluginState state = pf4j.startPlugin(pluginId);
+		return PluginState.STARTED.equals(state);
 	}
 
 	@Override
-	public boolean refreshRegistry() {
+	public synchronized boolean disablePlugin(String pluginId) {
+		return pf4j.disablePlugin(pluginId);
+	}
+
+	@Override
+	public synchronized boolean enablePlugin(String pluginId) {
+		if (pf4j.enablePlugin(pluginId)) {
+			return PluginState.STARTED == pf4j.startPlugin(pluginId);
+		}
+		return false;
+	}
+
+	@Override
+	public synchronized boolean deletePlugin(String pluginId) {
+		PluginWrapper pluginWrapper = getPlugin(pluginId);
+		final String name = pluginWrapper.getPluginPath().substring(1);
+		if (pf4j.deletePlugin(pluginId)) {
+
+			// delete the checksums
+			File pFolder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
+			File [] checksums = pFolder.listFiles(new FileFilter() {
+				@Override
+				public boolean accept(File file) {
+					if (!file.isFile()) {
+						return false;
+					}
+
+					return file.getName().startsWith(name) &&
+							(file.getName().toLowerCase().endsWith(".sha1")
+									|| file.getName().toLowerCase().endsWith(".md5"));
+				}
+
+			});
+
+			if (checksums != null) {
+				for (File checksum : checksums) {
+					checksum.delete();
+				}
+			}
+			return true;
+		}
+		return false;
+	}
+
+	@Override
+	public synchronized PluginState startPlugin(String pluginId) {
+		return pf4j.startPlugin(pluginId);
+	}
+
+	@Override
+	public synchronized PluginState stopPlugin(String pluginId) {
+		return pf4j.stopPlugin(pluginId);
+	}
+
+	@Override
+	public synchronized void startPlugins() {
+		pf4j.startPlugins();
+	}
+
+	@Override
+	public synchronized void stopPlugins() {
+		pf4j.stopPlugins();
+	}
+
+	@Override
+	public synchronized List<PluginWrapper> getPlugins() {
+		return pf4j.getPlugins();
+	}
+
+	@Override
+	public synchronized PluginWrapper getPlugin(String pluginId) {
+		return pf4j.getPlugin(pluginId);
+	}
+
+	@Override
+	public synchronized List<Class<?>> getExtensionClasses(String pluginId) {
+		List<Class<?>> list = new ArrayList<Class<?>>();
+		PluginClassLoader loader = pf4j.getPluginClassLoader(pluginId);
+		for (String className : pf4j.getExtensionClassNames(pluginId)) {
+			try {
+				list.add(loader.loadClass(className));
+			} catch (ClassNotFoundException e) {
+				logger.error(String.format("Failed to find %s in %s", className, pluginId), e);
+			}
+		}
+		return list;
+	}
+
+	@Override
+	public synchronized <T> List<T> getExtensions(Class<T> type) {
+		return pf4j.getExtensions(type);
+	}
+
+	@Override
+	public synchronized PluginWrapper whichPlugin(Class<?> clazz) {
+		return pf4j.whichPlugin(clazz);
+	}
+
+	@Override
+	public synchronized 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);
+			File file = download(url, true);
+			if (file != null && file.exists()) {
+				URL selfUrl = new URL(url.substring(0, url.lastIndexOf('/')));
+				// replace ${self} with the registry url
+				String content = FileUtils.readContent(file, "\n");
+				content = content.replace("${self}", selfUrl.toString());
+				FileUtils.writeContent(file, content);
+			}
 		} catch (Exception e) {
 			logger.error(String.format("Failed to retrieve plugins.json from %s", url), e);
 		}
@@ -124,7 +257,7 @@
 			}
 		};
 
-		File [] files = folder.listFiles(jsonFilter);
+		File[] files = folder.listFiles(jsonFilter);
 		if (files == null || files.length == 0) {
 			// automatically retrieve the registry if we don't have a local copy
 			refreshRegistry();
@@ -140,6 +273,7 @@
 			try {
 				String json = FileUtils.readContent(file, "\n");
 				registry = JsonUtils.fromJsonString(json, PluginRegistry.class);
+				registry.setup();
 			} catch (Exception e) {
 				logger.error("Failed to deserialize " + file, e);
 			}
@@ -151,18 +285,17 @@
 	}
 
 	@Override
-	public List<PluginRegistration> getRegisteredPlugins() {
+	public synchronized 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) {
+			list.addAll(registry.registrations);
+			for (PluginRegistration reg : list) {
 				reg.installedRelease = null;
 				map.put(reg.id, reg);
 			}
 		}
-		for (PluginWrapper pw : getPlugins()) {
+		for (PluginWrapper pw : pf4j.getPlugins()) {
 			String id = pw.getDescriptor().getPluginId();
 			PluginVersion pv = pw.getDescriptor().getVersion();
 			PluginRegistration reg = map.get(id);
@@ -174,10 +307,21 @@
 	}
 
 	@Override
-	public PluginRegistration lookupPlugin(String idOrName) {
-		for (PluginRegistry registry : getRegistries()) {
-			PluginRegistration reg = registry.lookup(idOrName);
-			if (reg != null) {
+	public synchronized List<PluginRegistration> getRegisteredPlugins(InstallState state) {
+		List<PluginRegistration> list = getRegisteredPlugins();
+		Iterator<PluginRegistration> itr = list.iterator();
+		while (itr.hasNext()) {
+			if (state != itr.next().getInstallState()) {
+				itr.remove();
+			}
+		}
+		return list;
+	}
+
+	@Override
+	public synchronized PluginRegistration lookupPlugin(String idOrName) {
+		for (PluginRegistration reg : getRegisteredPlugins()) {
+			if (reg.id.equalsIgnoreCase(idOrName) || reg.name.equalsIgnoreCase(idOrName)) {
 				return reg;
 			}
 		}
@@ -185,64 +329,107 @@
 	}
 
 	@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;
-				}
-			}
+	public synchronized PluginRelease lookupRelease(String idOrName, String version) {
+		PluginRegistration reg = lookupPlugin(idOrName);
+		if (reg == null) {
+			return null;
 		}
-		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);
+		PluginRelease pv;
+		if (StringUtils.isEmpty(version)) {
+			pv = reg.getCurrentRelease();
+		} else {
+			pv = reg.getRelease(version);
+		}
+		return pv;
 	}
 
 	/**
-	 * Installs the plugin from the url.
+	 * Downloads a file with optional checksum verification.
 	 *
 	 * @param url
-	 * @return true if successful
+	 * @param verifyChecksum
+	 * @return
+	 * @throws IOException
 	 */
-	@Override
-	public boolean installPlugin(String url) {
+	protected File download(String url, boolean verifyChecksum) throws IOException {
+		File file = downloadFile(url);
+
+		File sha1File = null;
 		try {
-			if (!download(url)) {
-				return false;
-			}
-			// TODO stop, unload, load
+			sha1File = downloadFile(url + ".sha1");
 		} catch (IOException e) {
-			logger.error("Failed to install plugin from " + url, e);
 		}
-		return true;
+
+		File md5File = null;
+		try {
+			md5File = downloadFile(url + ".md5");
+		} catch (IOException e) {
+
+		}
+
+		if (sha1File == null && md5File == null && verifyChecksum) {
+			throw new IOException("Missing SHA1 and MD5 checksums for " + url);
+		}
+
+		String expected;
+		MessageDigest md = null;
+		if (sha1File != null && sha1File.exists()) {
+			// prefer SHA1 to MD5
+			expected = FileUtils.readContent(sha1File, "\n").split(" ")[0].trim();
+			try {
+				md = MessageDigest.getInstance("SHA-1");
+			} catch (NoSuchAlgorithmException e) {
+				logger.error(null, e);
+			}
+		} else {
+			expected = FileUtils.readContent(md5File, "\n").split(" ")[0].trim();
+			try {
+				md = MessageDigest.getInstance("MD5");
+			} catch (Exception e) {
+				logger.error(null, e);
+			}
+		}
+
+		// calculate the checksum
+		FileInputStream is = null;
+		try {
+			is = new FileInputStream(file);
+			DigestInputStream dis = new DigestInputStream(is, md);
+			byte [] buffer = new byte[1024];
+			while ((dis.read(buffer)) > -1) {
+				// read
+			}
+			dis.close();
+
+			byte [] digest = md.digest();
+			String calculated = StringUtils.toHex(digest).trim();
+
+			if (!expected.equals(calculated)) {
+				String msg = String.format("Invalid checksum for %s\nAlgorithm:  %s\nExpected:   %s\nCalculated: %s",
+						file.getAbsolutePath(),
+						md.getAlgorithm(),
+						expected,
+						calculated);
+				file.delete();
+				throw new IOException(msg);
+			}
+		} finally {
+			if (is != null) {
+				is.close();
+			}
+		}
+		return file;
 	}
 
 	/**
 	 * Download a file to the plugins folder.
 	 *
 	 * @param url
-	 * @return
+	 * @return the downloaded file
 	 * @throws IOException
 	 */
-	protected boolean download(String url) throws IOException {
+	protected File downloadFile(String url) throws IOException {
 		File pFolder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
 		pFolder.mkdirs();
 		File tmpFile = new File(pFolder, StringUtils.getSHA1(url) + ".tmp");
@@ -257,9 +444,9 @@
 		long lastModified = conn.getHeaderFieldDate("Last-Modified", System.currentTimeMillis());
 
 		Files.copy(new InputSupplier<InputStream>() {
-			 @Override
+			@Override
 			public InputStream getInput() throws IOException {
-				 return new BufferedInputStream(conn.getInputStream());
+				return new BufferedInputStream(conn.getInputStream());
 			}
 		}, tmpFile);
 
@@ -270,7 +457,7 @@
 		tmpFile.renameTo(destFile);
 		destFile.setLastModified(lastModified);
 
-		return true;
+		return destFile;
 	}
 
 	protected URLConnection getConnection(URL url) throws IOException {
diff --git a/src/main/java/com/gitblit/models/PluginRegistry.java b/src/main/java/com/gitblit/models/PluginRegistry.java
index c81a0f2..b5cf0ee 100644
--- a/src/main/java/com/gitblit/models/PluginRegistry.java
+++ b/src/main/java/com/gitblit/models/PluginRegistry.java
@@ -19,6 +19,7 @@
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.parboiled.common.StringUtils;
 
@@ -37,7 +38,13 @@
 
 	public PluginRegistry(String name) {
 		this.name = name;
-		registrations = new ArrayList<PluginRegistration>();
+		registrations = new CopyOnWriteArrayList<PluginRegistration>();
+	}
+
+	public void setup() {
+		for (PluginRegistration reg : registrations) {
+			reg.registry = name;
+		}
 	}
 
 	public PluginRegistration lookup(String idOrName) {
@@ -80,6 +87,8 @@
 
 		public transient String installedRelease;
 
+		public transient String registry;
+
 		public List<PluginRelease> releases;
 
 		public PluginRegistration(String id) {
@@ -90,10 +99,12 @@
 		public PluginRelease getCurrentRelease() {
 			PluginRelease current = null;
 			if (!StringUtils.isEmpty(currentRelease)) {
+				// find specified
 				current = getRelease(currentRelease);
 			}
 
 			if (current == null) {
+				// find by date
 				Date date = new Date(0);
 				for (PluginRelease pv : releases) {
 					if (pv.date.after(date)) {
@@ -135,9 +146,15 @@
 		}
 	}
 
-	public static class PluginRelease {
+	public static class PluginRelease implements Comparable<PluginRelease> {
 		public String version;
 		public Date date;
+		public String requires;
 		public String url;
+
+		@Override
+		public int compareTo(PluginRelease o) {
+			return PluginVersion.createVersion(version).compareTo(PluginVersion.createVersion(o.version));
+		}
 	}
 }
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java b/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java
index ba6f30d..99dd6d1 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java
@@ -15,23 +15,26 @@
  */
 package com.gitblit.transport.ssh.commands;
 
-import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
+import ro.fortsoft.pf4j.ExtensionPoint;
 import ro.fortsoft.pf4j.PluginDependency;
 import ro.fortsoft.pf4j.PluginDescriptor;
 import ro.fortsoft.pf4j.PluginState;
 import ro.fortsoft.pf4j.PluginWrapper;
 
 import com.gitblit.manager.IGitblit;
+import com.gitblit.models.PluginRegistry.InstallState;
 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;
+import com.google.common.base.Joiner;
 
 /**
  * The plugin dispatcher and commands for runtime plugin management.
@@ -47,13 +50,16 @@
 		register(user, ListPlugins.class);
 		register(user, StartPlugin.class);
 		register(user, StopPlugin.class);
+		register(user, EnablePlugin.class);
+		register(user, DisablePlugin.class);
 		register(user, ShowPlugin.class);
-		register(user, RemovePlugin.class);
-		register(user, InstallPlugin.class);
+		register(user, RefreshPlugins.class);
 		register(user, AvailablePlugins.class);
+		register(user, InstallPlugin.class);
+		register(user, UninstallPlugin.class);
 	}
 
-	@CommandMetaData(name = "list", aliases = { "ls" }, description = "List the loaded plugins")
+	@CommandMetaData(name = "list", aliases = { "ls" }, description = "List plugins")
 	public static class ListPlugins extends ListCommand<PluginWrapper> {
 
 		@Override
@@ -67,7 +73,7 @@
 		protected void asTable(List<PluginWrapper> list) {
 			String[] headers;
 			if (verbose) {
-				String [] h = { "#", "Id", "Version", "State", "Mode", "Path", "Provider"};
+				String [] h = { "#", "Id", "Version", "State", "Path", "Provider"};
 				headers = h;
 			} else {
 				String [] h = { "#", "Id", "Version", "State", "Path"};
@@ -78,7 +84,7 @@
 				PluginWrapper p = list.get(i);
 				PluginDescriptor d = p.getDescriptor();
 				if (verbose) {
-					data[i] = new Object[] { "" + (i + 1), d.getPluginId(), d.getVersion(), p.getPluginState(), p.getRuntimeMode(), p.getPluginPath(), d.getProvider() };
+					data[i] = new Object[] { "" + (i + 1), d.getPluginId(), d.getVersion(), p.getPluginState(), p.getPluginPath(), d.getProvider() };
 				} else {
 					data[i] = new Object[] { "" + (i + 1), d.getPluginId(), d.getVersion(), p.getPluginState(), p.getPluginPath() };
 				}
@@ -92,7 +98,7 @@
 			for (PluginWrapper pw : list) {
 				PluginDescriptor d = pw.getDescriptor();
 				if (verbose) {
-					outTabbed(d.getPluginId(), d.getVersion(), pw.getPluginState(), pw.getRuntimeMode(), pw.getPluginPath(), d.getProvider());
+					outTabbed(d.getPluginId(), d.getVersion(), pw.getPluginState(), pw.getPluginPath(), d.getProvider());
 				} else {
 					outTabbed(d.getPluginId(), d.getVersion(), pw.getPluginState(), pw.getPluginPath());
 				}
@@ -100,146 +106,265 @@
 		}
 	}
 
+	static abstract class PluginCommand extends SshCommand {
+
+		protected PluginWrapper getPlugin(String id) throws Failure {
+			IGitblit gitblit = getContext().getGitblit();
+			PluginWrapper pluginWrapper = null;
+			try {
+				int index = Integer.parseInt(id);
+				List<PluginWrapper> plugins = gitblit.getPlugins();
+				if (index > plugins.size()) {
+					throw new UnloggedFailure(1, "Invalid plugin index specified!");
+				}
+				pluginWrapper = plugins.get(index - 1);
+			} catch (NumberFormatException e) {
+				pluginWrapper = gitblit.getPlugin(id);
+				if (pluginWrapper == null) {
+					PluginRegistration reg = gitblit.lookupPlugin(id);
+					if (reg == null) {
+						throw new UnloggedFailure("Invalid plugin specified!");
+					}
+					pluginWrapper = gitblit.getPlugin(reg.id);
+				}
+			}
+
+			return pluginWrapper;
+		}
+	}
+
 	@CommandMetaData(name = "start", description = "Start a plugin")
-	public static class StartPlugin extends SshCommand {
+	public static class StartPlugin extends PluginCommand {
 
 		@Argument(index = 0, required = true, metaVar = "ALL|<id>", usage = "the plugin to start")
-		protected String plugin;
+		protected String id;
 
 		@Override
-		public void run() throws UnloggedFailure {
+		public void run() throws Failure {
 			IGitblit gitblit = getContext().getGitblit();
-			if (plugin.equalsIgnoreCase("ALL")) {
+			if (id.equalsIgnoreCase("ALL")) {
 				gitblit.startPlugins();
 				stdout.println("All plugins started");
 			} else {
-				try {
-					int index = Integer.parseInt(plugin);
-					List<PluginWrapper> plugins = gitblit.getPlugins();
-					if (index > plugins.size()) {
-						throw new UnloggedFailure(1,  "Invalid plugin index specified!");
-					}
-					PluginWrapper pw = plugins.get(index - 1);
-					start(pw);
-				} catch (NumberFormatException n) {
-					for (PluginWrapper pw : gitblit.getPlugins()) {
-						PluginDescriptor pd = pw.getDescriptor();
-						if (pd.getPluginId().equalsIgnoreCase(plugin)) {
-							start(pw);
-							break;
-						}
-					}
+				PluginWrapper pluginWrapper = getPlugin(id);
+				if (pluginWrapper == null) {
+					throw new UnloggedFailure(String.format("Plugin %s is not installed!", id));
 				}
-			}
-		}
 
-		protected void start(PluginWrapper pw) throws UnloggedFailure {
-			String id = pw.getDescriptor().getPluginId();
-			if (pw.getPluginState() == PluginState.STARTED) {
-				throw new UnloggedFailure(1, String.format("%s is already started.", id));
-			}
-			try {
-				pw.getPlugin().start();
-//            	pw.setPluginState(PluginState.STARTED);
-				stdout.println(String.format("%s started", id));
-			} catch (Exception pe) {
-				throw new UnloggedFailure(1, String.format("Failed to start %s", id), pe);
+				PluginState state = gitblit.startPlugin(pluginWrapper.getPluginId());
+				if (PluginState.STARTED.equals(state)) {
+					stdout.println(String.format("Started %s", pluginWrapper.getPluginId()));
+				} else {
+					throw new Failure(1, String.format("Failed to start %s", pluginWrapper.getPluginId()));
+				}
 			}
 		}
 	}
 
-
 	@CommandMetaData(name = "stop", description = "Stop a plugin")
-	public static class StopPlugin extends SshCommand {
+	public static class StopPlugin extends PluginCommand {
 
 		@Argument(index = 0, required = true, metaVar = "ALL|<id>", usage = "the plugin to stop")
-		protected String plugin;
+		protected String id;
 
 		@Override
-		public void run() throws UnloggedFailure {
+		public void run() throws Failure {
 			IGitblit gitblit = getContext().getGitblit();
-			if (plugin.equalsIgnoreCase("ALL")) {
+			if (id.equalsIgnoreCase("ALL")) {
 				gitblit.stopPlugins();
 				stdout.println("All plugins stopped");
 			} else {
-				try {
-				int index = Integer.parseInt(plugin);
-				List<PluginWrapper> plugins = gitblit.getPlugins();
-				if (index > plugins.size()) {
-					throw new UnloggedFailure(1,  "Invalid plugin index specified!");
+				PluginWrapper pluginWrapper = getPlugin(id);
+				if (pluginWrapper == null) {
+					throw new UnloggedFailure(String.format("Plugin %s is not installed!", id));
 				}
-				PluginWrapper pw = plugins.get(index - 1);
-				stop(pw);
-			} catch (NumberFormatException n) {
-				for (PluginWrapper pw : gitblit.getPlugins()) {
-					PluginDescriptor pd = pw.getDescriptor();
-					if (pd.getPluginId().equalsIgnoreCase(plugin)) {
-						stop(pw);
-						break;
-					}
+
+				PluginState state = gitblit.stopPlugin(pluginWrapper.getPluginId());
+				if (PluginState.STOPPED.equals(state)) {
+					stdout.println(String.format("Stopped %s", pluginWrapper.getPluginId()));
+				} else {
+					throw new Failure(1, String.format("Failed to stop %s", pluginWrapper.getPluginId()));
 				}
-			}
 			}
 		}
+	}
 
-		protected void stop(PluginWrapper pw) throws UnloggedFailure {
-			String id = pw.getDescriptor().getPluginId();
-			if (pw.getPluginState() == PluginState.STOPPED) {
-				throw new UnloggedFailure(1, String.format("%s is already stopped.", id));
+	@CommandMetaData(name = "enable", description = "Enable a plugin")
+	public static class EnablePlugin extends PluginCommand {
+
+		@Argument(index = 0, required = true, metaVar = "<id>", usage = "the plugin id to enable")
+		protected String id;
+
+		@Override
+		public void run() throws Failure {
+			IGitblit gitblit = getContext().getGitblit();
+			PluginWrapper pluginWrapper = getPlugin(id);
+			if (pluginWrapper == null) {
+				throw new UnloggedFailure("Invalid plugin specified!");
 			}
-			try {
-				pw.getPlugin().stop();
-//            	pw.setPluginState(PluginState.STOPPED);
-				stdout.println(String.format("%s stopped", id));
-			} catch (Exception pe) {
-				throw new UnloggedFailure(1, String.format("Failed to stop %s", id), pe);
+
+			if (gitblit.enablePlugin(pluginWrapper.getPluginId())) {
+				stdout.println(String.format("Enabled %s", pluginWrapper.getPluginId()));
+			} else {
+				throw new Failure(1, String.format("Failed to enable %s", pluginWrapper.getPluginId()));
+			}
+		}
+	}
+
+	@CommandMetaData(name = "disable", description = "Disable a plugin")
+	public static class DisablePlugin extends PluginCommand {
+
+		@Argument(index = 0, required = true, metaVar = "<id>", usage = "the plugin to disable")
+		protected String id;
+
+		@Override
+		public void run() throws Failure {
+			IGitblit gitblit = getContext().getGitblit();
+			PluginWrapper pluginWrapper = getPlugin(id);
+			if (pluginWrapper == null) {
+				throw new UnloggedFailure("Invalid plugin specified!");
+			}
+
+			if (gitblit.disablePlugin(pluginWrapper.getPluginId())) {
+				stdout.println(String.format("Disabled %s", pluginWrapper.getPluginId()));
+			} else {
+				throw new Failure(1, String.format("Failed to disable %s", pluginWrapper.getPluginId()));
 			}
 		}
 	}
 
 	@CommandMetaData(name = "show", description = "Show the details of a plugin")
-	public static class ShowPlugin extends SshCommand {
+	public static class ShowPlugin extends PluginCommand {
 
-		@Argument(index = 0, required = true, metaVar = "<id>", usage = "the plugin to stop")
-		protected int index;
+		@Argument(index = 0, required = true, metaVar = "<id>", usage = "the plugin to show")
+		protected String id;
 
 		@Override
-		public void run() throws UnloggedFailure {
+		public void run() throws Failure {
 			IGitblit gitblit = getContext().getGitblit();
-			List<PluginWrapper> plugins = gitblit.getPlugins();
-			if (index > plugins.size()) {
-				throw new UnloggedFailure(1, "Invalid plugin index specified!");
+			PluginWrapper pw = getPlugin(id);
+			if (pw == null) {
+				PluginRegistration registration = gitblit.lookupPlugin(id);
+				if (registration == null) {
+					throw new Failure(1, String.format("Unknown plugin %s", id));
+				}
+				show(registration);
+			} else {
+				show(pw);
 			}
-			PluginWrapper pw = plugins.get(index - 1);
-			PluginDescriptor d = pw.getDescriptor();
+		}
+
+		protected String buildFieldTable(PluginWrapper pw, PluginRegistration reg) {
+			final String id = pw == null ? reg.id : pw.getPluginId();
+			final String name = reg == null ? "" : reg.name;
+			final String version = pw == null ? "" : pw.getDescriptor().getVersion().toString();
+			final String provider = pw == null ? reg.provider : pw.getDescriptor().getProvider();
+			final String registry = reg == null ? "" : reg.registry;
+			final String path = pw == null ? "" : pw.getPluginPath();
+			final String projectUrl = reg == null ? "" : reg.projectUrl;
+			final String state;
+			if (pw == null) {
+				// plugin could be installed
+				state = InstallState.NOT_INSTALLED.toString();
+			} else if (reg == null) {
+				// unregistered, installed plugin
+				state = Joiner.on(", ").join(InstallState.INSTALLED, pw.getPluginState());
+			} else {
+				// registered, installed plugin
+				state = Joiner.on(", ").join(reg.getInstallState(), pw.getPluginState());
+			}
+
+			StringBuilder sb = new StringBuilder();
+			sb.append("ID          : ").append(id).append('\n');
+			sb.append("Version     : ").append(version).append('\n');
+			sb.append("State       : ").append(state).append('\n');
+			sb.append("Path        : ").append(path).append('\n');
+			sb.append('\n');
+			sb.append("Name        : ").append(name).append('\n');
+			sb.append("Provider    : ").append(provider).append('\n');
+			sb.append("Project URL : ").append(projectUrl).append('\n');
+			sb.append("Registry    : ").append(registry).append('\n');
+
+			return sb.toString();
+		}
+
+		protected String buildReleaseTable(PluginRegistration reg) {
+			List<PluginRelease> releases = reg.releases;
+			Collections.sort(releases);
+			String releaseTable;
+			if (releases.isEmpty()) {
+				releaseTable = FlipTable.EMPTY;
+			} else {
+				String[] headers = { "Version", "Date", "Requires" };
+				Object[][] data = new Object[releases.size()][];
+				for (int i = 0; i < releases.size(); i++) {
+					PluginRelease release = releases.get(i);
+					data[i] = new Object[] { (release.version.equals(reg.installedRelease) ? ">" : " ") + release.version,
+							release.date, release.requires };
+				}
+				releaseTable = FlipTable.of(headers, data, Borders.COLS);
+			}
+			return releaseTable;
+		}
+
+		/**
+		 * Show an uninstalled plugin.
+		 *
+		 * @param reg
+		 */
+		protected void show(PluginRegistration reg) {
+			// REGISTRATION
+			final String fields = buildFieldTable(null, reg);
+			final String releases = buildReleaseTable(reg);
+
+			String[] headers = { reg.id };
+			Object[][] data = new Object[3][];
+			data[0] = new Object[] { fields };
+			data[1] = new Object[] { "RELEASES" };
+			data[2] = new Object[] { releases };
+			stdout.println(FlipTable.of(headers, data));
+		}
+
+		/**
+		 * Show an installed plugin.
+		 *
+		 * @param pw
+		 */
+		protected void show(PluginWrapper pw) {
+			IGitblit gitblit = getContext().getGitblit();
+			PluginRegistration reg = gitblit.lookupPlugin(pw.getPluginId());
 
 			// FIELDS
-			StringBuilder sb = new StringBuilder();
-			sb.append("Version  : ").append(d.getVersion()).append('\n');
-			sb.append("Provider : ").append(d.getProvider()).append('\n');
-			sb.append("Path     : ").append(pw.getPluginPath()).append('\n');
-			sb.append("State    : ").append(pw.getPluginState()).append('\n');
-			final String fields = sb.toString();
+			final String fields = buildFieldTable(pw, reg);
 
-			// TODO EXTENSIONS
-			sb.setLength(0);
-			List<String> exts = new ArrayList<String>();
+			// EXTENSIONS
+			StringBuilder sb = new StringBuilder();
+			List<Class<?>> exts = gitblit.getExtensionClasses(pw.getPluginId());
 			String extensions;
 			if (exts.isEmpty()) {
 				extensions = FlipTable.EMPTY;
 			} else {
-				String[] headers = { "Id", "Version" };
-				Object[][] data = new Object[exts.size()][];
+				StringBuilder description = new StringBuilder();
 				for (int i = 0; i < exts.size(); i++) {
-					String ext = exts.get(i);
-					data[0] = new Object[] { ext.toString(), ext.toString() };
+					Class<?> ext = exts.get(i);
+					if (ext.isAnnotationPresent(CommandMetaData.class)) {
+						CommandMetaData meta = ext.getAnnotation(CommandMetaData.class);
+						description.append(meta.name());
+						if (meta.description().length() > 0) {
+							description.append(": ").append(meta.description());
+						}
+						description.append('\n');
+					}
+					description.append(ext.getName()).append("\n  └ ");
+					description.append(getExtensionPoint(ext).getName());
+					description.append("\n\n");
 				}
-				extensions = FlipTable.of(headers, data, Borders.COLS);
+				extensions = description.toString();
 			}
 
 			// DEPENDENCIES
 			sb.setLength(0);
-			List<PluginDependency> deps = d.getDependencies();
+			List<PluginDependency> deps = pw.getDescriptor().getDependencies();
 			String dependencies;
 			if (deps.isEmpty()) {
 				dependencies = FlipTable.EMPTY;
@@ -248,80 +373,47 @@
 				Object[][] data = new Object[deps.size()][];
 				for (int i = 0; i < deps.size(); i++) {
 					PluginDependency dep = deps.get(i);
-					data[0] = new Object[] { dep.getPluginId(), dep.getPluginVersion() };
+					data[i] = new Object[] { dep.getPluginId(), dep.getPluginVersion() };
 				}
 				dependencies = FlipTable.of(headers, data, Borders.COLS);
 			}
 
-			String[] headers = { d.getPluginId() };
-			Object[][] data = new Object[5][];
+			// RELEASES
+			String releases;
+			if (reg == null) {
+				releases = FlipTable.EMPTY;
+			} else {
+				releases = buildReleaseTable(reg);
+			}
+
+			String[] headers = { pw.getPluginId() };
+			Object[][] data = new Object[7][];
 			data[0] = new Object[] { fields };
 			data[1] = new Object[] { "EXTENSIONS" };
 			data[2] = new Object[] { extensions };
 			data[3] = new Object[] { "DEPENDENCIES" };
 			data[4] = new Object[] { dependencies };
+			data[5] = new Object[] { "RELEASES" };
+			data[6] = new Object[] { releases };
 			stdout.println(FlipTable.of(headers, data));
 		}
-	}
 
-	@CommandMetaData(name = "remove", aliases= { "rm", "del" }, description = "Remove a plugin", hidden = true)
-	public static class RemovePlugin extends SshCommand {
-
-		@Argument(index = 0, required = true, metaVar = "<id>", usage = "the plugin to stop")
-		protected int index;
-
-		@Override
-		public void run() throws UnloggedFailure {
-			IGitblit gitblit = getContext().getGitblit();
-			List<PluginWrapper> plugins = gitblit.getPlugins();
-			if (index > plugins.size()) {
-				throw new UnloggedFailure(1, "Invalid plugin index specified!");
+		/* Find the ExtensionPoint */
+		protected Class<?> getExtensionPoint(Class<?> clazz) {
+			Class<?> superClass = clazz.getSuperclass();
+			if (ExtensionPoint.class.isAssignableFrom(superClass)) {
+				return superClass;
 			}
-			PluginWrapper pw = plugins.get(index - 1);
-			PluginDescriptor d = pw.getDescriptor();
-			if (gitblit.deletePlugin(pw)) {
-				stdout.println(String.format("Deleted %s %s", d.getPluginId(), d.getVersion()));
-			} else {
-				throw new UnloggedFailure(1,  String.format("Failed to delete %s %s", d.getPluginId(), d.getVersion()));
-			}
+			return getExtensionPoint(superClass);
 		}
 	}
 
-	@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;
-
+	@CommandMetaData(name = "refresh", description = "Refresh the plugin registry data")
+	public static class RefreshPlugins extends SshCommand {
 		@Override
-		public void run() throws UnloggedFailure {
+		public void run() throws Failure {
 			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);
-			}
+			gitblit.refreshRegistry();
 		}
 	}
 
@@ -331,13 +423,22 @@
 		@Option(name = "--refresh", aliases = { "-r" }, usage = "refresh the plugin registry")
 		protected boolean refresh;
 
+		@Option(name = "--updates", aliases = { "-u" }, usage = "show available updates")
+		protected boolean updates;
+
 		@Override
 		protected List<PluginRegistration> getItems() throws UnloggedFailure {
 			IGitblit gitblit = getContext().getGitblit();
 			if (refresh) {
 				gitblit.refreshRegistry();
 			}
-			List<PluginRegistration> list = gitblit.getRegisteredPlugins();
+
+			List<PluginRegistration> list;
+			if (updates) {
+				list = gitblit.getRegisteredPlugins(InstallState.CAN_UPDATE);
+			} else {
+				list = gitblit.getRegisteredPlugins();
+			}
 			return list;
 		}
 
@@ -350,19 +451,20 @@
 		protected void asTable(List<PluginRegistration> list) {
 			String[] headers;
 			if (verbose) {
-				String [] h = { "Name", "Description", "Installed", "Release", "State", "Id", "Provider" };
+				String [] h = { "Id", "Name", "Description", "Installed", "Current", "Requires", "State", "Registry" };
 				headers = h;
 			} else {
-				String [] h = { "Name", "Description", "Installed", "Release", "State" };
+				String [] h = { "Id", "Name", "Installed", "Current", "Requires", "State" };
 				headers = h;
 			}
 			Object[][] data = new Object[list.size()][];
 			for (int i = 0; i < list.size(); i++) {
 				PluginRegistration p = list.get(i);
+				PluginRelease curr = p.getCurrentRelease();
 				if (verbose) {
-					data[i] = new Object[] {p.name, p.description, p.installedRelease, p.currentRelease, p.getInstallState(), p.id, p.provider};
+					data[i] = new Object[] {p.id, p.name, p.description, p.installedRelease, curr.version, curr.requires, p.getInstallState(), p.registry};
 				} else {
-					data[i] = new Object[] {p.name, p.description, p.installedRelease, p.currentRelease, p.getInstallState()};
+					data[i] = new Object[] {p.id, p.name, p.installedRelease, curr.version, curr.requires, p.getInstallState()};
 				}
 			}
 
@@ -372,12 +474,76 @@
 		@Override
 		protected void asTabbed(List<PluginRegistration> list) {
 			for (PluginRegistration p : list) {
+				PluginRelease curr = p.getCurrentRelease();
 				if (verbose) {
-					outTabbed(p.name, p.description, p.currentRelease, p.getInstallState(), p.id, p.provider);
+					outTabbed(p.id, p.name, p.description, p.installedRelease, curr.version, curr.requires, p.getInstallState(), p.provider, p.registry);
 				} else {
-					outTabbed(p.name, p.description, p.currentRelease, p.getInstallState());
+					outTabbed(p.id, p.name, p.installedRelease, curr.version, curr.requires, p.getInstallState());
 				}
 			}
 		}
 	}
+
+	@CommandMetaData(name = "install", description = "Download and installs a plugin")
+	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;
+
+		@Option(name = "--noverify", usage = "Disable checksum verification")
+		private boolean disableChecksum;
+
+		@Override
+		public void run() throws Failure {
+			IGitblit gitblit = getContext().getGitblit();
+			try {
+				String ulc = urlOrIdOrName.toLowerCase();
+				if (ulc.startsWith("http://") || ulc.startsWith("https://")) {
+					if (gitblit.installPlugin(urlOrIdOrName, !disableChecksum)) {
+						stdout.println(String.format("Installed %s", urlOrIdOrName));
+					} else {
+						new Failure(1, String.format("Failed to install %s", urlOrIdOrName));
+					}
+				} else {
+					PluginRelease pv = gitblit.lookupRelease(urlOrIdOrName, version);
+					if (pv == null) {
+						throw new Failure(1,  String.format("Plugin \"%s\" is not in the registry!", urlOrIdOrName));
+					}
+					if (gitblit.installPlugin(pv.url, !disableChecksum)) {
+						stdout.println(String.format("Installed %s", urlOrIdOrName));
+					} else {
+						throw new Failure(1, String.format("Failed to install %s", urlOrIdOrName));
+					}
+				}
+			} catch (Exception e) {
+				log.error("Failed to install " + urlOrIdOrName, e);
+				throw new Failure(1, String.format("Failed to install %s", urlOrIdOrName), e);
+			}
+		}
+	}
+
+	@CommandMetaData(name = "uninstall", aliases = { "rm", "del" }, description = "Uninstall a plugin")
+	public static class UninstallPlugin extends PluginCommand {
+
+		@Argument(index = 0, required = true, metaVar = "<id>", usage = "the plugin to uninstall")
+		protected String id;
+
+		@Override
+		public void run() throws Failure {
+			IGitblit gitblit = getContext().getGitblit();
+			PluginWrapper pluginWrapper = getPlugin(id);
+			if (pluginWrapper == null) {
+				throw new UnloggedFailure(String.format("Plugin %s is not installed!", id));
+			}
+
+			if (gitblit.deletePlugin(pluginWrapper.getPluginId())) {
+				stdout.println(String.format("Uninstalled %s", pluginWrapper.getPluginId()));
+			} else {
+				throw new Failure(1, String.format("Failed to uninstall %s", pluginWrapper.getPluginId()));
+			}
+		}
+	}
 }
diff --git a/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java b/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java
index 3c37866..bebb4ac 100644
--- a/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java
@@ -20,6 +20,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import ro.fortsoft.pf4j.PluginWrapper;
+
 import com.gitblit.manager.IGitblit;
 import com.gitblit.models.UserModel;
 import com.gitblit.transport.ssh.SshDaemonClient;
@@ -49,9 +51,10 @@
 		List<DispatchCommand> exts = gitblit.getExtensions(DispatchCommand.class);
 		for (DispatchCommand ext : exts) {
 			Class<? extends DispatchCommand> extClass = ext.getClass();
-			String plugin = gitblit.whichPlugin(extClass).getDescriptor().getPluginId();
+			PluginWrapper wrapper = gitblit.whichPlugin(extClass);
+			String plugin = wrapper.getDescriptor().getPluginId();
 			CommandMetaData meta = extClass.getAnnotation(CommandMetaData.class);
-			log.info("Dispatcher {} is loaded from plugin {}", meta.name(), plugin);
+			log.debug("Dispatcher {} is loaded from plugin {}", meta.name(), plugin);
 			register(user, ext);
 		}
 	}
diff --git a/src/main/java/com/gitblit/utils/StringUtils.java b/src/main/java/com/gitblit/utils/StringUtils.java
index 5813c3a..7605fe0 100644
--- a/src/main/java/com/gitblit/utils/StringUtils.java
+++ b/src/main/java/com/gitblit/utils/StringUtils.java
@@ -307,7 +307,7 @@
 	 * @param bytes
 	 * @return byte array as hex string
 	 */
-	private static String toHex(byte[] bytes) {
+	public static String toHex(byte[] bytes) {
 		StringBuilder sb = new StringBuilder(bytes.length * 2);
 		for (int i = 0; i < bytes.length; i++) {
 			if ((bytes[i] & 0xff) < 0x10) {

--
Gitblit v1.9.1