Merge "Add a new permission to permit rebasing changes in the web UI"
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
index 4d1c1f5..1d4d3e2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
@@ -65,7 +65,7 @@
       PatchSetInfoNotAvailableException, RepositoryNotFoundException,
       IOException {
     final ReviewResult result =
-        abandonChangeFactory.create(patchSetId, message).call();
+        abandonChangeFactory.create(patchSetId.getParentKey(), message).call();
     if (result.getErrors().size() > 0) {
       throw new NoSuchChangeException(result.getChangeId());
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
index bcb03b0..5d7fe32 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
@@ -64,7 +64,7 @@
       PatchSetInfoNotAvailableException, RepositoryNotFoundException,
       IOException {
     final ReviewResult result =
-        restoreChangeFactory.create(patchSetId, message).call();
+        restoreChangeFactory.create(patchSetId.getParentKey(), message).call();
     if (result.getErrors().size() > 0) {
       throw new NoSuchChangeException(result.getChangeId());
     }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 8cef4d7..85b1012 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -46,11 +46,12 @@
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
+import com.google.gerrit.server.plugins.PluginModule;
 import com.google.gerrit.server.schema.SchemaVersionCheck;
 import com.google.gerrit.server.ssh.NoSshModule;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.MasterCommandModule;
-import com.google.gerrit.sshd.commands.MasterPluginsModule;
 import com.google.gerrit.sshd.commands.SlaveCommandModule;
 import com.google.inject.Injector;
 import com.google.inject.Module;
@@ -141,6 +142,8 @@
       dbInjector = createDbInjector(MULTI_USER);
       cfgInjector = createCfgInjector();
       sysInjector = createSysInjector();
+      sysInjector.getInstance(PluginGuiceEnvironment.class)
+        .setCfgInjector(cfgInjector);
       manager.add(dbInjector, cfgInjector, sysInjector);
 
       if (sshd) {
@@ -209,6 +212,7 @@
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
     modules.add(new PushReplication.Module());
+    modules.add(new PluginModule());
     if (httpd) {
       modules.add(new CanonicalWebUrlModule() {
         @Override
@@ -232,6 +236,8 @@
 
   private void initSshd() {
     sshInjector = createSshInjector();
+    sysInjector.getInstance(PluginGuiceEnvironment.class)
+        .setSshInjector(sshInjector);
     manager.add(sshInjector);
   }
 
@@ -243,7 +249,6 @@
         modules.add(new SlaveCommandModule());
       } else {
         modules.add(new MasterCommandModule());
-        modules.add(cfgInjector.getInstance(MasterPluginsModule.class));
       }
     } else {
       modules.add(new NoSshModule());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/common/PluginLoader.java
deleted file mode 100644
index ae428e1..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/common/PluginLoader.java
+++ /dev/null
@@ -1,146 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import com.google.inject.Module;
-import com.google.inject.Singleton;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.FileFilter;
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.jar.Attributes;
-import java.util.jar.JarFile;
-import java.util.jar.Manifest;
-
-@Singleton
-public class PluginLoader {
-  private static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
-
-  private final File pluginsDir;
-  private Map<String, Plugin> pluginCache;
-
-  @Inject
-  public PluginLoader(SitePaths sitePaths) {
-    pluginsDir = sitePaths.plugins_dir;
-  }
-
-  private synchronized void initialize() {
-    if (pluginCache != null) {
-      return;
-    }
-
-    pluginCache = new HashMap<String, Plugin>();
-    loadPlugins();
-  }
-
-  public Plugin get(String pluginName) {
-    initialize();
-    return pluginCache.get(pluginName);
-  }
-
-  public Collection<Plugin> getPlugins() {
-    initialize();
-    return pluginCache.values();
-  }
-
-  private void loadPlugins() {
-    Collection<File> pluginJars;
-    try {
-      pluginJars = getPluginFiles();
-    } catch (IOException e) {
-      log.error("Cannot scan Gerrit plugins directory looking for jar files", e);
-      return;
-    }
-
-    for (File jarFile : pluginJars) {
-      Plugin plugin;
-      try {
-        plugin = loadPlugin(jarFile);
-        pluginCache.put(plugin.name, plugin);
-      } catch (IOException e) {
-        log.error("Cannot access plugin jar " + jarFile, e);
-      } catch (ClassNotFoundException e) {
-        log.error("Cannot load plugin class module from " + jarFile, e);
-      }
-    }
-  }
-
-  @SuppressWarnings("unchecked")
-  private Plugin loadPlugin(File jarFile) throws IOException,
-      ClassNotFoundException {
-    Manifest jarManifest = new JarFile(jarFile).getManifest();
-    ClassLoader parentLoader = PluginLoader.class.getClassLoader();
-    ClassLoader jarClassLoader =
-        new URLClassLoader(getPluginURLs(jarFile), parentLoader);
-
-    Attributes attrs = jarManifest.getMainAttributes();
-    String pluginName = attrs.getValue("Gerrit-Plugin");
-    if (Strings.isNullOrEmpty(pluginName)) {
-      throw new IOException("No Gerrit-Plugin attribute in manifest");
-    }
-
-    String moduleName = attrs.getValue("Gerrit-SshModule");
-    if (Strings.isNullOrEmpty(moduleName)) {
-      throw new IOException("No Gerrit-SshModule attribute in manifest");
-    }
-
-    Class<?> moduleClass = Class.forName(moduleName, false, jarClassLoader);
-    if (!Module.class.isAssignableFrom(moduleClass)) {
-      throw new ClassNotFoundException(String.format(
-          "Gerrit-SshModule %s is not a Guice Module",
-          moduleClass.getName()));
-    }
-
-    return new Plugin(pluginName, (Class<? extends Module>) moduleClass);
-  }
-
-  private URL[] getPluginURLs(File jarFile) throws MalformedURLException {
-    return new URL[] {jarFile.toURI().toURL()};
-  }
-
-  private List<File> getPluginFiles() throws IOException {
-    if (pluginsDir == null || !pluginsDir.exists()) {
-      return Collections.emptyList();
-    }
-
-    File[] plugins = pluginsDir.listFiles(new FileFilter() {
-      @Override
-      public boolean accept(File pathname) {
-        return pathname.isFile() && pathname.getName().endsWith(".jar");
-      }
-    });
-    if (plugins == null) {
-      log.error("Cannot list " + pluginsDir.getAbsolutePath());
-      return Collections.emptyList();
-    }
-
-    return Arrays.asList(plugins);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
index 1fac8c5..83fa671 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
@@ -35,10 +35,12 @@
 
 import java.util.concurrent.Callable;
 
+import javax.annotation.Nullable;
+
 public class AbandonChange implements Callable<ReviewResult> {
 
   public interface Factory {
-    AbandonChange create(PatchSet.Id patchSetId, String changeComment);
+    AbandonChange create(Change.Id changeId, String changeComment);
   }
 
   private final AbandonedSender.Factory abandonedSenderFactory;
@@ -47,22 +49,22 @@
   private final IdentifiedUser currentUser;
   private final ChangeHooks hooks;
 
-  private final PatchSet.Id patchSetId;
+  private final Change.Id changeId;
   private final String changeComment;
 
   @Inject
   AbandonChange(final AbandonedSender.Factory abandonedSenderFactory,
       final ChangeControl.Factory changeControlFactory, final ReviewDb db,
       final IdentifiedUser currentUser, final ChangeHooks hooks,
-      @Assisted final PatchSet.Id patchSetId,
-      @Assisted final String changeComment) {
+      @Assisted final Change.Id changeId,
+      @Assisted @Nullable final String changeComment) {
     this.abandonedSenderFactory = abandonedSenderFactory;
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.currentUser = currentUser;
     this.hooks = hooks;
 
-    this.patchSetId = patchSetId;
+    this.changeId = changeId;
     this.changeComment = changeComment;
   }
 
@@ -70,10 +72,11 @@
   public ReviewResult call() throws EmailException,
       InvalidChangeOperationException, NoSuchChangeException, OrmException {
     final ReviewResult result = new ReviewResult();
-
-    final Change.Id changeId = patchSetId.getParentKey();
     result.setChangeId(changeId);
+
     final ChangeControl control = changeControlFactory.validateFor(changeId);
+    final Change change = db.changes().get(changeId);
+    final PatchSet.Id patchSetId = change.currentPatchSetId();
     final PatchSet patch = db.patchSets().get(patchSetId);
     if (!control.canAbandon()) {
       result.addError(new ReviewResult.Error(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
index 7e52564..966efce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
@@ -41,10 +41,12 @@
 import java.io.IOException;
 import java.util.concurrent.Callable;
 
+import javax.annotation.Nullable;
+
 public class RestoreChange implements Callable<ReviewResult> {
 
   public interface Factory {
-    RestoreChange create(PatchSet.Id patchSetId, String changeComment);
+    RestoreChange create(Change.Id changeId, String changeComment);
   }
 
   private final RestoredSender.Factory restoredSenderFactory;
@@ -54,15 +56,15 @@
   private final IdentifiedUser currentUser;
   private final ChangeHooks hooks;
 
-  private final PatchSet.Id patchSetId;
+  private final Change.Id changeId;
   private final String changeComment;
 
   @Inject
   RestoreChange(final RestoredSender.Factory restoredSenderFactory,
       final ChangeControl.Factory changeControlFactory, final ReviewDb db,
       final GitRepositoryManager repoManager, final IdentifiedUser currentUser,
-      final ChangeHooks hooks, @Assisted final PatchSet.Id patchSetId,
-      @Assisted final String changeComment) {
+      final ChangeHooks hooks, @Assisted final Change.Id changeId,
+      @Assisted @Nullable final String changeComment) {
     this.restoredSenderFactory = restoredSenderFactory;
     this.changeControlFactory = changeControlFactory;
     this.db = db;
@@ -70,7 +72,7 @@
     this.currentUser = currentUser;
     this.hooks = hooks;
 
-    this.patchSetId = patchSetId;
+    this.changeId = changeId;
     this.changeComment = changeComment;
   }
 
@@ -79,10 +81,11 @@
       InvalidChangeOperationException, NoSuchChangeException, OrmException,
       RepositoryNotFoundException, IOException {
     final ReviewResult result = new ReviewResult();
-
-    final Change.Id changeId = patchSetId.getParentKey();
     result.setChangeId(changeId);
+
     final ChangeControl control = changeControlFactory.validateFor(changeId);
+    final Change change = db.changes().get(changeId);
+    final PatchSet.Id patchSetId = change.currentPatchSetId();
     if (!control.canRestore()) {
       result.addError(new ReviewResult.Error(
           ReviewResult.Error.Type.RESTORE_NOT_PERMITTED));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
new file mode 100644
index 0000000..f34826d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.File;
+
+/**
+ * Copies critical objects from the {@code dbInjector} into a plugin.
+ * <p>
+ * Most explicit bindings are copied automatically from the cfgInjector and
+ * sysInjector to be made available to a plugin's private world. This module is
+ * necessary to get things bound in the dbInjector that are not otherwise easily
+ * available, but that a plugin author might expect to exist.
+ */
+@Singleton
+class CopyConfigModule extends AbstractModule {
+  @Inject
+  @SitePath
+  private File sitePath;
+
+  @Provides
+  @SitePath
+  File getSitePath() {
+    return sitePath;
+  }
+
+  @Inject
+  private SitePaths sitePaths;
+
+  @Provides
+  SitePaths getSitePaths() {
+    return sitePaths;
+  }
+
+  @Inject
+  private TrackingFooters trackingFooters;
+
+  @Provides
+  TrackingFooters getTrackingFooters() {
+    return trackingFooters;
+  }
+
+  @Inject
+  @GerritServerConfig
+  private Config gerritServerConfig;
+
+  @Provides
+  @GerritServerConfig
+  Config getGerritServerConfig() {
+    return gerritServerConfig;
+  }
+
+  @Inject
+  private SchemaFactory<ReviewDb> schemaFactory;
+
+  @Provides
+  SchemaFactory<ReviewDb> getSchemaFactory() {
+    return schemaFactory;
+  }
+
+  @Inject
+  private GitRepositoryManager gitRepositoryManager;
+
+  @Provides
+  GitRepositoryManager getGitRepositoryManager() {
+    return gitRepositoryManager;
+  }
+
+  @Inject
+  CopyConfigModule() {
+  }
+
+  @Override
+  protected void configure() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
new file mode 100644
index 0000000..0c1ab0f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -0,0 +1,145 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+
+import org.eclipse.jgit.storage.file.FileSnapshot;
+
+import java.io.File;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+import javax.annotation.Nullable;
+
+public class Plugin {
+  private final String name;
+  private final File jar;
+  private final Manifest manifest;
+  private final FileSnapshot snapshot;
+  private Class<? extends Module> sysModule;
+  private Class<? extends Module> sshModule;
+
+  private Injector sysInjector;
+  private Injector sshInjector;
+  private LifecycleManager manager;
+
+  public Plugin(String name,
+      File jar,
+      Manifest manifest,
+      FileSnapshot snapshot,
+      @Nullable Class<? extends Module> sysModule,
+      @Nullable Class<? extends Module> sshModule) {
+    this.name = name;
+    this.jar = jar;
+    this.manifest = manifest;
+    this.snapshot = snapshot;
+    this.sysModule = sysModule;
+    this.sshModule = sshModule;
+  }
+
+  File getJar() {
+    return jar;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public String getVersion() {
+    Attributes main = manifest.getMainAttributes();
+    return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+  }
+
+  boolean isModified(File jar) {
+    return snapshot.lastModified() != jar.lastModified();
+  }
+
+  public void start(PluginGuiceEnvironment env) throws Exception {
+    Injector root = newRootInjector(env);
+    manager = new LifecycleManager();
+
+    if (sysModule != null) {
+      sysInjector = root.createChildInjector(root.getInstance(sysModule));
+      manager.add(sysInjector);
+    } else {
+      sysInjector = root;
+    }
+
+    if (sshModule != null && env.hasSshModule()) {
+      sshInjector = sysInjector.createChildInjector(
+          env.getSshModule(),
+          sysInjector.getInstance(sshModule));
+      manager.add(sshInjector);
+    }
+
+    manager.start();
+    env.onStartPlugin(this);
+  }
+
+  private Injector newRootInjector(PluginGuiceEnvironment env) {
+    return Guice.createInjector(
+        env.getSysModule(),
+        new AbstractModule() {
+          @Override
+          protected void configure() {
+            bind(String.class)
+              .annotatedWith(PluginName.class)
+              .toInstance(name);
+          }
+        });
+  }
+
+  public void stop() {
+    if (manager != null) {
+      manager.stop();
+      manager = null;
+      sysInjector = null;
+      sshInjector = null;
+    }
+  }
+
+  @Nullable
+  public Injector getSshInjector() {
+    return sshInjector;
+  }
+
+  public void add(final RegistrationHandle handle) {
+    add(new LifecycleListener() {
+      @Override
+      public void start() {
+      }
+
+      @Override
+      public void stop() {
+        handle.remove();
+      }
+    });
+  }
+
+  public void add(LifecycleListener listener) {
+    manager.add(listener);
+  }
+
+  @Override
+  public String toString() {
+    return "Plugin [" + name + "]";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
new file mode 100644
index 0000000..418fbf2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.inject.AbstractModule;
+import com.google.inject.Binding;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.inject.Inject;
+
+/**
+ * Tracks Guice bindings that should be exposed to loaded plugins.
+ * <p>
+ * This is an internal implementation detail of how the main server is able to
+ * export its explicit Guice bindings to tightly coupled plugins, giving them
+ * access to singletons and request scoped resources just like any core code.
+ */
+@Singleton
+public class PluginGuiceEnvironment {
+  private final Injector sysInjector;
+  private final CopyConfigModule copyConfigModule;
+  private final List<StartPluginListener> listeners;
+  private Module sysModule;
+  private Module sshModule;
+
+  @Inject
+  PluginGuiceEnvironment(Injector sysInjector, CopyConfigModule ccm) {
+    this.sysInjector = sysInjector;
+    this.copyConfigModule = ccm;
+    this.listeners = new CopyOnWriteArrayList<StartPluginListener>();
+    this.listeners.addAll(getListeners(sysInjector));
+  }
+
+  Module getSysModule() {
+    return sysModule;
+  }
+
+  public void setCfgInjector(Injector cfgInjector) {
+    final Module cm = copy(cfgInjector);
+    final Module sm = copy(sysInjector);
+    sysModule = new AbstractModule() {
+      @Override
+      protected void configure() {
+        install(copyConfigModule);
+        install(cm);
+        install(sm);
+      }
+    };
+  }
+
+  public void setSshInjector(Injector sshInjector) {
+    sshModule = copy(sshInjector);
+    listeners.addAll(getListeners(sshInjector));
+  }
+
+  boolean hasSshModule() {
+    return sshModule != null;
+  }
+
+  Module getSshModule() {
+    return sshModule;
+  }
+
+  void onStartPlugin(Plugin plugin) {
+    for (StartPluginListener l : listeners) {
+      l.onStartPlugin(plugin);
+    }
+  }
+
+  private static List<StartPluginListener> getListeners(Injector src) {
+    List<Binding<StartPluginListener>> bindings =
+        src.findBindingsByType(new TypeLiteral<StartPluginListener>() {});
+    List<StartPluginListener> found =
+        Lists.newArrayListWithCapacity(bindings.size());
+    for (Binding<StartPluginListener> b : bindings) {
+      found.add(b.getProvider().get());
+    }
+    return found;
+  }
+
+  private static Module copy(Injector src) {
+    final Map<Key<?>, Binding<?>> bindings = Maps.newLinkedHashMap();
+    for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+      if (shouldCopy(e.getKey())) {
+        bindings.put(e.getKey(), e.getValue());
+      }
+    }
+    bindings.remove(Key.get(Injector.class));
+    bindings.remove(Key.get(java.util.logging.Logger.class));
+
+    return new AbstractModule() {
+      @SuppressWarnings("unchecked")
+      @Override
+      protected void configure() {
+        for (Map.Entry<Key<?>, Binding<?>> e : bindings.entrySet()) {
+          Key<Object> k = (Key<Object>) e.getKey();
+          Binding<Object> b = (Binding<Object>) e.getValue();
+          bind(k).toProvider(b.getProvider());
+        }
+      }
+    };
+  }
+
+  private static boolean shouldCopy(Key<?> key) {
+    Class<?> type = key.getTypeLiteral().getRawType();
+    if (type == LifecycleListener.class) {
+      return false;
+    }
+    if (type == StartPluginListener.class) {
+      return false;
+    }
+    if ("org.apache.sshd.server.Command".equals(type.getName())) {
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.java
new file mode 100644
index 0000000..77fa702
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+public class PluginInstallException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public PluginInstallException(Throwable why) {
+    super(why.getMessage(), why);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
new file mode 100644
index 0000000..44b2f12
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -0,0 +1,291 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileSnapshot;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+@Singleton
+public class PluginLoader implements LifecycleListener {
+  private static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
+
+  private final File pluginsDir;
+  private final PluginGuiceEnvironment env;
+  private final Map<String, Plugin> running;
+  private final Map<String, FileSnapshot> broken;
+  private final PluginScannerThread scanner;
+
+  @Inject
+  public PluginLoader(SitePaths sitePaths,
+      PluginGuiceEnvironment pe,
+      @GerritServerConfig Config cfg) {
+    pluginsDir = sitePaths.plugins_dir;
+    env = pe;
+    running = Maps.newHashMap();
+    broken = Maps.newHashMap();
+    scanner = new PluginScannerThread(
+        this,
+        ConfigUtil.getTimeUnit(cfg,
+            "plugins", null, "checkFrequency",
+            TimeUnit.MINUTES.toMillis(1), TimeUnit.MILLISECONDS));
+  }
+
+  public synchronized List<Plugin> getPlugins() {
+    return Lists.newArrayList(running.values());
+  }
+
+  public void installPluginFromStream(String name, InputStream in)
+      throws IOException, PluginInstallException {
+    if (!name.endsWith(".jar")) {
+      name += ".jar";
+    }
+
+    File jar = new File(pluginsDir, name);
+    name = nameOf(jar);
+
+    File old = new File(pluginsDir, ".last_" + name + ".zip");
+    File tmp = copyToTemp(name, in);
+
+    synchronized (this) {
+      Plugin active = running.get(name);
+      if (active != null) {
+        log.info(String.format("Replacing plugin %s", name));
+        active.stop();
+        running.remove(name);
+        old.delete();
+        jar.renameTo(old);
+      }
+
+      tmp.renameTo(jar);
+      FileSnapshot snapshot = FileSnapshot.save(jar);
+      Plugin next;
+      try {
+        next = loadPlugin(name, snapshot, jar);
+        next.start(env);
+      } catch (Throwable err) {
+        jar.delete();
+        throw new PluginInstallException(err);
+      }
+      broken.remove(name);
+      running.put(name, next);
+      if (active == null) {
+        log.info(String.format("Installed plugin %s", name));
+      }
+    }
+  }
+
+  private File copyToTemp(String name, InputStream in) throws IOException {
+    File tmp = File.createTempFile(".next_" + name, ".zip", pluginsDir);
+    boolean keep = false;
+    try {
+      FileOutputStream out = new FileOutputStream(tmp);
+      try {
+        byte[] data = new byte[8192];
+        int n;
+        while ((n = in.read(data)) > 0) {
+          out.write(data, 0, n);
+        }
+        keep = true;
+        return tmp;
+      } finally {
+        out.close();
+      }
+    } finally {
+      if (!keep) {
+        tmp.delete();
+      }
+    }
+  }
+
+  public synchronized void disablePlugins(Set<String> names) {
+    for (String name : names) {
+      Plugin active = running.get(name);
+      if (active == null) {
+        continue;
+      }
+
+      log.info(String.format("Disabling plugin %s", name));
+      active.stop();
+      running.remove(name);
+
+      File off = new File(pluginsDir, active.getName() + ".jar.disabled");
+      active.getJar().renameTo(off);
+    }
+  }
+
+  @Override
+  public synchronized void start() {
+    log.info("Loading plugins from " + pluginsDir.getAbsolutePath());
+    rescan();
+    scanner.start();
+  }
+
+  @Override
+  public void stop() {
+    scanner.end();
+    synchronized (this) {
+      for (Plugin p : running.values()) {
+        p.stop();
+      }
+      running.clear();
+      broken.clear();
+    }
+  }
+
+  public synchronized void rescan() {
+    List<File> jars = scanJarsInPluginsDirectory();
+
+    stopRemovedPlugins(jars);
+
+    for (File jar : jars) {
+      String name = nameOf(jar);
+      FileSnapshot brokenTime = broken.get(name);
+      if (brokenTime != null && !brokenTime.isModified(jar)) {
+        continue;
+      }
+
+      Plugin active = running.get(name);
+      if (active != null && !active.isModified(jar)) {
+        continue;
+      }
+
+      if (active != null) {
+        log.warn(String.format(
+            "Detected %s was replaced/overwritten."
+            + " This is not a safe way to update a plugin.",
+            jar.getAbsolutePath()));
+        log.info(String.format("Reloading plugin %s", name));
+        active.stop();
+        running.remove(name);
+      }
+
+      FileSnapshot snapshot = FileSnapshot.save(jar);
+      Plugin next;
+      try {
+        next = loadPlugin(name, snapshot, jar);
+        next.start(env);
+      } catch (Throwable err) {
+        log.warn(String.format("Cannot load plugin %s", name), err);
+        broken.put(name, snapshot);
+        continue;
+      }
+      broken.remove(name);
+      running.put(name, next);
+
+      if (active == null) {
+        log.info(String.format("Loaded plugin %s", name));
+      }
+    }
+  }
+
+  private void stopRemovedPlugins(List<File> jars) {
+    Set<String> unload = Sets.newHashSet(running.keySet());
+    for (File jar : jars) {
+      unload.remove(nameOf(jar));
+    }
+    for (String name : unload){
+      log.info(String.format("Unloading plugin %s", name));
+      running.remove(name).stop();
+    }
+  }
+
+  private static String nameOf(File jar) {
+    String name = jar.getName();
+    int ext = name.lastIndexOf('.');
+    return 0 < ext ? name.substring(0, ext) : name;
+  }
+
+  private Plugin loadPlugin(String name, FileSnapshot snapshot, File jarFile)
+      throws IOException, ClassNotFoundException {
+    Manifest manifest = new JarFile(jarFile).getManifest();
+
+    Attributes main = manifest.getMainAttributes();
+    String sysName = main.getValue("Gerrit-Module");
+    String sshName = main.getValue("Gerrit-SshModule");
+
+    URL[] urls = {jarFile.toURI().toURL()};
+    ClassLoader parentLoader = PluginLoader.class.getClassLoader();
+    ClassLoader pluginLoader = new URLClassLoader(urls, parentLoader);
+
+    Class<? extends Module> sysModule = load(sysName, pluginLoader);
+    Class<? extends Module> sshModule = load(sshName, pluginLoader);
+    return new Plugin(name, jarFile, manifest, snapshot, sysModule, sshModule);
+  }
+
+  private Class<? extends Module> load(String name, ClassLoader pluginLoader)
+      throws ClassNotFoundException {
+    if (Strings.isNullOrEmpty(name)) {
+      return null;
+    }
+
+    @SuppressWarnings("unchecked")
+    Class<? extends Module> clazz =
+        (Class<? extends Module>) Class.forName(name, false, pluginLoader);
+    if (!Module.class.isAssignableFrom(clazz)) {
+      throw new ClassCastException(String.format(
+          "Class %s does not implement %s",
+          name, Module.class.getName()));
+    }
+    return clazz;
+  }
+
+  private List<File> scanJarsInPluginsDirectory() {
+    if (pluginsDir == null || !pluginsDir.exists()) {
+      return Collections.emptyList();
+    }
+    File[] matches = pluginsDir.listFiles(new FileFilter() {
+      @Override
+      public boolean accept(File pathname) {
+        return pathname.getName().endsWith(".jar") && pathname.isFile();
+      }
+    });
+    if (matches == null) {
+      log.error("Cannot list " + pluginsDir.getAbsolutePath());
+      return Collections.emptyList();
+    }
+    return Arrays.asList(matches);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
similarity index 61%
rename from gerrit-server/src/main/java/com/google/gerrit/common/Plugin.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
index 36f4311..0431ee1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
@@ -12,21 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.common;
+package com.google.gerrit.server.plugins;
 
-import com.google.inject.Module;
+import com.google.gerrit.lifecycle.LifecycleModule;
 
-public class Plugin {
-  public final String name;
-  public final Class<? extends Module> sshModule;
-
-  public Plugin(String name, Class<? extends Module> sshModule) {
-    this.name = name;
-    this.sshModule = sshModule;
-  }
-
+public class PluginModule extends LifecycleModule {
   @Override
-  public String toString() {
-    return "Plugin [" + name + "; SshModule=" + sshModule.getName() + "]";
+  protected void configure() {
+    bind(PluginGuiceEnvironment.class);
+    bind(PluginLoader.class);
+    bind(CopyConfigModule.class);
+    listener().to(PluginLoader.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginName.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginName.java
new file mode 100644
index 0000000..6a47b93
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginName.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Target({ElementType.PARAMETER, ElementType.FIELD})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface PluginName {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
new file mode 100644
index 0000000..a484c5d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+class PluginScannerThread extends Thread {
+  private final CountDownLatch done = new CountDownLatch(1);
+  private final PluginLoader loader;
+  private final long checkFrequencyMillis;
+
+  PluginScannerThread(PluginLoader loader, long checkFrequencyMillis) {
+    this.loader = loader;
+    this.checkFrequencyMillis = checkFrequencyMillis;
+    setDaemon(true);
+    setName("PluginScanner");
+  }
+
+  @Override
+  public void run() {
+    for (;;) {
+      try {
+        if (done.await(checkFrequencyMillis, TimeUnit.MILLISECONDS)) {
+          return;
+        }
+      } catch (InterruptedException e) {
+      }
+      loader.rescan();
+    }
+  }
+
+  void end() {
+    done.countDown();
+    try {
+      join();
+    } catch (InterruptedException e) {
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java
new file mode 100644
index 0000000..cd0b334
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/RegistrationHandle.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+/** Handle for registered information. */
+public interface RegistrationHandle {
+  /** Delete this registration. */
+  public void remove();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java
new file mode 100644
index 0000000..aaad370
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+/** Broadcasts event indicating a plugin was loaded. */
+public interface StartPluginListener {
+  public void onStartPlugin(Plugin plugin);
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
index 0b69228..349cc79 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.plugins.RegistrationHandle;
 import com.google.inject.Binding;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -23,11 +25,8 @@
 import org.apache.sshd.server.Command;
 
 import java.lang.annotation.Annotation;
-import java.util.Collections;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
+import java.util.concurrent.ConcurrentMap;
 
 /**
  * Creates DispatchCommand using commands registered by {@link CommandModule}.
@@ -42,7 +41,7 @@
   private final String dispatcherName;
   private final CommandName parent;
 
-  private volatile Map<String, Provider<Command>> map;
+  private volatile ConcurrentMap<String, Provider<Command>> map;
 
   public DispatchCommandProvider(final CommandName cn) {
     this(Commands.nameOf(cn), cn);
@@ -59,7 +58,21 @@
     return factory.create(dispatcherName, getMap());
   }
 
-  private Map<String, Provider<Command>> getMap() {
+  public RegistrationHandle register(final CommandName name,
+      final Provider<Command> cmd) {
+    final ConcurrentMap<String, Provider<Command>> m = getMap();
+    if (m.putIfAbsent(name.value(), cmd) != null) {
+      throw new IllegalArgumentException(name.value() + " exists");
+    }
+    return new RegistrationHandle() {
+      @Override
+      public void remove() {
+        m.remove(name.value(), cmd);
+      }
+    };
+  }
+
+  private ConcurrentMap<String, Provider<Command>> getMap() {
     if (map == null) {
       synchronized (this) {
         if (map == null) {
@@ -71,10 +84,8 @@
   }
 
   @SuppressWarnings("unchecked")
-  private Map<String, Provider<Command>> createMap() {
-    final Map<String, Provider<Command>> m =
-        new TreeMap<String, Provider<Command>>();
-
+  private ConcurrentMap<String, Provider<Command>> createMap() {
+    ConcurrentMap<String, Provider<Command>> m = Maps.newConcurrentMap();
     for (final Binding<?> b : allCommands()) {
       final Annotation annotation = b.getKey().getAnnotation();
       if (annotation instanceof CommandName) {
@@ -84,9 +95,7 @@
         }
       }
     }
-
-    return Collections.unmodifiableMap(
-        new LinkedHashMap<String, Provider<Command>>(m));
+    return m;
   }
 
   private static final TypeLiteral<Command> type =
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index 558707b..2637529 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -30,12 +31,14 @@
 import com.google.gerrit.server.config.GerritRequestModule;
 import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.plugins.StartPluginListener;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gerrit.sshd.args4j.AccountGroupIdHandler;
 import com.google.gerrit.sshd.args4j.AccountGroupUUIDHandler;
 import com.google.gerrit.sshd.args4j.AccountIdHandler;
+import com.google.gerrit.sshd.args4j.ChangeIdHandler;
 import com.google.gerrit.sshd.args4j.ObjectIdHandler;
 import com.google.gerrit.sshd.args4j.PatchSetIdHandler;
 import com.google.gerrit.sshd.args4j.ProjectControlHandler;
@@ -44,6 +47,7 @@
 import com.google.gerrit.sshd.commands.QueryShell;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gerrit.util.cli.OptionHandlerUtil;
+import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.servlet.RequestScoped;
 
 import org.apache.sshd.common.KeyPairProvider;
@@ -89,6 +93,9 @@
     install(new LifecycleModule() {
       @Override
       protected void configure() {
+        bind(StartPluginListener.class)
+          .annotatedWith(UniqueAnnotations.create())
+          .to(SshPluginStarterCallback.class);
         listener().to(SshLog.class);
         listener().to(SshDaemon.class);
       }
@@ -120,6 +127,7 @@
     registerOptionHandler(Account.Id.class, AccountIdHandler.class);
     registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
     registerOptionHandler(AccountGroup.UUID.class, AccountGroupUUIDHandler.class);
+    registerOptionHandler(Change.Id.class, ChangeIdHandler.class);
     registerOptionHandler(ObjectId.class, ObjectIdHandler.class);
     registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
     registerOptionHandler(ProjectControl.class, ProjectControlHandler.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
new file mode 100644
index 0000000..b82eb8f
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.apache.sshd.server.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+
+@Singleton
+class SshPluginStarterCallback implements StartPluginListener {
+  private static final Logger log = LoggerFactory
+      .getLogger(SshPluginStarterCallback.class);
+
+  private final DispatchCommandProvider root;
+
+  @Inject
+  SshPluginStarterCallback(
+      @CommandName(Commands.ROOT) DispatchCommandProvider root) {
+    this.root = root;
+  }
+
+  @Override
+  public void onStartPlugin(Plugin plugin) {
+    if (plugin.getSshInjector() != null) {
+      Key<Command> key = Commands.key(plugin.getName());
+      Provider<Command> cmd;
+      try {
+        cmd = plugin.getSshInjector().getProvider(key);
+      } catch (RuntimeException err) {
+        log.warn(String.format("Plugin %s does not define command",
+            plugin.getName()), err);
+        return;
+      }
+      plugin.add(root.register(Commands.named(plugin.getName()), cmd));
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ChangeIdHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ChangeIdHandler.java
new file mode 100644
index 0000000..0194b91
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ChangeIdHandler.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.args4j;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+public class ChangeIdHandler extends OptionHandler<Change.Id> {
+
+  @Inject
+  private ReviewDb db;
+
+  @Inject
+  public ChangeIdHandler(
+      final ReviewDb db,
+      @Assisted final CmdLineParser parser, @Assisted final OptionDef option,
+      @Assisted final Setter<Change.Id> setter) {
+    super(parser, option, setter);
+    this.db = db;
+  }
+
+  @Override
+  public final int parseArguments(final Parameters params)
+      throws CmdLineException {
+    final String token = params.getParameter(0);
+    final String[] tokens = token.split(",");
+    if (tokens.length != 3) {
+      throw new CmdLineException(owner, "change should be specified as "
+                                 + "<project>,<branch>,<change-id>");
+    }
+
+    try {
+      final Change.Key key = Change.Key.parse(tokens[2]);
+      final Project.NameKey project = new Project.NameKey(tokens[0]);
+      final Branch.NameKey branch = new Branch.NameKey(project, tokens[1]);
+      for (final Change change : db.changes().byBranchKey(branch, key)) {
+        setter.addValue(change.getId());
+        return 1;
+      }
+    } catch (IllegalArgumentException e) {
+      throw new CmdLineException(owner, "Change-Id is not valid");
+    } catch (OrmException e) {
+      throw new CmdLineException(owner, "Database error: " + e.getMessage());
+    }
+
+    throw new CmdLineException(owner, "\"" + token + "\": change not found");
+  }
+
+  @Override
+  public final String getDefaultMetaVariable() {
+    return "CHANGE";
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index 4d7c93e..64e7289 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -28,6 +28,7 @@
   protected void configure() {
     final CommandName git = Commands.named("git");
     final CommandName gerrit = Commands.named("gerrit");
+    final CommandName plugin = Commands.named(gerrit, "plugin");
 
     // The following commands can be ran on a server in either Master or Slave
     // mode. If a command should only be used on a server in one mode, but not
@@ -46,6 +47,14 @@
     command(gerrit, "stream-events").to(StreamEvents.class);
     command(gerrit, "version").to(VersionCommand.class);
 
+    command(gerrit, "plugin").toProvider(new DispatchCommandProvider(plugin));
+    command(plugin, "ls").to(PluginLsCommand.class);
+    command(plugin, "install").to(PluginInstallCommand.class);
+    command(plugin, "reload").to(PluginReloadCommand.class);
+    command(plugin, "remove").to(PluginRemoveCommand.class);
+    command(plugin, "add").to(Commands.key(plugin, "install"));
+    command(plugin, "rm").to(Commands.key(plugin, "remove"));
+
     command(git).toProvider(new DispatchCommandProvider(git));
     command(git, "receive-pack").to(Commands.key(gerrit, "receive-pack"));
     command(git, "upload-pack").to(Upload.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterPluginsModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterPluginsModule.java
deleted file mode 100644
index 76eebce..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterPluginsModule.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd.commands;
-
-import com.google.gerrit.common.Plugin;
-import com.google.gerrit.common.PluginLoader;
-import com.google.gerrit.sshd.CommandModule;
-import com.google.inject.Inject;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Collection;
-
-public class MasterPluginsModule extends CommandModule {
-  private static final Logger log =
-      LoggerFactory.getLogger(MasterPluginsModule.class);
-
-  private PluginLoader pluginLoader;
-
-  @Inject
-  MasterPluginsModule(PluginLoader loader) {
-    pluginLoader = loader;
-  }
-
-  @Override
-  protected void configure() {
-    Collection<Plugin> plugins = pluginLoader.getPlugins();
-    for (Plugin p : plugins) {
-      if (PluginCommandModule.class.isAssignableFrom(p.sshModule)) {
-        @SuppressWarnings("unchecked")
-        Class<PluginCommandModule> c = (Class<PluginCommandModule>) p.sshModule;
-        try {
-          PluginCommandModule module = c.newInstance();
-          module.initSshModule(p.name);
-          install(module);
-        } catch (InstantiationException e) {
-          log.warn("Initialization of plugin module '" + p.name + "' failed");
-        } catch (IllegalAccessException e) {
-          log.warn("Initialization of plugin module '" + p.name + "' failed");
-        }
-      }
-    }
-  }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java
index 788dfa1..d9015c6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.base.Preconditions;
+import com.google.gerrit.server.plugins.PluginName;
 import com.google.gerrit.sshd.CommandName;
 import com.google.gerrit.sshd.Commands;
 import com.google.gerrit.sshd.DispatchCommandProvider;
@@ -22,20 +24,24 @@
 
 import org.apache.sshd.server.Command;
 
+import javax.inject.Inject;
+
 public abstract class PluginCommandModule extends AbstractModule {
   private CommandName command;
 
-  public void initSshModule(String pluginName) {
-    command = Commands.named(pluginName);
+  @Inject
+  void setPluginName(@PluginName String name) {
+    this.command = Commands.named(name);
   }
 
   @Override
   protected final void configure() {
+    Preconditions.checkState(command != null, "@PluginName must be provided");
     bind(Commands.key(command)).toProvider(new DispatchCommandProvider(command));
-    configureCmds();
+    configureCommands();
   }
 
-  protected abstract void configureCmds();
+  protected abstract void configureCommands();
 
   protected LinkedBindingBuilder<Command> command(String subCmd) {
     return bind(Commands.key(command, subCmd));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
new file mode 100644
index 0000000..2328847
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.server.plugins.PluginInstallException;
+import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.RequiresCapability;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginInstallCommand extends SshCommand {
+  @Option(name = "--name", aliases = {"-n"}, usage = "install under name")
+  private String name;
+
+  @Option(name = "-")
+  void useInput(boolean on) {
+    source = "-";
+  }
+
+  @Argument(index = 0, metaVar = "-|URL", usage = "JAR to load")
+  private String source;
+
+  @Inject
+  private PluginLoader loader;
+
+  @Override
+  protected void run() throws UnloggedFailure {
+    if (Strings.isNullOrEmpty(source)) {
+      throw die("Argument \"-|URL\" is required");
+    }
+    if (Strings.isNullOrEmpty(name) && "-".equalsIgnoreCase(source)) {
+      throw die("--name required when source is stdin");
+    }
+
+    if (Strings.isNullOrEmpty(name)) {
+      int s = source.lastIndexOf('/');
+      if (0 <= s) {
+        name = source.substring(s + 1);
+      } else {
+        name = source;
+      }
+    }
+
+    InputStream data;
+    if ("-".equalsIgnoreCase(source)) {
+      data = in;
+    } else if (new File(source).isFile()
+        && source.equals(new File(source).getAbsolutePath())) {
+      try {
+        data = new FileInputStream(new File(source));
+      } catch (FileNotFoundException e) {
+        throw die("cannot read " + source);
+      }
+    } else {
+      try {
+        data = new URL(source).openStream();
+      } catch (MalformedURLException e) {
+        throw die("invalid url " + source);
+      } catch (IOException e) {
+        throw die("cannot read " + source);
+      }
+    }
+    try {
+      loader.installPluginFromStream(name, data);
+    } catch (IOException e) {
+      throw die("cannot install plugin");
+    } catch (PluginInstallException e) {
+      e.printStackTrace(stderr);
+      throw die("plugin failed to install");
+    } finally {
+      try {
+        data.close();
+      } catch (IOException err) {
+      }
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
new file mode 100644
index 0000000..6044151
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.RequiresCapability;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginLsCommand extends SshCommand {
+  @Inject
+  private PluginLoader loader;
+
+  @Override
+  protected void run() {
+    List<Plugin> running = loader.getPlugins();
+    Collections.sort(running, new Comparator<Plugin>() {
+      @Override
+      public int compare(Plugin a, Plugin b) {
+        return a.getName().compareTo(b.getName());
+      }
+    });
+
+    stdout.format("%-30s %-10s\n", "Name", "Version");
+    stdout.print("----------------------------------------------------------------------\n");
+    for (Plugin p : running) {
+      stdout.format("%-30s %-10s\n", p.getName(),
+          Strings.nullToEmpty(p.getVersion()));
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
new file mode 100644
index 0000000..5486698
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.RequiresCapability;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginReloadCommand extends SshCommand {
+  @Inject
+  private PluginLoader loader;
+
+  @Override
+  protected void run() {
+    loader.rescan();
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
new file mode 100644
index 0000000..6444e71
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.RequiresCapability;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Argument;
+
+import java.util.List;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginRemoveCommand extends SshCommand {
+  @Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin to remove")
+  List<String> names;
+
+  @Inject
+  private PluginLoader loader;
+
+  @Override
+  protected void run() {
+    if (names != null && !names.isEmpty()) {
+      loader.disablePlugins(Sets.newHashSet(names));
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 74782ed..f38e17e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -202,11 +202,11 @@
 
       if (abandonChange) {
         final ReviewResult result = abandonChangeFactory.create(
-            patchSetId, changeComment).call();
+            patchSetId.getParentKey(), changeComment).call();
         handleReviewResultErrors(result);
       } else if (restoreChange) {
         final ReviewResult result = restoreChangeFactory.create(
-            patchSetId, changeComment).call();
+            patchSetId.getParentKey(), changeComment).call();
         handleReviewResultErrors(result);
       }
       if (submitChange) {
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 8ffc531..0c15dfd 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -37,13 +37,14 @@
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
+import com.google.gerrit.server.plugins.PluginModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DatabaseModule;
 import com.google.gerrit.server.schema.SchemaModule;
 import com.google.gerrit.server.schema.SchemaVersionCheck;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.MasterCommandModule;
-import com.google.gerrit.sshd.commands.MasterPluginsModule;
 import com.google.inject.AbstractModule;
 import com.google.inject.CreationException;
 import com.google.inject.Guice;
@@ -113,6 +114,10 @@
       sshInjector = createSshInjector();
       webInjector = createWebInjector();
 
+      PluginGuiceEnvironment env = sysInjector.getInstance(PluginGuiceEnvironment.class);
+      env.setCfgInjector(cfgInjector);
+      env.setSshInjector(sshInjector);
+
       // Push the Provider<HttpServletRequest> down into the canonical
       // URL provider. Its optional for that provider, but since we can
       // supply one we should do so, in case the administrator has not
@@ -198,6 +203,7 @@
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
     modules.add(new PushReplication.Module());
+    modules.add(new PluginModule());
     modules.add(new CanonicalWebUrlModule() {
       @Override
       protected Class<? extends Provider<String>> provider() {
@@ -212,7 +218,6 @@
     final List<Module> modules = new ArrayList<Module>();
     modules.add(new SshModule());
     modules.add(new MasterCommandModule());
-    modules.add(cfgInjector.getInstance(MasterPluginsModule.class));
     return sysInjector.createChildInjector(modules);
   }