James Moger
2014-04-10 b2fec20f1f1081607b54b3e7dd20b12d03cef113
Improve plugin manager based on upstreamed contributions to pf4j
10 files modified
1110 ■■■■ changed files
.classpath 2 ●●● patch | view | raw | blame | history
build.moxie 2 ●●● patch | view | raw | blame | history
gitblit.iml 6 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/GitblitManager.java 152 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/IPluginManager.java 103 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/PluginManager.java 335 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/PluginRegistry.java 21 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/commands/PluginDispatcher.java 480 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/commands/RootDispatcher.java 7 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/StringUtils.java 2 ●●● patch | view | raw | blame | history
.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" />
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
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>
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);
    }
}
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
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 {
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));
        }
    }
}
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()));
            }
        }
    }
}
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);
        }
    }
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) {