Merge "Fix deadlock on destroy of CommandFactoryProvider."
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index fa727b5..5172e03 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -703,6 +703,21 @@
 is already restricted to the correct set of users.
 
 
+[[category_rebase]]
+Rebase
+~~~~~~
+
+This category permits users to rebase changes via the web UI by pushing
+the `Rebase Change` button.
+
+The change owner and submitters can always rebase changes in the web UI
+(even without having the `Rebase` access right assigned).
+
+Users without this access right who are able to upload new patch sets
+can still do the rebase locally and upload the rebased commit as a new
+patch set.
+
+
 [[category_submit]]
 Submit
 ~~~~~~
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index e7d59fb..7970084 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -96,6 +96,9 @@
 link:cmd-create-account.html[gerrit create-account]::
 	Create a new batch/role account.
 
+link:cmd-set-account.html[gerrit set-account]::
+	Change an account's settings.
+
 link:cmd-create-group.html[gerrit create-group]::
 	Create a new account group.
 
diff --git a/Documentation/cmd-set-account.txt b/Documentation/cmd-set-account.txt
new file mode 100644
index 0000000..5719a9c
--- /dev/null
+++ b/Documentation/cmd-set-account.txt
@@ -0,0 +1,92 @@
+gerrit set-account
+==================
+
+NAME
+----
+gerrit set-account - Change an account's settings.
+
+SYNOPSIS
+--------
+[verse]
+set-account [--full-name <FULLNAME>] [--active|--inactive] \
+            [--add-email <EMAIL>] [--delete-email <EMAIL> | ALL] \
+            [--add-ssh-key - | <KEY>] \
+            [--delete-ssh-key - | <KEY> | ALL] <USER>
+
+DESCRIPTION
+-----------
+Modifies a given user's settings. This command can be useful to
+deactivate an account or add/delete ssh keys without going through
+the UI.
+
+It also allows managing email addresses, which bypasses the
+verification step we force within the UI.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<USER>::
+    Required; Full name, email-address, SSH username or account id.
+
+--full-name::
+    Display name of the user account.
++
+Names containing spaces should be quoted in single quotes (').
+This most likely requires double quoting the value, for example
+`--full-name "'A description string'"`.
+
+--active::
+    Set the account state to be active.
+
+--inactive::
+    Set the account state to be inactive. This prevents the
+    user from logging in.
+
+--add-email::
+    Add another email to the user's account. This doesn't
+    trigger the mail validation and adds the email directly
+    to the user's account.
+    May be supplied more than once to add multiple emails to
+    an account in a single command execution.
+
+--delete-email::
+    Delete an email from this user's account if it exists.
+    If the email provided is 'ALL', all associated emails are
+    deleted from this account.
+    Maybe supplied more than once to remove multiple emails
+    from an account in a single command execution.
+
+--add-ssh-key::
+    Content of the public SSH key to add to the account's
+    keyring.  If `-` the key is read from stdin, rather than
+    from the command line.
+    May be supplied more than once to add multiple SSH keys
+    in a single command execution.
+
+--delete-ssh-key::
+    Content of the public SSH key to remove from the account's
+    keyring or the comment associated with this key.
+    If `-` the key is read from stdin, rather than from the
+    command line. If the key provided is 'ALL', all
+    associated SSH keys are removed from this account.
+    May be supplied more than once to delete multiple SSH
+    keys in a single command execution.
+
+EXAMPLES
+--------
+Add an email and SSH key to `watcher`'s account:
+
+====
+    $ cat ~/.ssh/id_watcher.pub | ssh -p 29418 review.example.com gerrit set-account --add-ssh-key - --add-email mail@example.com watcher
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index e7cc9e4..ddbddbe 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1680,6 +1680,22 @@
 By default, 1.
 
 
+[[plugins]]Section plugins
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+[[plugins.checkFrequency]]plugins.checkFrequency::
++
+How often plugins should be examined for new plugins to load, removed
+plugins to be unloaded, or updated plugins to be reloaded.  Values can
+be specified using standard time unit abbreviations ('ms', 'sec',
+'min', etc.).
++
+If set to 0, automatic plugin reloading is disabled.  Administrators
+may force reloading with link:cmd-plugin.html[gerrit plugin reload].
++
+Default is 1 minute.
+
+
 [[receive]]Section receive
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 This section is used to set who can execute the 'receive-pack' and
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 2609b05..065e9d1 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -150,7 +150,7 @@
     should be before the instance members.
   * Annotations should go before language keywords (final, private...) +
     Example: @Assisted @Nullable final type varName
-  * Imports should be mostly aphabetical (uppercase sorts before
+  * Imports should be mostly alphabetical (uppercase sorts before
     all lowercase, which means classes come before packages at the
     same level).
 
@@ -164,7 +164,7 @@
 Design
 ------
 
-Here are some design level ojectives that you should keep in mind
+Here are some design level objectives that you should keep in mind
 when coding:
 
   * ORM entity objects should match exactly one row in the database.
@@ -191,6 +191,7 @@
     on slow links.  If the action buttons are disabled, they cannot
     be resubmitted and the user can see that Gerrit is still busy.
   * GWT EventBus is the new way forward.
+  * ...and so is Guava (previously known as Google Collections).
 
 
 Tests
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index e239a63..ca56da3 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -94,6 +94,16 @@
 * Change Save as to be Local file.
 
 
+Known problems
+--------------
+
+When running Gerrit under the Eclipse debugger, code that attempts
+to load Prolog code may erroneously raise ClassNotFoundException,
+claiming that classes in the `Gerrit` package can't be found. The
+error can often be resolved by rebuilding Gerrit with `mvn package`
+and restarting the debug session.
+
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
new file mode 100644
index 0000000..f79b8c0
--- /dev/null
+++ b/Documentation/dev-plugins.txt
@@ -0,0 +1,40 @@
+Gerrit Code Review - Plugin Development
+=======================================
+
+A plugin in gerrit is tightly coupled code that runs in the same
+JVM as gerrit. It has full access to all gerrit internals. Plugins
+are coupled to a specific major.minor gerrit version.
+
+REQUIREMENTS
+------------
+
+To start development, you may download the sample maven project, which downloads
+the following dependencies;
+
+* gerrit-sdk.jar file that matches the war file you are developing against
+
+
+Manifest
+--------
+
+Plugins need to include the following data in the jar manifest file;
+Gerrit-Plugin = plugin_name
+Gerrit-Module = pkg.class
+
+SSH Commands
+------------
+
+You may develop plugins which provide commands that can be accessed through the SSH interface.
+These commands register themselves as a part of SSH Commands (link).
+
+Each of your plugins commands needs to extend BaseCommand.
+
+Any plugin which implements at least one ssh command needs to also provide a class which extends
+the PluginCommandModule in order to register the ssh command(s) in its configure method which you
+must override.
+
+Registering is done by calling the command(String commandName).to(ClassName<? extends BaseCommand> klass)
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index f439504..83c47f3 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -114,6 +114,12 @@
 ** new war: [release-candidate], featured...
 ** old war: deprecated
 
+Plugin API JAR File
+~~~~~~~~~~~~~~~~~~~
+
+* Push JAR to commondatastorage.googleapis.com
+** Run tools/deploy_plugin_api.sh
+
 Tag
 ~~~
 
diff --git a/Documentation/install.txt b/Documentation/install.txt
index b90bbce..9cec83d 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -3,7 +3,7 @@
 
 [[requirements]]
 Requirements
------------
+------------
 To run the Gerrit service, the following requirements must be met on
 the host:
 
@@ -222,6 +222,13 @@
 * http://www.kernel.org/pub/software/scm/git/docs/git-daemon.html[man git-daemon]
 
 
+[[plugins]]
+Plugins
+-------
+
+Place Gerrit plugins in the review_site/plugins directory to have them loaded on Gerrit startup.
+
+
 External Documentation Links
 ----------------------------
 
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 890c964..4fd6b2f 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -16,9 +16,10 @@
 |All > Open           | status:open '(or is:open)'
 |All > Merged         | status:merged
 |All > Abandoned      | status:abandoned
-|My > Dafts           | has:draft
+|My > Drafts          | is:draft
 |My > Watched Changes | status:open is:watched
 |My > Starred Changes | is:starred
+|My > Draft Comments  | has:draft
 |Open changes in Foo  | status:open project:Foo
 |=================================================
 
@@ -230,6 +231,10 @@
 +
 True if the change is other open or submitted, merge pending.
 
+is:draft::
++
+True if the change is a draft.
+
 is:closed::
 +
 True if the change is either merged or abandoned.
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.txt b/ReleaseNotes/ReleaseNotes-2.2.2.txt
index 3f1f76f..ddfe323 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.txt
@@ -33,7 +33,7 @@
 +
 Projects now inherit the prolog rules defined in their parent
 project. Submit results from the child project are filtered by the
-parent project using the filter predicate defined the parent's
+parent project using the filter predicate defined in the parent's
 rules.pl. The results of the filtering are then passed up to the
 parent's parent and filtered, repeating this process up to the top
 level All-Projects.
@@ -56,7 +56,7 @@
 * prolog-shell: Simple command line Prolog interpreter
 +
 Define a small interactive interpreter that users or site
-administartors can play around with by downloading the Gerrit WAR
+administrators can play around with by downloading the Gerrit WAR
 file and executing: java -jar gerrit.war prolog-shell
 
 Prolog Predicates
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index 71df400..25f4f22a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -59,6 +59,10 @@
     return "/admin/projects/" + p.get() + ",access";
   }
 
+  public static String toAccountQuery(final String fullname) {
+    return "/q/owner:\"" + KeyUtil.encode(fullname) + "\"," + TOP;
+  }
+
   public static String toAccountDashboard(final AccountInfo acct) {
     return toAccountDashboard(acct.getId());
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
index f818d7b..20261de 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -30,6 +30,7 @@
   public static final String PUSH_MERGE = "pushMerge";
   public static final String PUSH_TAG = "pushTag";
   public static final String READ = "read";
+  public static final String REBASE = "rebase";
   public static final String SUBMIT = "submit";
 
   private static final List<String> NAMES_LC;
@@ -47,6 +48,7 @@
     NAMES_LC.add(PUSH_MERGE.toLowerCase());
     NAMES_LC.add(PUSH_TAG.toLowerCase());
     NAMES_LC.add(LABEL.toLowerCase());
+    NAMES_LC.add(REBASE.toLowerCase());
     NAMES_LC.add(SUBMIT.toLowerCase());
 
     labelIndex = NAMES_LC.indexOf(Permission.LABEL);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
index 001f9b4..28cf49b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
@@ -76,7 +76,10 @@
       NOT_A_DRAFT,
 
       /** Error writing change to git repository */
-      GIT_ERROR
+      GIT_ERROR,
+
+      /** The destination branch does not exist */
+      DEST_BRANCH_NOT_FOUND
     }
 
     protected Type type;
diff --git a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
index c25c381..f4e85ba 100644
--- a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
+++ b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
@@ -94,6 +94,7 @@
     this.caches = new HashMap<String, CacheProvider<?, ?>>();
   }
 
+  @SuppressWarnings({"rawtypes", "unchecked"})
   private void start() {
     synchronized (lock) {
       if (manager != null) {
diff --git a/gerrit-extension-api/.gitignore b/gerrit-extension-api/.gitignore
new file mode 100644
index 0000000..4e1ec9c
--- /dev/null
+++ b/gerrit-extension-api/.gitignore
@@ -0,0 +1,6 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-extension-api.iml
diff --git a/gerrit-extension-api/.settings/org.eclipse.core.resources.prefs b/gerrit-extension-api/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..fc11c3f
--- /dev/null
+++ b/gerrit-extension-api/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,5 @@
+#Thu Jul 28 11:02:36 PDT 2011
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/test/java=UTF-8
+encoding/<project>=UTF-8
diff --git a/gerrit-extension-api/.settings/org.eclipse.core.runtime.prefs b/gerrit-extension-api/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000..8667cfd
--- /dev/null
+++ b/gerrit-extension-api/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,3 @@
+#Tue Sep 02 16:59:24 PDT 2008
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs b/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..470942d
--- /dev/null
+++ b/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,269 @@
+#Thu Jul 28 11:02:36 PDT 2011
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assignment=16
+org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=0
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=0
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=2
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=true
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
+org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.lineSplit=80
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=3
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=false
+org.eclipse.jdt.core.formatter.tabulation.char=space
+org.eclipse.jdt.core.formatter.tabulation.size=2
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
diff --git a/gerrit-extension-api/.settings/org.eclipse.jdt.ui.prefs b/gerrit-extension-api/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..d4218a5
--- /dev/null
+++ b/gerrit-extension-api/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,61 @@
+#Wed Jul 29 11:31:38 PDT 2009
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+formatter_profile=_Google Format
+formatter_settings_version=11
+org.eclipse.jdt.ui.ignorelowercasenames=true
+org.eclipse.jdt.ui.importorder=com.google;com;junit;net;org;java;javax;
+org.eclipse.jdt.ui.ondemandthreshold=99
+org.eclipse.jdt.ui.staticondemandthreshold=99
+org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/>
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=false
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.format_source_code=false
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.make_local_variable_final=true
+sp_cleanup.make_parameters_final=true
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=false
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_trailing_whitespaces=true
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_casts=false
+sp_cleanup.remove_unnecessary_nls_tags=false
+sp_cleanup.remove_unused_imports=false
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_blocks=false
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
new file mode 100644
index 0000000..0209f3f
--- /dev/null
+++ b/gerrit-extension-api/pom.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>com.google.gerrit</groupId>
+    <artifactId>gerrit-parent</artifactId>
+    <version>2.5-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>gerrit-extension-api</artifactId>
+  <name>Gerrit Code Review - Extension API</name>
+
+  <description>
+    Interfaces describing the extension API
+  </description>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.inject</groupId>
+      <artifactId>guice</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.inject.extensions</groupId>
+      <artifactId>guice-servlet</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.tomcat</groupId>
+      <artifactId>servlet-api</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <configuration>
+          <createSourcesJar>true</createSourcesJar>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java
new file mode 100644
index 0000000..4811e407
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.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.extensions.annotations;
+
+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;
+
+/**
+ * Annotation applied to auto-registered, exported types.
+ * <p>
+ * Plugins or extensions using auto-registration should apply this annotation to
+ * any non-abstract class they want exported for access.
+ * <p>
+ * For SSH commands the @Export annotation names the subcommand:
+ *
+ * <pre>
+ *   @Export("print")
+ *   class MyCommand extends SshCommand {
+ * </pre>
+ *
+ * For HTTP servlets, the @Export annotation names the URL the servlet is bound
+ * to, relative to the plugin or extension's namespace within the Gerrit
+ * container.
+ *
+ * <pre>
+ *  @Export("/index.html")
+ *  class ShowIndexHtml extends HttpServlet {
+ * </pre>
+ */
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface Export {
+  String value();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java
new file mode 100644
index 0000000..a3e72bc
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.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.extensions.annotations;
+
+import java.io.Serializable;
+import java.lang.annotation.Annotation;
+
+final class ExportImpl implements Export, Serializable {
+  private static final long serialVersionUID = 0;
+  private final String value;
+
+  ExportImpl(String value) {
+    this.value = value;
+  }
+
+  @Override
+  public Class<? extends Annotation> annotationType() {
+    return Export.class;
+  }
+
+  @Override
+  public String value() {
+    return value;
+  }
+
+  @Override
+  public int hashCode() {
+    return (127 * "value".hashCode()) ^ value.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return o instanceof Export && value.equals(((Export) o).value());
+  }
+
+  @Override
+  public String toString() {
+    return "@" + Export.class.getName() + "(value=" + value + ")";
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
new file mode 100644
index 0000000..c48bcfb
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
@@ -0,0 +1,26 @@
+// 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.extensions.annotations;
+
+/** Static constructors for {@link Export} annotations. */
+public final class Exports {
+  /** Create an annotation to export under a specific name. */
+  public static Export named(String name) {
+    return new ExportImpl(name);
+  }
+
+  private Exports() {
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExtensionPoint.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExtensionPoint.java
new file mode 100644
index 0000000..4799f5e
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExtensionPoint.java
@@ -0,0 +1,41 @@
+// 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.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for interfaces that accept auto-registered implementations.
+ * <p>
+ * Interfaces that accept automatically registered implementations into their
+ * {@link DynamicSet} must be tagged with this annotation.
+ * <p>
+ * Plugins or extensions that implement an {@code @ExtensionPoint} interface
+ * should use the {@link Listen} annotation to automatically register.
+ *
+ * @see Listen
+ */
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface ExtensionPoint {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Listen.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Listen.java
new file mode 100644
index 0000000..e4ba931
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Listen.java
@@ -0,0 +1,39 @@
+// 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.extensions.annotations;
+
+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;
+
+/**
+ * Annotation for auto-registered extension point implementations.
+ * <p>
+ * Plugins or extensions using auto-registration should apply this annotation to
+ * any non-abstract class that implements an unnamed extension point, such as a
+ * notification listener. Gerrit will automatically determine which extension
+ * points to apply based on the interfaces the type implements.
+ *
+ * @see Export
+ */
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface Listen {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginName.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginName.java
new file mode 100644
index 0000000..672bab2
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginName.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.extensions.annotations;
+
+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;
+
+/**
+ * Annotation applied to a String containing the plugin or extension name.
+ * <p>
+ * A plugin or extension may receive this string by Guice injection to discover
+ * the name that an administrator has installed the plugin or extension under:
+ *
+ * <pre>
+ *  @Inject
+ *  MyType(@PluginName String myName) {
+ *  ...
+ *  }
+ * </pre>
+ */
+@Target({ElementType.PARAMETER, ElementType.FIELD})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface PluginName {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
new file mode 100644
index 0000000..8cac117
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -0,0 +1,155 @@
+// 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.extensions.registration;
+
+import com.google.inject.Binder;
+import com.google.inject.Key;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import com.google.inject.util.Types;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * A map of members that can be modified as plugins reload.
+ * <p>
+ * Maps index their members by plugin name and export name.
+ * <p>
+ * DynamicMaps are always mapped as singletons in Guice, and only may contain
+ * singletons, as providers are resolved to an instance before the member is
+ * added to the map.
+ */
+public abstract class DynamicMap<T> {
+  /**
+   * Declare a singleton {@code DynamicMap<T>} with a binder.
+   * <p>
+   * Maps must be defined in a Guice module before they can be bound:
+   *
+   * <pre>
+   * DynamicMap.mapOf(binder(), Interface.class);
+   * bind(Interface.class)
+   *   .annotatedWith(Exports.named(&quot;foo&quot;))
+   *   .to(Impl.class);
+   * </pre>
+   *
+   * @param binder a new binder created in the module.
+   * @param member type of value in the map.
+   */
+  public static <T> void mapOf(Binder binder, Class<T> member) {
+    mapOf(binder, TypeLiteral.get(member));
+  }
+
+  /**
+   * Declare a singleton {@code DynamicMap<T>} with a binder.
+   * <p>
+   * Maps must be defined in a Guice module before they can be bound:
+   *
+   * <pre>
+   * DynamicMap.mapOf(binder(), new TypeLiteral<Thing<Bar>>(){});
+   * bind(new TypeLiteral<Thing<Bar>>() {})
+   *   .annotatedWith(Exports.named(&quot;foo&quot;))
+   *   .to(Impl.class);
+   * </pre>
+   *
+   * @param binder a new binder created in the module.
+   * @param member type of value in the map.
+   */
+  public static <T> void mapOf(Binder binder, TypeLiteral<T> member) {
+    @SuppressWarnings("unchecked")
+    Key<DynamicMap<T>> key = (Key<DynamicMap<T>>) Key.get(
+        Types.newParameterizedType(DynamicMap.class, member.getType()));
+    binder.bind(key)
+        .toProvider(new DynamicMapProvider<T>(member))
+        .in(Scopes.SINGLETON);
+  }
+
+  final ConcurrentMap<NamePair, T> items;
+
+  DynamicMap() {
+    items = new ConcurrentHashMap<NamePair, T>(16, 0.75f, 1);
+  }
+
+  /**
+   * Lookup an implementation by name.
+   *
+   * @param pluginName local name of the plugin providing the item.
+   * @param exportName name the plugin exports the item as.
+   * @return the implementation. Null if the plugin is not running, or if the
+   *         plugin does not export this name.
+   */
+  public T get(String pluginName, String exportName) {
+    return items.get(new NamePair(pluginName, exportName));
+  }
+
+  /**
+   * Get the names of all running plugins supplying this type.
+   *
+   * @return sorted set of active plugins that supply at least one item.
+   */
+  public SortedSet<String> plugins() {
+    SortedSet<String> r = new TreeSet<String>();
+    for (NamePair p : items.keySet()) {
+      r.add(p.pluginName);
+    }
+    return Collections.unmodifiableSortedSet(r);
+  }
+
+  /**
+   * Get the items exported by a single plugin.
+   *
+   * @param pluginName name of the plugin.
+   * @return items exported by a plugin, keyed by the export name.
+   */
+  public SortedMap<String, T> byPlugin(String pluginName) {
+    SortedMap<String, T> r = new TreeMap<String, T>();
+    for (Map.Entry<NamePair, T> e : items.entrySet()) {
+      if (e.getKey().pluginName.equals(pluginName)) {
+        r.put(e.getKey().exportName, e.getValue());
+      }
+    }
+    return Collections.unmodifiableSortedMap(r);
+  }
+
+  static class NamePair {
+    private final String pluginName;
+    private final String exportName;
+
+    NamePair(String pn, String en) {
+      this.pluginName = pn;
+      this.exportName = en;
+    }
+
+    @Override
+    public int hashCode() {
+      return pluginName.hashCode() * 31 + exportName.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (other instanceof NamePair) {
+        NamePair np = (NamePair) other;
+        return pluginName.equals(np.pluginName) && exportName.equals(np.exportName);
+      }
+      return false;
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
new file mode 100644
index 0000000..d771d13
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
@@ -0,0 +1,46 @@
+// 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.extensions.registration;
+
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+
+import java.util.List;
+
+class DynamicMapProvider<T> implements Provider<DynamicMap<T>> {
+  private final TypeLiteral<T> type;
+
+  @Inject
+  private Injector injector;
+
+  DynamicMapProvider(TypeLiteral<T> type) {
+    this.type = type;
+  }
+
+  public DynamicMap<T> get() {
+    PrivateInternals_DynamicMapImpl<T> m =
+        new PrivateInternals_DynamicMapImpl<T>();
+    List<Binding<T>> bindings = injector.findBindingsByType(type);
+    if (bindings != null) {
+      for (Binding<T> b : bindings) {
+        m.put("gerrit", b.getKey(), b.getProvider().get());
+      }
+    }
+    return m;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
new file mode 100644
index 0000000..7f46ad4
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -0,0 +1,231 @@
+// 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.extensions.registration;
+
+import com.google.inject.Binder;
+import com.google.inject.Key;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.internal.UniqueAnnotations;
+import com.google.inject.name.Named;
+import com.google.inject.util.Types;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A set of members that can be modified as plugins reload.
+ * <p>
+ * DynamicSets are always mapped as singletons in Guice, and only may contain
+ * singletons, as providers are resolved to an instance before the member is
+ * added to the set.
+ */
+public class DynamicSet<T> implements Iterable<T> {
+  /**
+   * Declare a singleton {@code DynamicSet<T>} with a binder.
+   * <p>
+   * Sets must be defined in a Guice module before they can be bound:
+   * <pre>
+   *   DynamicSet.setOf(binder(), Interface.class);
+   *   DynamicSet.bind(binder(), Interface.class).to(Impl.class);
+   * </pre>
+   *
+   * @param binder a new binder created in the module.
+   * @param member type of entry in the set.
+   */
+  public static <T> void setOf(Binder binder, Class<T> member) {
+    setOf(binder, TypeLiteral.get(member));
+  }
+
+  /**
+   * Declare a singleton {@code DynamicSet<T>} with a binder.
+   * <p>
+   * Sets must be defined in a Guice module before they can be bound:
+   * <pre>
+   *   DynamicSet.setOf(binder(), new TypeLiteral<Thing<Foo>>() {});
+   * </pre>
+   *
+   * @param binder a new binder created in the module.
+   * @param member type of entry in the set.
+   */
+  public static <T> void setOf(Binder binder, TypeLiteral<T> member) {
+    @SuppressWarnings("unchecked")
+    Key<DynamicSet<T>> key = (Key<DynamicSet<T>>) Key.get(
+        Types.newParameterizedType(DynamicSet.class, member.getType()));
+    binder.bind(key)
+      .toProvider(new DynamicSetProvider<T>(member))
+      .in(Scopes.SINGLETON);
+  }
+
+  /**
+   * Bind one implementation into the set using a unique annotation.
+   *
+   * @param binder a new binder created in the module.
+   * @param type type of entries in the set.
+   * @return a binder to continue configuring the new set member.
+   */
+  public static <T> LinkedBindingBuilder<T> bind(Binder binder, Class<T> type) {
+    return bind(binder, TypeLiteral.get(type));
+  }
+
+  /**
+   * Bind one implementation into the set using a unique annotation.
+   *
+   * @param binder a new binder created in the module.
+   * @param type type of entries in the set.
+   * @return a binder to continue configuring the new set member.
+   */
+  public static <T> LinkedBindingBuilder<T> bind(Binder binder, TypeLiteral<T> type) {
+    return binder.bind(type).annotatedWith(UniqueAnnotations.create());
+  }
+
+  /**
+   * Bind a named implementation into the set.
+   *
+   * @param binder a new binder created in the module.
+   * @param type type of entries in the set.
+   * @param name {@code @Named} annotation to apply instead of a unique
+   *        annotation.
+   * @return a binder to continue configuring the new set member.
+   */
+  public static <T> LinkedBindingBuilder<T> bind(Binder binder,
+      Class<T> type,
+      Named name) {
+    return bind(binder, TypeLiteral.get(type));
+  }
+
+  /**
+   * Bind a named implementation into the set.
+   *
+   * @param binder a new binder created in the module.
+   * @param type type of entries in the set.
+   * @param name {@code @Named} annotation to apply instead of a unique
+   *        annotation.
+   * @return a binder to continue configuring the new set member.
+   */
+  public static <T> LinkedBindingBuilder<T> bind(Binder binder,
+      TypeLiteral<T> type,
+      Named name) {
+    return binder.bind(type).annotatedWith(name);
+  }
+
+  private final CopyOnWriteArrayList<AtomicReference<T>> items;
+
+  DynamicSet(Collection<AtomicReference<T>> base) {
+    items = new CopyOnWriteArrayList<AtomicReference<T>>(base);
+  }
+
+  @Override
+  public Iterator<T> iterator() {
+    final Iterator<AtomicReference<T>> itr = items.iterator();
+    return new Iterator<T>() {
+      private T next;
+
+      @Override
+      public boolean hasNext() {
+        while (next == null && itr.hasNext()) {
+          next = itr.next().get();
+        }
+        return next != null;
+      }
+
+      @Override
+      public T next() {
+        if (hasNext()) {
+          T result = next;
+          next = null;
+          return result;
+        }
+        throw new NoSuchElementException();
+      }
+
+      @Override
+      public void remove() {
+        throw new UnsupportedOperationException();
+      }
+    };
+  }
+
+  /**
+   * Add one new element to the set.
+   *
+   * @param item the item to add to the collection. Must not be null.
+   * @return handle to remove the item at a later point in time.
+   */
+  public RegistrationHandle add(final T item) {
+    final AtomicReference<T> ref = new AtomicReference<T>(item);
+    items.add(ref);
+    return new RegistrationHandle() {
+      @Override
+      public void remove() {
+        if (ref.compareAndSet(item, null)) {
+          items.remove(ref);
+        }
+      }
+    };
+  }
+
+  /**
+   * Add one new element that may be hot-replaceable in the future.
+   *
+   * @param key unique description from the item's Guice binding. This can be
+   *        later obtained from the registration handle to facilitate matching
+   *        with the new equivalent instance during a hot reload.
+   * @param item the item to add to the collection right now. Must not be null.
+   * @return a handle that can remove this item later, or hot-swap the item
+   *         without it ever leaving the collection.
+   */
+  public ReloadableRegistrationHandle<T> add(Key<T> key, T item) {
+    AtomicReference<T> ref = new AtomicReference<T>(item);
+    items.add(ref);
+    return new ReloadableHandle(ref, key, item);
+  }
+
+  private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
+    private final AtomicReference<T> ref;
+    private final Key<T> key;
+    private final T item;
+
+    ReloadableHandle(AtomicReference<T> ref, Key<T> key, T item) {
+      this.ref = ref;
+      this.key = key;
+      this.item = item;
+    }
+
+    @Override
+    public void remove() {
+      if (ref.compareAndSet(item, null)) {
+        items.remove(ref);
+      }
+    }
+
+    @Override
+    public Key<T> getKey() {
+      return key;
+    }
+
+    @Override
+    public ReloadableHandle replace(Key<T> newKey, T newItem) {
+      if (ref.compareAndSet(item, newItem)) {
+        return new ReloadableHandle(ref, newKey, newItem);
+      }
+      return null;
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
new file mode 100644
index 0000000..694fbd8
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
@@ -0,0 +1,56 @@
+// 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.extensions.registration;
+
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+class DynamicSetProvider<T> implements Provider<DynamicSet<T>> {
+  private final TypeLiteral<T> type;
+
+  @Inject
+  private Injector injector;
+
+  DynamicSetProvider(TypeLiteral<T> type) {
+    this.type = type;
+  }
+
+  public DynamicSet<T> get() {
+    return new DynamicSet<T>(find(injector, type));
+  }
+
+  private static <T> List<AtomicReference<T>> find(
+      Injector src,
+      TypeLiteral<T> type) {
+    List<Binding<T>> bindings = src.findBindingsByType(type);
+    int cnt = bindings != null ? bindings.size() : 0;
+    if (cnt == 0) {
+      return Collections.emptyList();
+    }
+    List<AtomicReference<T>> r = new ArrayList<AtomicReference<T>>(cnt);
+    for (Binding<T> b : bindings) {
+      r.add(new AtomicReference<T>(b.getProvider().get()));
+    }
+    return r;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
new file mode 100644
index 0000000..0ce4014
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
@@ -0,0 +1,96 @@
+// 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.extensions.registration;
+
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.inject.Key;
+
+/** <b>DO NOT USE</b> */
+public class PrivateInternals_DynamicMapImpl<T> extends DynamicMap<T> {
+  PrivateInternals_DynamicMapImpl() {
+  }
+
+  /**
+   * Store one new element into the map.
+   *
+   * @param pluginName unique name of the plugin providing the export.
+   * @param exportName name the plugin has exported the item as.
+   * @param item the item to add to the collection. Must not be null.
+   * @return handle to remove the item at a later point in time.
+   */
+  public RegistrationHandle put(
+      String pluginName, String exportName,
+      final T item) {
+    final NamePair key = new NamePair(pluginName, exportName);
+    items.put(key, item);
+    return new RegistrationHandle() {
+      @Override
+      public void remove() {
+        items.remove(key, item);
+      }
+    };
+  }
+
+  /**
+   * Store one new element that may be hot-replaceable in the future.
+   *
+   * @param pluginName unique name of the plugin providing the export.
+   * @param key unique description from the item's Guice binding. This can be
+   *        later obtained from the registration handle to facilitate matching
+   *        with the new equivalent instance during a hot reload. The key must
+   *        use an {@link @Export} annotation.
+   * @param item the item to add to the collection right now. Must not be null.
+   * @return a handle that can remove this item later, or hot-swap the item
+   *         without it ever leaving the collection.
+   */
+  public ReloadableRegistrationHandle<T> put(
+      String pluginName, Key<T> key,
+      T item) {
+    String exportName = ((Export) key.getAnnotation()).value();
+    NamePair np = new NamePair(pluginName, exportName);
+    items.put(np, item);
+    return new ReloadableHandle(np, key, item);
+  }
+
+  private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
+    private final NamePair np;
+    private final Key<T> key;
+    private final T item;
+
+    ReloadableHandle(NamePair np, Key<T> key, T item) {
+      this.np = np;
+      this.key = key;
+      this.item = item;
+    }
+
+    @Override
+    public void remove() {
+      items.remove(np, item);
+    }
+
+    @Override
+    public Key<T> getKey() {
+      return key;
+    }
+
+    @Override
+    public ReloadableHandle replace(Key<T> newKey, T newItem) {
+      if (items.replace(np, item, newItem)) {
+        return new ReloadableHandle(np, newKey, newItem);
+      }
+      return null;
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
new file mode 100644
index 0000000..2243786
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/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.extensions.registration;
+
+/** Handle for registered information. */
+public interface RegistrationHandle {
+  /** Delete this registration. */
+  public void remove();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
new file mode 100644
index 0000000..b7d78c9
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.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.extensions.registration;
+
+import com.google.inject.Key;
+
+public interface ReloadableRegistrationHandle<T> extends RegistrationHandle {
+  public Key<T> getKey();
+
+  public RegistrationHandle replace(Key<T> key, T item);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index ffa76ed..40ffc7d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -235,6 +235,10 @@
     }
 
     if (matchExact("mine,drafts", token)) {
+      return PageLinks.toChangeQuery("is:draft");
+    }
+
+    if (matchExact("mine,comments", token)) {
       return PageLinks.toChangeQuery("has:draft");
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index e578eae..f10762a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
@@ -25,9 +25,10 @@
 public class FormatUtil {
   private static final long ONE_YEAR = 182L * 24 * 60 * 60 * 1000;
 
-  private static DateTimeFormat sTime = DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.TIME_SHORT);
-  private static DateTimeFormat sDate = DateTimeFormat.getFormat("MMM d");
-  private static DateTimeFormat mDate = DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_MEDIUM);
+  private static DateTimeFormat sTime;
+  private static DateTimeFormat sDate;
+  private static DateTimeFormat sdtFmt;
+  private static DateTimeFormat mDate;
   private static DateTimeFormat dtfmt;
 
   public static void setPreferences(AccountGeneralPreferences pref) {
@@ -41,10 +42,12 @@
     }
 
     String fmt_sTime = pref.getTimeFormat().getFormat();
+    String fmt_sDate = pref.getDateFormat().getShortFormat();
     String fmt_mDate = pref.getDateFormat().getLongFormat();
 
     sTime = DateTimeFormat.getFormat(fmt_sTime);
-    sDate = DateTimeFormat.getFormat(pref.getDateFormat().getShortFormat());
+    sDate = DateTimeFormat.getFormat(fmt_sDate);
+    sdtFmt = DateTimeFormat.getFormat(fmt_sDate + " " + fmt_sTime);
     mDate = DateTimeFormat.getFormat(fmt_mDate);
     dtfmt = DateTimeFormat.getFormat(fmt_mDate + " " + fmt_sTime);
   }
@@ -75,6 +78,32 @@
     }
   }
 
+  /** Format a date using a really short format. */
+  public static String shortFormatDayTime(Date dt) {
+    if (dt == null) {
+      return "";
+    }
+
+    ensureInited();
+    final Date now = new Date();
+    dt = new Date(dt.getTime());
+    if (mDate.format(now).equals(mDate.format(dt))) {
+      // Same day as today, report only the time.
+      //
+      return sTime.format(dt);
+
+    } else if (Math.abs(now.getTime() - dt.getTime()) < ONE_YEAR) {
+      // Within the last year, show a shorter date.
+      //
+      return sdtFmt.format(dt);
+
+    } else {
+      // Report only date and year, its far away from now.
+      //
+      return mDate.format(dt);
+    }
+  }
+
   /** Format a date using the locale's medium length format. */
   public static String mediumFormat(final Date dt) {
     if (dt == null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index 6dbfeee..fd6ba2d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -548,9 +548,10 @@
     if (signedIn) {
       m = new LinkMenuBar();
       addLink(m, C.menuMyChanges(), PageLinks.MINE);
-      addLink(m, C.menuMyDrafts(), PageLinks.toChangeQuery("has:draft"));
+      addLink(m, C.menuMyDrafts(), PageLinks.toChangeQuery("is:draft"));
       addLink(m, C.menuMyWatchedChanges(), PageLinks.toChangeQuery("is:watched status:open"));
       addLink(m, C.menuMyStarredChanges(), PageLinks.toChangeQuery("is:starred"));
+      addLink(m, C.menuMyDraftComments(), PageLinks.toChangeQuery("has:draft"));
       menuLeft.add(m, C.menuMine());
       menuLeft.selectTab(1);
     } else {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
index ee107d0..f716814 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java
@@ -60,6 +60,7 @@
   String menuMyDrafts();
   String menuMyWatchedChanges();
   String menuMyStarredChanges();
+  String menuMyDraftComments();
 
   String menuDiff();
   String menuDiffCommit();
@@ -96,4 +97,5 @@
   String jumpMineDrafts();
   String jumpMineWatched();
   String jumpMineStarred();
+  String jumpMineDraftComments();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
index 41db3d5..8e3ca6c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties
@@ -43,6 +43,7 @@
 menuMyDrafts = Drafts
 menuMyStarredChanges = Starred Changes
 menuMyWatchedChanges = Watched Changes
+menuMyDraftComments = Draft Comments
 
 menuDiff = Differences
 menuDiffCommit = Commit Message
@@ -79,3 +80,4 @@
 jumpMineWatched = Go to watched changes
 jumpMineDrafts = Go to drafts
 jumpMineStarred = Go to starred changes
+jumpMineDraftComments = Go to draft comments
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
index 873045d..a41ff02 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
@@ -55,6 +55,12 @@
       jumps.add(new KeyCommand(0, 'd', Gerrit.C.jumpMineDrafts()) {
         @Override
         public void onKeyPress(final KeyPressEvent event) {
+          Gerrit.display(PageLinks.toChangeQuery("is:draft"));
+        }
+      });
+      jumps.add(new KeyCommand(0, 'c', Gerrit.C.jumpMineDraftComments()) {
+        @Override
+        public void onKeyPress(final KeyPressEvent event) {
           Gerrit.display(PageLinks.toChangeQuery("has:draft"));
         }
       });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
index b777be7..3bd2e77 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.client.account;
 
-import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.FancyFlexTable;
@@ -24,8 +23,6 @@
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 public class MyAgreementsScreen extends SettingsScreen {
   private AgreementTable agreements;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
index 936bfe5..eaf564f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
@@ -374,39 +374,41 @@
         new GerritCallback<List<AccountGroup.ExternalNameKey>>() {
           @Override
           public void onSuccess(List<AccountGroup.ExternalNameKey> result) {
-            final CellFormatter fmt = externalMatches.getCellFormatter();
+            try {
+              final CellFormatter fmt = externalMatches.getCellFormatter();
 
-            if (result.isEmpty()) {
-              externalMatches.resize(1, 1);
-              externalMatches.setText(0, 0, Util.C.errorNoMatchingGroups());
+              if (result.isEmpty()) {
+                externalMatches.resize(1, 1);
+                externalMatches.setText(0, 0, Util.C.errorNoMatchingGroups());
+                fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().header());
+                return;
+              }
+
+              externalMatches.resize(1 + result.size(), 2);
+
+              externalMatches.setText(0, 0, Util.C.columnGroupName());
+              externalMatches.setText(0, 1, "");
               fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().header());
-              return;
+              fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().header());
+
+              for (int row = 0; row < result.size(); row++) {
+                final AccountGroup.ExternalNameKey key = result.get(row);
+                final Button b = new Button(Util.C.buttonSelectGroup());
+                b.addClickHandler(new ClickHandler() {
+                  @Override
+                  public void onClick(ClickEvent event) {
+                    setExternalGroup(key);
+                  }
+                });
+                externalMatches.setText(1 + row, 0, key.get());
+                externalMatches.setWidget(1 + row, 1, b);
+                fmt.setStyleName(1 + row, 1, Gerrit.RESOURCES.css().rightmost());
+              }
+            } finally {
+              externalMatches.setVisible(true);
+              externalNameFilter.setEnabled(true);
+              externalNameSearch.setEnabled(true);
             }
-
-            externalMatches.resize(1 + result.size(), 2);
-
-            externalMatches.setText(0, 0, Util.C.columnGroupName());
-            externalMatches.setText(0, 1, "");
-            fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().header());
-            fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().header());
-
-            for (int row = 0; row < result.size(); row++) {
-              final AccountGroup.ExternalNameKey key = result.get(row);
-              final Button b = new Button(Util.C.buttonSelectGroup());
-              b.addClickHandler(new ClickHandler() {
-                @Override
-                public void onClick(ClickEvent event) {
-                  setExternalGroup(key);
-                }
-              });
-              externalMatches.setText(1 + row, 0, key.get());
-              externalMatches.setWidget(1 + row, 1, b);
-              fmt.setStyleName(1 + row, 1, Gerrit.RESOURCES.css().rightmost());
-            }
-            externalMatches.setVisible(true);
-
-            externalNameFilter.setEnabled(true);
-            externalNameSearch.setEnabled(true);
           }
 
           @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index b5cca86..4c0b1ba 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -17,8 +17,8 @@
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
+import com.google.gerrit.client.ui.AccountLink;
 import com.google.gerrit.client.ui.AddMemberBox;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
@@ -286,7 +286,7 @@
       CheckBox checkBox = new CheckBox();
       table.setWidget(row, 1, checkBox);
       checkBox.setEnabled(enabled);
-      table.setWidget(row, 2, AccountDashboardLink.link(accounts, accountId));
+      table.setWidget(row, 2, AccountLink.link(accounts, accountId));
       table.setText(row, 3, accounts.get(accountId).getPreferredEmail());
 
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 7e0edec..4330513 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -102,6 +102,7 @@
 	pushMerge, \
 	pushTag, \
 	read, \
+	rebase, \
 	submit
 create = Create Reference
 forgeAuthor = Forge Author Identity
@@ -112,6 +113,7 @@
 pushMerge = Push Merge Commit
 pushTag = Push Annotated Tag
 read = Read
+rebase = Rebase
 submit = Submit
 
 refErrorEmpty = Reference must be supplied
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
index 09716cc..c0c9ce8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.AccountLink;
 import com.google.gerrit.client.ui.AddMemberBox;
 import com.google.gerrit.client.ui.ReviewerSuggestOracle;
 import com.google.gerrit.common.data.AccountInfoCache;
@@ -129,8 +129,8 @@
     accountCache = aic;
   }
 
-  private AccountDashboardLink link(final Account.Id id) {
-    return AccountDashboardLink.link(accountCache, id);
+  private AccountLink link(final Account.Id id) {
+    return AccountLink.link(accountCache, id);
   }
 
   void display(ChangeDetail detail) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
index f8373cc..865e389 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
@@ -17,7 +17,7 @@
 import static com.google.gerrit.client.FormatUtil.mediumFormat;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.AccountLink;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.ProjectLink;
@@ -92,7 +92,7 @@
     changeIdLabel.setPreviewText(chg.getKey().get());
     table.setWidget(R_CHANGE_ID, 1, changeIdLabel);
 
-    table.setWidget(R_OWNER, 1, AccountDashboardLink.link(acc, chg.getOwner()));
+    table.setWidget(R_OWNER, 1, AccountLink.link(acc, chg.getOwner()));
     table.setWidget(R_PROJECT, 1, new ProjectLink(chg.getProject(), chg.getStatus()));
     table.setWidget(R_BRANCH, 1, new BranchLink(dst.getShortName(), chg
         .getProject(), chg.getStatus(), dst.get(), null));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 19a770e..44a49a8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.AccountLink;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.NavigationTable;
@@ -226,8 +226,8 @@
     setRowItem(row, c);
   }
 
-  private AccountDashboardLink link(final Account.Id id) {
-    return AccountDashboardLink.link(accountCache, id);
+  private AccountLink link(final Account.Id id) {
+    return AccountLink.link(accountCache, id);
   }
 
   public void addSection(final Section s) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
index 1372aa2..1b9db39 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
+import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
 import com.google.gerrit.client.ui.ProjectLink;
@@ -209,7 +210,9 @@
     if (c.owner() != null && c.owner().name() != null) {
       owner = c.owner().name();
     }
-    table.setText(row, C_OWNER, owner);
+
+    table.setWidget(row, C_OWNER, new InlineHyperlink(owner,
+        PageLinks.toAccountQuery(owner)));
 
     table.setWidget(
         row, C_PROJECT, new ProjectLink(c.project_name_key(), c.status()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
index 81b4f14..8b86d50 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
@@ -20,9 +20,9 @@
 import com.google.gerrit.client.GitwebLink;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
 import com.google.gerrit.client.ui.CommentedActionDialog;
 import com.google.gerrit.client.ui.ComplexDisclosurePanel;
+import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.ChangeDetail;
@@ -383,7 +383,8 @@
     if (who.getName() != null) {
       final Account.Id aId = who.getAccount();
       if (aId != null) {
-        fp.add(new AccountDashboardLink(who.getName(), aId));
+        fp.add(new InlineHyperlink(who.getName(), PageLinks.toAccountQuery(who
+            .getName())));
       } else {
         final InlineLabel lbl = new InlineLabel(who.getName());
         lbl.setStyleName(Gerrit.RESOURCES.css().accountName());
@@ -437,16 +438,10 @@
         public void onClick(final ClickEvent event) {
           b.setEnabled(false);
           Util.MANAGE_SVC.submit(patchSet.getId(),
-              new GerritCallback<ChangeDetail>() {
+              new ChangeDetailCache.GerritWidgetCallback(b) {
                 public void onSuccess(ChangeDetail result) {
                   onSubmitResult(result);
                 }
-
-                @Override
-                public void onFailure(Throwable caught) {
-                  b.setEnabled(true);
-                  super.onFailure(caught);
-                }
               });
         }
       });
@@ -611,17 +606,7 @@
       public void onClick(final ClickEvent event) {
         b.setEnabled(false);
         Util.MANAGE_SVC.publish(patchSet.getId(),
-            new GerritCallback<ChangeDetail>() {
-              public void onSuccess(ChangeDetail result) {
-                detailCache.set(result);
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                b.setEnabled(true);
-                super.onFailure(caught);
-              }
-            });
+            new ChangeDetailCache.GerritWidgetCallback(b));
       }
     });
     actionsPanel.add(b);
@@ -634,7 +619,7 @@
       public void onClick(final ClickEvent event) {
         b.setEnabled(false);
         PatchUtil.DETAIL_SVC.deleteDraftPatchSet(patchSet.getId(),
-            new GerritCallback<ChangeDetail>() {
+            new ChangeDetailCache.GerritWidgetCallback(b) {
               public void onSuccess(final ChangeDetail result) {
                 if (result != null) {
                   detailCache.set(result);
@@ -642,12 +627,6 @@
                   Gerrit.display(PageLinks.MINE);
                 }
               }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                b.setEnabled(true);
-                super.onFailure(caught);
-              }
             });
       }
     });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
similarity index 60%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
index 5233a6b..a4f4509 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2008 The Android Open Source Project
+// 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.
@@ -16,41 +16,39 @@
 
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.AccountDashboardScreen;
+import com.google.gerrit.client.changes.QueryScreen;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.reviewdb.client.Account;
 
 /** Link to any user's account dashboard. */
-public class AccountDashboardLink extends InlineHyperlink {
+public class AccountLink extends InlineHyperlink {
   /** Create a link after locating account details from an active cache. */
-  public static AccountDashboardLink link(final AccountInfoCache cache,
+  public static AccountLink link(final AccountInfoCache cache,
       final Account.Id id) {
     final AccountInfo ai = cache.get(id);
-    return ai != null ? new AccountDashboardLink(ai) : null;
+    return ai != null ? new AccountLink(ai) : null;
   }
 
-  private Account.Id accountId;
+  private final String query;
 
-  public AccountDashboardLink(final AccountInfo ai) {
+  public AccountLink(final AccountInfo ai) {
     this(FormatUtil.name(ai), ai);
   }
 
-  public AccountDashboardLink(final String text, final AccountInfo ai) {
-    this(text, ai.getId());
+  public AccountLink(final String text, final AccountInfo ai) {
+    super(text, PageLinks.toAccountQuery(FormatUtil.name(ai)));
     setTitle(FormatUtil.nameEmail(ai));
+    this.query = "owner:\"" + FormatUtil.name(ai) + "\"";
   }
 
-  public AccountDashboardLink(final String text, final Account.Id ai) {
-    super(text, PageLinks.toAccountDashboard(ai));
-    addStyleName(Gerrit.RESOURCES.css().accountName());
-    accountId = ai;
+  private Screen createScreen() {
+    return QueryScreen.forQuery(query);
   }
 
   @Override
   public void go() {
-    Gerrit.display(getTargetHistoryToken(), //
-        new AccountDashboardScreen(accountId));
+    Gerrit.display(getTargetHistoryToken(), createScreen());
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
index 0cea2c7..ddd2b27 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
@@ -61,7 +61,7 @@
 
     setMessageText(message);
     setAuthorNameText(FormatUtil.name(author));
-    setDateText(FormatUtil.shortFormat(when));
+    setDateText(FormatUtil.shortFormatDayTime(when));
 
     final CellFormatter fmt = header.getCellFormatter();
     fmt.getElement(0, 0).setTitle(FormatUtil.nameEmail(author));
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
new file mode 100644
index 0000000..2d957f2
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
@@ -0,0 +1,69 @@
+// 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.httpd.plugins;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
+import com.google.inject.servlet.ServletModule;
+
+import java.util.Map;
+
+import javax.servlet.http.HttpServlet;
+
+class HttpAutoRegisterModuleGenerator extends ServletModule
+    implements ModuleGenerator {
+  private final Map<String, Class<HttpServlet>> serve = Maps.newHashMap();
+
+  @Override
+  protected void configureServlets() {
+    for (Map.Entry<String, Class<HttpServlet>> e : serve.entrySet()) {
+      bind(e.getValue()).in(Scopes.SINGLETON);
+      serve(e.getKey()).with(e.getValue());
+    }
+  }
+
+  @Override
+  public void setPluginName(String name) {
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void export(Export export, Class<?> type)
+      throws InvalidPluginException {
+    if (HttpServlet.class.isAssignableFrom(type)) {
+      Class<HttpServlet> old = serve.get(export.value());
+      if (old != null) {
+        throw new InvalidPluginException(String.format(
+            "@Export(\"%s\") has duplicate bindings:\n  %s\n  %s",
+            export.value(), old.getName(), type.getName()));
+      }
+      serve.put(export.value(), (Class<HttpServlet>) type);
+    } else {
+      throw new InvalidPluginException(String.format(
+          "Class %s with @Export(\"%s\") must extend %s",
+          type.getName(), export.value(),
+          HttpServlet.class.getName()));
+    }
+  }
+
+  @Override
+  public Module create() throws InvalidPluginException {
+    return this;
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
new file mode 100644
index 0000000..2e5001b
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
@@ -0,0 +1,40 @@
+// 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.httpd.plugins;
+
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
+import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.inject.internal.UniqueAnnotations;
+import com.google.inject.servlet.ServletModule;
+
+public class HttpPluginModule extends ServletModule {
+  @Override
+  protected void configureServlets() {
+    bind(HttpPluginServlet.class);
+    serve("/plugins/*").with(HttpPluginServlet.class);
+
+    bind(StartPluginListener.class)
+      .annotatedWith(UniqueAnnotations.create())
+      .to(HttpPluginServlet.class);
+
+    bind(ReloadPluginListener.class)
+      .annotatedWith(UniqueAnnotations.create())
+      .to(HttpPluginServlet.class);
+
+    bind(ModuleGenerator.class)
+      .to(HttpAutoRegisterModuleGenerator.class);
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
new file mode 100644
index 0000000..23dbaac
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -0,0 +1,280 @@
+// 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.httpd.plugins;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.MimeUtilFileTypeRegistry;
+import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
+import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.servlet.GuiceFilter;
+
+import eu.medsea.mimeutil.MimeType;
+
+import org.eclipse.jgit.util.IO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.concurrent.ConcurrentMap;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+class HttpPluginServlet extends HttpServlet
+    implements StartPluginListener, ReloadPluginListener {
+  private static final long serialVersionUID = 1L;
+  private static final Logger log
+      = LoggerFactory.getLogger(HttpPluginServlet.class);
+
+  private final MimeUtilFileTypeRegistry mimeUtil;
+  private List<Plugin> pending = Lists.newArrayList();
+  private String base;
+  private final ConcurrentMap<String, PluginHolder> plugins
+      = Maps.newConcurrentMap();
+
+  @Inject
+  HttpPluginServlet(MimeUtilFileTypeRegistry mimeUtil) {
+    this.mimeUtil = mimeUtil;
+  }
+
+  @Override
+  public synchronized void init(ServletConfig config) throws ServletException {
+    super.init(config);
+
+    String path = config.getServletContext().getContextPath();
+    base = Strings.nullToEmpty(path) + "/plugins/";
+    for (Plugin plugin : pending) {
+      install(plugin);
+    }
+    pending = null;
+  }
+
+  @Override
+  public synchronized void onStartPlugin(Plugin plugin) {
+    if (pending != null) {
+      pending.add(plugin);
+    } else {
+      install(plugin);
+    }
+  }
+
+  @Override
+  public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
+    install(newPlugin);
+  }
+
+  private void install(Plugin plugin) {
+    GuiceFilter filter = load(plugin);
+    final String name = plugin.getName();
+    final PluginHolder holder = new PluginHolder(plugin, filter);
+    plugin.add(new RegistrationHandle() {
+      @Override
+      public void remove() {
+        plugins.remove(name, holder);
+      }
+    });
+    plugins.put(name, holder);
+  }
+
+  private GuiceFilter load(Plugin plugin) {
+    if (plugin.getHttpInjector() != null) {
+      final String name = plugin.getName();
+      final GuiceFilter filter;
+      try {
+        filter = plugin.getHttpInjector().getInstance(GuiceFilter.class);
+      } catch (RuntimeException e) {
+        log.warn(String.format("Plugin %s cannot load GuiceFilter", name), e);
+        return null;
+      }
+
+      try {
+        WrappedContext ctx = new WrappedContext(plugin, base + name);
+        filter.init(new WrappedFilterConfig(ctx));
+      } catch (ServletException e) {
+        log.warn(String.format("Plugin %s failed to initialize HTTP", name), e);
+        return null;
+      }
+
+      plugin.add(new RegistrationHandle() {
+        @Override
+        public void remove() {
+          filter.destroy();
+        }
+      });
+      return filter;
+    }
+    return null;
+  }
+
+  @Override
+  public void service(HttpServletRequest req, HttpServletResponse res)
+      throws IOException, ServletException {
+    String name = extractName(req);
+    final PluginHolder holder = plugins.get(name);
+    if (holder == null) {
+      noCache(res);
+      res.sendError(HttpServletResponse.SC_NOT_FOUND);
+      return;
+    }
+
+    WrappedRequest wr = new WrappedRequest(req, base + name);
+    FilterChain chain = new FilterChain() {
+      @Override
+      public void doFilter(ServletRequest req, ServletResponse res)
+          throws IOException {
+        onDefault(holder, (HttpServletRequest) req, (HttpServletResponse) res);
+      }
+    };
+    if (holder.filter != null) {
+      holder.filter.doFilter(wr, res, chain);
+    } else {
+      chain.doFilter(wr, res);
+    }
+  }
+
+  private void onDefault(PluginHolder holder,
+      HttpServletRequest req,
+      HttpServletResponse res) throws IOException {
+    String uri = req.getRequestURI();
+    String ctx = req.getContextPath();
+    String file = uri.substring(ctx.length() + 1);
+    if (file.startsWith("Documentation/") || file.startsWith("static/")) {
+      JarFile jar = holder.plugin.getJarFile();
+      JarEntry entry = jar.getJarEntry(file);
+      if (entry != null && entry.getSize() > 0) {
+        sendResource(jar, entry, res);
+        return;
+      }
+    }
+
+    noCache(res);
+    res.sendError(HttpServletResponse.SC_NOT_FOUND);
+  }
+
+  private void sendResource(JarFile jar, JarEntry entry, HttpServletResponse res)
+      throws IOException {
+    byte[] data = null;
+    if (entry.getSize() <= 128 * 1024) {
+      data = new byte[(int) entry.getSize()];
+      InputStream in = jar.getInputStream(entry);
+      try {
+        IO.readFully(in, data, 0, data.length);
+      } finally {
+        in.close();
+      }
+    }
+
+    String contentType = null;
+    Attributes atts = entry.getAttributes();
+    if (atts != null) {
+      contentType = Strings.emptyToNull(atts.getValue("Content-Type"));
+    }
+    if (contentType == null) {
+      MimeType type = mimeUtil.getMimeType(entry.getName(), data);
+      contentType = type.toString();
+    }
+
+    long time = entry.getTime();
+    if (0 < time) {
+      res.setDateHeader("Last-Modified", time);
+    }
+    res.setContentType(contentType);
+    res.setHeader("Content-Length", Long.toString(entry.getSize()));
+    if (data != null) {
+      res.getOutputStream().write(data);
+    } else {
+      InputStream in = jar.getInputStream(entry);
+      try {
+        OutputStream out = res.getOutputStream();
+        try {
+          byte[] tmp = new byte[1024];
+          int n;
+          while ((n = in.read(tmp)) > 0) {
+            out.write(tmp, 0, n);
+          }
+        } finally {
+          out.close();
+        }
+      } finally {
+        in.close();
+      }
+    }
+  }
+
+  private static String extractName(HttpServletRequest req) {
+    String path = req.getPathInfo();
+    if (Strings.isNullOrEmpty(path) || "/".equals(path)) {
+      return "";
+    }
+    int s = path.indexOf('/', 1);
+    return 0 <= s ? path.substring(1, s) : path.substring(1);
+  }
+
+  private static void noCache(HttpServletResponse res) {
+    res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
+    res.setHeader("Pragma", "no-cache");
+    res.setHeader("Cache-Control", "no-cache, must-revalidate");
+    res.setHeader("Content-Disposition", "attachment");
+  }
+
+  private static class PluginHolder {
+    final Plugin plugin;
+    final GuiceFilter filter;
+
+    PluginHolder(Plugin plugin, GuiceFilter filter) {
+      this.plugin = plugin;
+      this.filter = filter;
+    }
+  }
+
+  private static class WrappedRequest extends HttpServletRequestWrapper {
+    private final String contextPath;
+
+    WrappedRequest(HttpServletRequest req, String contextPath) {
+      super(req);
+      this.contextPath = contextPath;
+    }
+
+    @Override
+    public String getContextPath() {
+      return contextPath;
+    }
+
+    @Override
+    public String getServletPath() {
+      return ((HttpServletRequest) getRequest()).getRequestURI();
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java
new file mode 100644
index 0000000..daeb6ff
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java
@@ -0,0 +1,178 @@
+// 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.httpd.plugins;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.server.plugins.Plugin;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.Servlet;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+
+class WrappedContext implements ServletContext {
+  private static final Logger log = LoggerFactory.getLogger("plugin");
+  private final Plugin plugin;
+  private final String contextPath;
+  private final ConcurrentMap<String, Object> attributes;
+
+  WrappedContext(Plugin plugin, String contextPath) {
+    this.plugin = plugin;
+    this.contextPath = contextPath;
+    this.attributes = Maps.newConcurrentMap();
+  }
+
+  @Override
+  public String getContextPath() {
+    return contextPath;
+  }
+
+  @Override
+  public String getInitParameter(String name) {
+    return null;
+  }
+
+  @SuppressWarnings("rawtypes")
+  @Override
+  public Enumeration getInitParameterNames() {
+    return Collections.enumeration(Collections.emptyList());
+  }
+
+  @Override
+  public ServletContext getContext(String name) {
+    return null;
+  }
+
+  @Override
+  public RequestDispatcher getNamedDispatcher(String name) {
+    return null;
+  }
+
+  @Override
+  public RequestDispatcher getRequestDispatcher(String name) {
+    return null;
+  }
+
+  @Override
+  public URL getResource(String name) throws MalformedURLException {
+    return null;
+  }
+
+  @Override
+  public InputStream getResourceAsStream(String name) {
+    return null;
+  }
+
+  @SuppressWarnings("rawtypes")
+  @Override
+  public Set getResourcePaths(String name) {
+    return null;
+  }
+
+  @Override
+  public Servlet getServlet(String name) throws ServletException {
+    return null;
+  }
+
+  @Override
+  public String getRealPath(String name) {
+    return null;
+  }
+
+  @Override
+  public String getServletContextName() {
+    return plugin.getName();
+  }
+
+  @SuppressWarnings("rawtypes")
+  @Override
+  public Enumeration getServletNames() {
+    return Collections.enumeration(Collections.emptyList());
+  }
+
+  @SuppressWarnings("rawtypes")
+  @Override
+  public Enumeration getServlets() {
+    return Collections.enumeration(Collections.emptyList());
+  }
+
+  @Override
+  public void log(Exception reason, String msg) {
+    log(msg, reason);
+  }
+
+  @Override
+  public void log(String msg) {
+    log(msg, null);
+  }
+
+  @Override
+  public void log(String msg, Throwable reason) {
+    log.warn(String.format("[plugin %s] %s", plugin.getName(), msg), reason);
+  }
+
+  @Override
+  public Object getAttribute(String name) {
+    return attributes.get(name);
+  }
+
+  @Override
+  public Enumeration<String> getAttributeNames() {
+    return Collections.enumeration(attributes.keySet());
+  }
+
+  @Override
+  public void setAttribute(String name, Object value) {
+    attributes.put(name, value);
+  }
+
+  @Override
+  public void removeAttribute(String name) {
+    attributes.remove(name);
+  }
+
+  @Override
+  public String getMimeType(String file) {
+    return null;
+  }
+
+  @Override
+  public int getMajorVersion() {
+    return 2;
+  }
+
+  @Override
+  public int getMinorVersion() {
+    return 5;
+  }
+
+  @Override
+  public String getServerInfo() {
+    String v = Version.getVersion();
+    return "Gerrit Code Review/" + (v != null ? v : "dev");
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java
new file mode 100644
index 0000000..c9107dc
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.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.httpd.plugins;
+
+import com.google.inject.servlet.GuiceFilter;
+
+import java.util.Collections;
+import java.util.Enumeration;
+
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+
+class WrappedFilterConfig implements FilterConfig {
+  private final WrappedContext context;
+
+  WrappedFilterConfig(WrappedContext context) {
+    this.context = context;
+  }
+
+  @Override
+  public String getFilterName() {
+    return GuiceFilter.class.getName();
+  }
+
+  @Override
+  public String getInitParameter(String name) {
+    return null;
+  }
+
+  @SuppressWarnings("rawtypes")
+  @Override
+  public Enumeration getInitParameterNames() {
+    return Collections.enumeration(Collections.emptyList());
+  }
+
+  @Override
+  public ServletContext getServletContext() {
+    return context;
+  }
+}
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 3aecb0c..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
@@ -28,6 +28,10 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
 import javax.annotation.Nullable;
 
 class AbandonChangeHandler extends Handler<ChangeDetail> {
@@ -58,9 +62,10 @@
   @Override
   public ChangeDetail call() throws NoSuchChangeException, OrmException,
       EmailException, NoSuchEntityException, InvalidChangeOperationException,
-      PatchSetInfoNotAvailableException {
+      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/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index ab266f3..d765f39 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
@@ -33,8 +33,10 @@
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
@@ -46,8 +48,10 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -70,6 +74,7 @@
   private final AccountInfoCacheFactory aic;
   private final AnonymousUser anonymousUser;
   private final ReviewDb db;
+  private final GitRepositoryManager repoManager;
 
   private final Change.Id changeId;
 
@@ -84,6 +89,7 @@
   ChangeDetailFactory(final ApprovalTypes approvalTypes,
       final FunctionState.Factory functionState,
       final PatchSetDetailFactory.Factory patchSetDetail, final ReviewDb db,
+      final GitRepositoryManager repoManager,
       final ChangeControl.Factory changeControlFactory,
       final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
       final AnonymousUser anonymousUser,
@@ -94,6 +100,7 @@
     this.functionState = functionState;
     this.patchSetDetail = patchSetDetail;
     this.db = db;
+    this.repoManager = repoManager;
     this.changeControlFactory = changeControlFactory;
     this.anonymousUser = anonymousUser;
     this.aic = accountInfoCacheFactory.create();
@@ -106,7 +113,8 @@
 
   @Override
   public ChangeDetail call() throws OrmException, NoSuchEntityException,
-      PatchSetInfoNotAvailableException, NoSuchChangeException {
+      PatchSetInfoNotAvailableException, NoSuchChangeException,
+      RepositoryNotFoundException, IOException {
     control = changeControlFactory.validateFor(changeId);
     final Change change = control.getChange();
     final PatchSet patch = db.patchSets().get(change.currentPatchSetId());
@@ -122,7 +130,9 @@
 
     detail.setCanAbandon(change.getStatus() != Change.Status.DRAFT && change.getStatus().isOpen() && control.canAbandon());
     detail.setCanPublish(control.canPublish(db));
-    detail.setCanRestore(change.getStatus() == Change.Status.ABANDONED && control.canRestore());
+    detail.setCanRestore(change.getStatus() == Change.Status.ABANDONED
+        && control.canRestore()
+        && ProjectUtil.branchExists(repoManager, change.getDest()));
     detail.setCanDeleteDraft(control.canDeleteDraft(db));
     detail.setStarred(control.getCurrentUser().getStarredChanges().contains(
         changeId));
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java
index 4ea279f..f57b29c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java
@@ -26,6 +26,10 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
 class PublishAction extends Handler<ChangeDetail> {
   interface Factory {
     PublishAction create(PatchSet.Id patchSetId);
@@ -49,7 +53,7 @@
   @Override
   public ChangeDetail call() throws OrmException, NoSuchEntityException,
       IllegalStateException, PatchSetInfoNotAvailableException,
-      NoSuchChangeException {
+      NoSuchChangeException, RepositoryNotFoundException, IOException {
     final ReviewResult result = publishFactory.create(patchSetId).call();
     if (result.getErrors().size() > 0) {
       throw new IllegalStateException("Cannot publish patchset");
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 f018750..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
@@ -28,6 +28,10 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
 import javax.annotation.Nullable;
 
 class RestoreChangeHandler extends Handler<ChangeDetail> {
@@ -57,9 +61,10 @@
   @Override
   public ChangeDetail call() throws NoSuchChangeException, OrmException,
       EmailException, NoSuchEntityException, InvalidChangeOperationException,
-      PatchSetInfoNotAvailableException {
+      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-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
index 80100ad..23b21d5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
@@ -27,6 +27,10 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
 class SubmitAction extends Handler<ChangeDetail> {
   interface Factory {
     SubmitAction create(PatchSet.Id patchSetId);
@@ -50,7 +54,8 @@
   @Override
   public ChangeDetail call() throws OrmException, NoSuchEntityException,
       IllegalStateException, InvalidChangeOperationException,
-      PatchSetInfoNotAvailableException, NoSuchChangeException {
+      PatchSetInfoNotAvailableException, NoSuchChangeException,
+      RepositoryNotFoundException, IOException {
     final ReviewResult result =
         submitFactory.create(patchSetId).call();
     if (result.getErrors().size() > 0) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
index e90467e..40c9b84 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
@@ -52,6 +52,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -166,6 +169,10 @@
           throw new Failure(e);
         } catch (PatchSetInfoNotAvailableException e) {
           throw new Failure(e);
+        } catch (RepositoryNotFoundException e) {
+          throw new Failure(e);
+        } catch (IOException e) {
+          throw new Failure(e);
         }
       }
     });
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index 7f2007e..61bb52f 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -423,36 +423,42 @@
   }
 
   private static File tmproot() {
-    // Try to find the user's home directory. If we can't find it
-    // return null so the JVM's default temporary directory is used
-    // instead. This is probably /tmp or /var/tmp.
-    //
-    String userHome = System.getProperty("user.home");
-    if (userHome == null || "".equals(userHome)) {
-      userHome = System.getenv("HOME");
+    File tmp;
+    String gerritTemp = System.getenv("GERRIT_TMP");
+    if (gerritTemp != null && gerritTemp.length() > 0) {
+      tmp = new File(gerritTemp);
+    } else {
+      // Try to find the user's home directory. If we can't find it
+      // return null so the JVM's default temporary directory is used
+      // instead. This is probably /tmp or /var/tmp.
+      //
+      String userHome = System.getProperty("user.home");
       if (userHome == null || "".equals(userHome)) {
-        System.err.println("warning: cannot determine home directory");
-        System.err.println("warning: using system temporary directory instead");
-        return null;
+        userHome = System.getenv("HOME");
+        if (userHome == null || "".equals(userHome)) {
+          System.err.println("warning: cannot determine home directory");
+          System.err.println("warning: using system temporary directory instead");
+          return null;
+        }
       }
-    }
 
-    // Ensure the home directory exists. If it doesn't, try to make it.
-    //
-    final File home = new File(userHome);
-    if (!home.exists()) {
-      if (home.mkdirs()) {
-        System.err.println("warning: created " + home.getAbsolutePath());
-      } else {
-        System.err.println("warning: " + home.getAbsolutePath() + " not found");
-        System.err.println("warning: using system temporary directory instead");
-        return null;
+      // Ensure the home directory exists. If it doesn't, try to make it.
+      //
+      final File home = new File(userHome);
+      if (!home.exists()) {
+        if (home.mkdirs()) {
+          System.err.println("warning: created " + home.getAbsolutePath());
+        } else {
+          System.err.println("warning: " + home.getAbsolutePath() + " not found");
+          System.err.println("warning: using system temporary directory instead");
+          return null;
+        }
       }
-    }
 
-    // Use $HOME/.gerritcodereview/tmp for our temporary file area.
-    //
-    final File tmp = new File(new File(home, ".gerritcodereview"), "tmp");
+      // Use $HOME/.gerritcodereview/tmp for our temporary file area.
+      //
+      tmp = new File(new File(home, ".gerritcodereview"), "tmp");
+    }
     if (!tmp.exists() && !tmp.mkdirs()) {
       System.err.println("warning: cannot create " + tmp.getAbsolutePath());
       System.err.println("warning: using system temporary directory instead");
diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
index 1df89b7..9a55e0f 100644
--- a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
+++ b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
@@ -76,7 +76,7 @@
   public JsonElement serialize(final Edit src, final Type typeOfSrc,
       final JsonSerializationContext context) {
     if (src == null) {
-      return new JsonNull();
+      return JsonNull.INSTANCE;
     }
     final JsonArray a = new JsonArray();
     add(a, src);
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 25b7699..bbff5cb 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
@@ -24,6 +24,7 @@
 import com.google.gerrit.httpd.WebModule;
 import com.google.gerrit.httpd.WebSshGlueModule;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
+import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.pgm.http.jetty.GetUserFilter;
 import com.google.gerrit.pgm.http.jetty.JettyEnv;
@@ -46,6 +47,8 @@
 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;
@@ -140,6 +143,8 @@
       dbInjector = createDbInjector(MULTI_USER);
       cfgInjector = createCfgInjector();
       sysInjector = createSysInjector();
+      sysInjector.getInstance(PluginGuiceEnvironment.class)
+        .setCfgInjector(cfgInjector);
       manager.add(dbInjector, cfgInjector, sysInjector);
 
       if (sshd) {
@@ -208,6 +213,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
@@ -231,6 +237,8 @@
 
   private void initSshd() {
     sshInjector = createSshInjector();
+    sysInjector.getInstance(PluginGuiceEnvironment.class)
+        .setSshInjector(sshInjector);
     manager.add(sshInjector);
   }
 
@@ -252,6 +260,9 @@
   private void initHttpd() {
     webInjector = createWebInjector();
 
+    sysInjector.getInstance(PluginGuiceEnvironment.class)
+        .setHttpInjector(webInjector);
+
     sysInjector.getInstance(HttpCanonicalWebUrlProvider.class)
         .setHttpServletRequest(
             webInjector.getProvider(HttpServletRequest.class));
@@ -266,6 +277,7 @@
     modules.add(HttpContactStoreConnection.module());
     modules.add(sysInjector.getInstance(GitOverHttpModule.class));
     modules.add(sysInjector.getInstance(WebModule.class));
+    modules.add(new HttpPluginModule());
     if (sshd) {
       modules.add(sshInjector.getInstance(WebSshGlueModule.class));
       modules.add(new ProjectQoSFilter.Module());
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index fa5ef59..a57de3c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -40,6 +40,7 @@
 import org.eclipse.jetty.server.nio.SelectChannelConnector;
 import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
 import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.FilterHolder;
 import org.eclipse.jetty.servlet.FilterMapping;
 import org.eclipse.jetty.servlet.ServletContextHandler;
 import org.eclipse.jetty.servlet.ServletHolder;
@@ -328,7 +329,8 @@
     // of using the listener to create the injector pass the one we
     // already have built.
     //
-    app.addFilter(GuiceFilter.class, "/*", FilterMapping.DEFAULT);
+    GuiceFilter filter = env.webInjector.getInstance(GuiceFilter.class);
+    app.addFilter(new FilterHolder(filter), "/*", FilterMapping.DEFAULT);
     app.addEventListener(new GuiceServletContextListener() {
       @Override
       protected Injector getInjector() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index dae0893..d26d46e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -67,9 +67,11 @@
     mkdir(site.bin_dir);
     mkdir(site.etc_dir);
     mkdir(site.lib_dir);
+    mkdir(site.tmp_dir);
     mkdir(site.logs_dir);
     mkdir(site.mail_dir);
     mkdir(site.static_dir);
+    mkdir(site.plugins_dir);
 
     for (InitStep step : steps) {
       step.run();
@@ -84,6 +86,7 @@
 
     extract(site.gerrit_sh, Init.class, "gerrit.sh");
     chmod(0755, site.gerrit_sh);
+    chmod(0700, site.tmp_dir);
 
     extractMailExample("Abandoned.vm");
     extractMailExample("ChangeFooter.vm");
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
index 4148847..3857ebd 100755
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
@@ -176,6 +176,8 @@
 
 GERRIT_PID="$GERRIT_SITE/logs/gerrit.pid"
 GERRIT_RUN="$GERRIT_SITE/logs/gerrit.run"
+GERRIT_TMP="$GERRIT_SITE/tmp"
+export GERRIT_TMP
 
 ##################################################
 # Check for JAVA_HOME
@@ -302,7 +304,7 @@
   done
 fi
 if test -z "$GERRIT_WAR" ; then
-  echo >&2 "** ERROR: Cannot find gerrit.war (try setting gerrit.war)"
+  echo >&2 "** ERROR: Cannot find gerrit.war (try setting \$GERRIT_WAR)"
   exit 1
 fi
 
@@ -492,6 +494,7 @@
     echo "  GERRIT_SITE     =  $GERRIT_SITE"
     echo "  GERRIT_CONFIG   =  $GERRIT_CONFIG"
     echo "  GERRIT_PID      =  $GERRIT_PID"
+    echo "  GERRIT_TMP      =  $GERRIT_TMP"
     echo "  GERRIT_WAR      =  $GERRIT_WAR"
     echo "  GERRIT_FDS      =  $GERRIT_FDS"
     echo "  GERRIT_USER     =  $GERRIT_USER"
diff --git a/gerrit-plugin-api/.gitignore b/gerrit-plugin-api/.gitignore
new file mode 100644
index 0000000..574d1fc
--- /dev/null
+++ b/gerrit-plugin-api/.gitignore
@@ -0,0 +1,8 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/.settings/org.eclipse.core.resources.prefs
+/.settings/org.eclipse.jdt.core.prefs
+/gerrit-pluginapi-ssh.iml
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
new file mode 100644
index 0000000..5c4ca3449
--- /dev/null
+++ b/gerrit-plugin-api/pom.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>com.google.gerrit</groupId>
+    <artifactId>gerrit-parent</artifactId>
+    <version>2.5-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>gerrit-plugin-api</artifactId>
+  <name>Gerrit Code Review - Plugin API</name>
+
+  <description>
+    API for tightly coupled plugins to compile against
+  </description>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-sshd</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-httpd</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.tomcat</groupId>
+      <artifactId>servlet-api</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <configuration>
+          <createSourcesJar>true</createSourcesJar>
+          <artifactSet>
+            <excludes>
+              <exclude>gwtexpui:gwtexpui</exclude>
+              <exclude>gwtjsonrpc:gwtjsonrpc</exclude>
+              <exclude>com.google.gerrit:gerrit-ehcache</exclude>
+              <exclude>com.google.gerrit:gerrit-prettify</exclude>
+              <exclude>com.google.gerrit:gerrit-patch-commonsnet</exclude>
+              <exclude>com.google.gerrit:gerrit-patch-jgit</exclude>
+              <exclude>com.google.gerrit:gerrit-util-ssl</exclude>
+              <exclude>com.google.gerrit:juniversalchardet</exclude>
+
+              <exclude>com.googlecode.prolog-cafe:PrologCafe</exclude>
+              <exclude>org.slf4j:slf4j-log4j12</exclude>
+              <exclude>log4j:log4j</exclude>
+
+              <exclude>commons-collections:commons-collections</exclude>
+              <exclude>commons-codec:commons-codec</exclude>
+              <exclude>commons-dbcp:commons-dbcp</exclude>
+              <exclude>commons-lang:commons-lang</exclude>
+              <exclude>commons-net:commons-net</exclude>
+              <exclude>commons-pool:commons-pool</exclude>
+
+              <exclude>asm:asm</exclude>
+              <exclude>eu.medsea.mimeutil:mime-util</exclude>
+              <exclude>net.sf.ehcache:ehcache-core</exclude>
+              <exclude>org.antlr:antlr</exclude>
+              <exclude>org.antlr:antlr-runtime</exclude>
+              <exclude>org.apache.mina:mina-core</exclude>
+              <exclude>oro:oro</exclude>
+            </excludes>
+          </artifactSet>
+          <filters>
+            <filter>
+              <artifact>com.google.gerrit:gerrit-server</artifact>
+              <excludes>
+                <exclude>gerrit/**</exclude>
+              </excludes>
+            </filter>
+          </filters>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index f35608c..70397d8 100644
--- a/gerrit-server/pom.xml
+++ b/gerrit-server/pom.xml
@@ -123,6 +123,12 @@
 
     <dependency>
       <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-extension-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-util-cli</artifactId>
       <version>${project.version}</version>
     </dependency>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index 727207f..274e73ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.AnonymousCowardName;
 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.events.ApprovalAttribute;
 import com.google.gerrit.server.events.ChangeAbandonedEvent;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index b87cce3..556ae82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -34,6 +34,16 @@
 import java.util.List;
 import java.util.Set;
 
+/**
+ * Utility functions to manipulate patchset approvals.
+ * <p>
+ * Approvals are overloaded, they represent both approvals and reviewers
+ * which should be CCed on a change.  To ensure that reviewers are not lost
+ * there must always be an approval on each patchset for each reviewer,
+ * even if the reviewer hasn't actually given a score to the change.  To
+ * mark the "no score" case, a dummy approval, which may live in any of
+ * the available categories, with a score of 0 is used.
+ */
 public class ApprovalsUtil {
   private final ReviewDb db;
   private final ApprovalTypes approvalTypes;
@@ -65,8 +75,9 @@
    * @param change Change to update
    * @throws OrmException
    * @throws IOException
+   * @return List<PatchSetApproval> The previous approvals
    */
-  public void copyVetosToLatestPatchSet(Change change)
+  public List<PatchSetApproval> copyVetosToLatestPatchSet(Change change)
       throws OrmException, IOException {
     PatchSet.Id source;
     if (change.getNumberOfPatchSets() > 1) {
@@ -76,16 +87,20 @@
     }
 
     PatchSet.Id dest = change.currPatchSetId();
-    for (PatchSetApproval a : db.patchSetApprovals().byPatchSet(source)) {
+    List<PatchSetApproval> patchSetApprovals = db.patchSetApprovals().byChange(change.getId()).toList();
+    for (PatchSetApproval a : patchSetApprovals) {
       // ApprovalCategory.SUBMIT is still in db but not relevant in git-store
       if (!ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
         final ApprovalType type = approvalTypes.byId(a.getCategoryId());
-        if (type.getCategory().isCopyMinScore() && type.isMaxNegative(a)) {
+        if (a.getPatchSetId().equals(source) &&
+            type.getCategory().isCopyMinScore() &&
+            type.isMaxNegative(a)) {
           db.patchSetApprovals().insert(
               Collections.singleton(new PatchSetApproval(dest, a)));
         }
       }
     }
+    return patchSetApprovals;
   }
 
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 2812d39..ec28378 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -383,18 +383,12 @@
 
         replication.scheduleUpdate(change.getProject(), ru.getName());
 
-        approvalsUtil.copyVetosToLatestPatchSet(change);
-
-        final ChangeMessage cmsg =
-            new ChangeMessage(new ChangeMessage.Key(changeId,
-                ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
-        cmsg.setMessage("Patch Set " + patchSetId.get() + ": Rebased");
-        db.changeMessages().insert(Collections.singleton(cmsg));
+        List<PatchSetApproval> patchSetApprovals = approvalsUtil.copyVetosToLatestPatchSet(change);
 
         final Set<Account.Id> oldReviewers = new HashSet<Account.Id>();
         final Set<Account.Id> oldCC = new HashSet<Account.Id>();
 
-        for (PatchSetApproval a : db.patchSetApprovals().byChange(change.getId())) {
+        for (PatchSetApproval a : patchSetApprovals) {
           if (a.getValue() != 0) {
             oldReviewers.add(a.getAccountId());
           } else {
@@ -402,6 +396,12 @@
           }
         }
 
+        final ChangeMessage cmsg =
+            new ChangeMessage(new ChangeMessage.Key(changeId,
+                ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
+        cmsg.setMessage("Patch Set " + patchSetId.get() + ": Rebased");
+        db.changeMessages().insert(Collections.singleton(cmsg));
+
         final ReplacePatchSetSender cm =
             rebasedPatchSetSenderFactory.create(change);
         cm.setFrom(user.getAccountId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
new file mode 100644
index 0000000..8847f96
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
@@ -0,0 +1,48 @@
+// 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;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.server.git.GitRepositoryManager;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+public class ProjectUtil {
+
+  /**
+   * Checks whether the specified branch exists.
+   *
+   * @param repoManager Git repository manager to open the git repository
+   * @param branch the branch for which it should be checked if it exists
+   * @return <code>true</code> if the specified branch exists, otherwise
+   *         <code>false</code>
+   * @throws RepositoryNotFoundException the repository of the branch's project
+   *         does not exist.
+   * @throws IOException error while retrieving the branch from the repository.
+   */
+  public static boolean branchExists(final GitRepositoryManager repoManager,
+      final Branch.NameKey branch) throws RepositoryNotFoundException,
+      IOException {
+    final Repository repo = repoManager.openRepository(branch.getParentKey());
+    try {
+      return repo.getRef(branch.get()) != null;
+    } finally {
+      repo.close();
+    }
+  }
+}
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 7232755..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
@@ -17,12 +17,15 @@
 
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.ReviewResult;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ProjectUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.RestoredSender;
 import com.google.gerrit.server.project.ChangeControl;
@@ -33,91 +36,109 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+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;
   private final ChangeControl.Factory changeControlFactory;
   private final ReviewDb db;
+  private final GitRepositoryManager repoManager;
   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 IdentifiedUser currentUser, final ChangeHooks hooks,
-      @Assisted final PatchSet.Id patchSetId,
-      @Assisted final String changeComment) {
+      final GitRepositoryManager repoManager, final IdentifiedUser currentUser,
+      final ChangeHooks hooks, @Assisted final Change.Id changeId,
+      @Assisted @Nullable final String changeComment) {
     this.restoredSenderFactory = restoredSenderFactory;
     this.changeControlFactory = changeControlFactory;
     this.db = db;
+    this.repoManager = repoManager;
     this.currentUser = currentUser;
     this.hooks = hooks;
 
-    this.patchSetId = patchSetId;
+    this.changeId = changeId;
     this.changeComment = changeComment;
   }
 
   @Override
   public ReviewResult call() throws EmailException,
-      InvalidChangeOperationException, NoSuchChangeException, OrmException {
+      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 PatchSet patch = db.patchSets().get(patchSetId);
+    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));
-    } else if (patch == null) {
-      throw new NoSuchChangeException(changeId);
-    } else {
-
-      // Create a message to accompany the restored change
-      final ChangeMessage cmsg =
-          new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil
-              .messageUUID(db)), currentUser.getAccountId(), patchSetId);
-      final StringBuilder msgBuf =
-          new StringBuilder("Patch Set " + patchSetId.get() + ": Restored");
-      if (changeComment != null && changeComment.length() > 0) {
-        msgBuf.append("\n\n");
-        msgBuf.append(changeComment);
-      }
-      cmsg.setMessage(msgBuf.toString());
-
-      // Restore the change
-      final Change updatedChange = db.changes().atomicUpdate(changeId,
-          new AtomicUpdate<Change>() {
-        @Override
-        public Change update(Change change) {
-          if (change.getStatus() == Change.Status.ABANDONED
-              && change.currentPatchSetId().equals(patchSetId)) {
-            change.setStatus(Change.Status.NEW);
-            ChangeUtil.updated(change);
-            return change;
-          } else {
-            return null;
-          }
-        }
-      });
-
-      ChangeUtil.updatedChange(
-          db, currentUser, updatedChange, cmsg, restoredSenderFactory,
-         "Change is not abandoned or patchset is not latest");
-
-      hooks.doChangeRestoreHook(updatedChange, currentUser.getAccount(),
-                                changeComment, db);
+      return result;
     }
 
+    final PatchSet patch = db.patchSets().get(patchSetId);
+    if (patch == null) {
+      throw new NoSuchChangeException(changeId);
+    }
+
+    final Branch.NameKey destBranch = control.getChange().getDest();
+    if (!ProjectUtil.branchExists(repoManager, destBranch)) {
+      result.addError(new ReviewResult.Error(
+          ReviewResult.Error.Type.DEST_BRANCH_NOT_FOUND, destBranch.get()));
+      return result;
+    }
+
+    // Create a message to accompany the restored change
+    final ChangeMessage cmsg =
+        new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil
+            .messageUUID(db)), currentUser.getAccountId(), patchSetId);
+    final StringBuilder msgBuf =
+        new StringBuilder("Patch Set " + patchSetId.get() + ": Restored");
+    if (changeComment != null && changeComment.length() > 0) {
+      msgBuf.append("\n\n");
+      msgBuf.append(changeComment);
+    }
+    cmsg.setMessage(msgBuf.toString());
+
+    // Restore the change
+    final Change updatedChange = db.changes().atomicUpdate(changeId,
+        new AtomicUpdate<Change>() {
+          @Override
+          public Change update(Change change) {
+            if (change.getStatus() == Change.Status.ABANDONED
+                && change.currentPatchSetId().equals(patchSetId)) {
+              change.setStatus(Change.Status.NEW);
+              ChangeUtil.updated(change);
+              return change;
+            } else {
+              return null;
+            }
+          }
+        });
+
+    ChangeUtil.updatedChange(
+        db, currentUser, updatedChange, cmsg, restoredSenderFactory,
+        "Change is not abandoned or patchset is not latest");
+
+    hooks.doChangeRestoreHook(updatedChange, currentUser.getAccount(),
+                              changeComment, db);
+
     return result;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index ab52a9d..4205420 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -28,7 +28,9 @@
   public final File bin_dir;
   public final File etc_dir;
   public final File lib_dir;
+  public final File tmp_dir;
   public final File logs_dir;
+  public final File plugins_dir;
   public final File mail_dir;
   public final File hooks_dir;
   public final File static_dir;
@@ -62,6 +64,8 @@
     bin_dir = new File(site_path, "bin");
     etc_dir = new File(site_path, "etc");
     lib_dir = new File(site_path, "lib");
+    tmp_dir = new File(site_path, "tmp");
+    plugins_dir = new File(site_path, "plugins");
     logs_dir = new File(site_path, "logs");
     mail_dir = new File(etc_dir, "mail");
     hooks_dir = new File(site_path, "hooks");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 83877d0..095625d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -20,13 +20,10 @@
 import com.google.common.collect.ListMultimap;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.errors.NoSuchAccountException;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.ApprovalCategory;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -204,7 +201,6 @@
 
   private final IdentifiedUser currentUser;
   private final ReviewDb db;
-  private final ApprovalTypes approvalTypes;
   private final AccountResolver accountResolver;
   private final CreateChangeSender.Factory createChangeSenderFactory;
   private final MergedSender.Factory mergedSenderFactory;
@@ -253,7 +249,7 @@
   private MessageSender messageSender;
 
   @Inject
-  ReceiveCommits(final ReviewDb db, final ApprovalTypes approvalTypes,
+  ReceiveCommits(final ReviewDb db,
       final AccountResolver accountResolver,
       final CreateChangeSender.Factory createChangeSenderFactory,
       final MergedSender.Factory mergedSenderFactory,
@@ -276,7 +272,6 @@
       final SubmoduleOp.Factory subOpFactory) throws IOException {
     this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
     this.db = db;
-    this.approvalTypes = approvalTypes;
     this.accountResolver = accountResolver;
     this.createChangeSenderFactory = createChangeSenderFactory;
     this.mergedSenderFactory = mergedSenderFactory;
@@ -1387,33 +1382,21 @@
       result.patchSet = ps;
       result.info = info;
 
+      List<PatchSetApproval> patchSetApprovals = approvalsUtil.copyVetosToLatestPatchSet(change);
+
       final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
       oldReviewers.clear();
       oldCC.clear();
 
-      for (PatchSetApproval a : db.patchSetApprovals().byChange(change.getId())) {
+      for (PatchSetApproval a : patchSetApprovals) {
         haveApprovals.add(a.getAccountId());
-
         if (a.getValue() != 0) {
           oldReviewers.add(a.getAccountId());
         } else {
           oldCC.add(a.getAccountId());
         }
-
-        // ApprovalCategory.SUBMIT is still in db but not relevant in git-store
-        if (!ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
-          final ApprovalType type = approvalTypes.byId(a.getCategoryId());
-          if (a.getPatchSetId().equals(priorPatchSet)) {
-            if (type.getCategory().isCopyMinScore() && type.isMaxNegative(a)) {
-              // If there was a negative vote on the prior patch set, carry it
-              // into this patch set.
-              //
-              db.patchSetApprovals().insert(
-                  Collections.singleton(new PatchSetApproval(ps.getId(), a)));
-            }
-          }
-        }
       }
+
       approvalsUtil.addReviewers(change, ps, info, reviewers, haveApprovals);
 
       msg =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
index 4307854..8501426 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
@@ -40,6 +40,8 @@
 
   /** Exception thrown when a token does not parse correctly. */
   public static class InvalidTokenException extends Exception {
+    private static final long serialVersionUID = 1L;
+
     public InvalidTokenException() {
       super("Invalid token");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
new file mode 100644
index 0000000..5aee9bf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -0,0 +1,392 @@
+// 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 com.google.gerrit.server.plugins.PluginGuiceEnvironment.is;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.annotations.Listen;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import com.google.inject.internal.UniqueAnnotations;
+
+import org.eclipse.jgit.util.IO;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+class AutoRegisterModules {
+  private static final int SKIP_ALL = ClassReader.SKIP_CODE
+      | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
+  private final String pluginName;
+  private final PluginGuiceEnvironment env;
+  private final JarFile jarFile;
+  private final ClassLoader classLoader;
+  private final ModuleGenerator sshGen;
+  private final ModuleGenerator httpGen;
+
+  private Set<Class<?>> sysSingletons;
+  private Map<TypeLiteral<?>, Class<?>> sysListen;
+
+  Module sysModule;
+  Module sshModule;
+  Module httpModule;
+
+  AutoRegisterModules(String pluginName,
+      PluginGuiceEnvironment env,
+      JarFile jarFile,
+      ClassLoader classLoader) {
+    this.pluginName = pluginName;
+    this.env = env;
+    this.jarFile = jarFile;
+    this.classLoader = classLoader;
+    this.sshGen = env.hasSshModule() ? env.newSshModuleGenerator() : null;
+    this.httpGen = env.hasHttpModule() ? env.newHttpModuleGenerator() : null;
+  }
+
+  AutoRegisterModules discover() throws InvalidPluginException {
+    sysSingletons = Sets.newHashSet();
+    sysListen = Maps.newHashMap();
+
+    if (sshGen != null) {
+      sshGen.setPluginName(pluginName);
+    }
+    if (httpGen != null) {
+      httpGen.setPluginName(pluginName);
+    }
+
+    scan();
+
+    if (!sysSingletons.isEmpty() || !sysListen.isEmpty()) {
+      sysModule = makeSystemModule();
+    }
+    if (sshGen != null) {
+      sshModule = sshGen.create();
+    }
+    if (httpGen != null) {
+      httpModule = httpGen.create();
+    }
+    return this;
+  }
+
+  private Module makeSystemModule() {
+    return new AbstractModule() {
+      @Override
+      protected void configure() {
+        for (Class<?> clazz : sysSingletons) {
+          bind(clazz).in(Scopes.SINGLETON);
+        }
+        for (Map.Entry<TypeLiteral<?>, Class<?>> e : sysListen.entrySet()) {
+          @SuppressWarnings("unchecked")
+          TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+          @SuppressWarnings("unchecked")
+          Class<Object> impl = (Class<Object>) e.getValue();
+
+          Annotation n = impl.getAnnotation(Export.class);
+          if (n == null) {
+            n = impl.getAnnotation(javax.inject.Named.class);
+          }
+          if (n == null) {
+            n = impl.getAnnotation(com.google.inject.name.Named.class);
+          }
+          if (n == null) {
+            n = UniqueAnnotations.create();
+          }
+          bind(type).annotatedWith(n).to(impl);
+        }
+      }
+    };
+  }
+
+  private void scan() throws InvalidPluginException {
+    Enumeration<JarEntry> e = jarFile.entries();
+    while (e.hasMoreElements()) {
+      JarEntry entry = e.nextElement();
+      if (skip(entry)) {
+        continue;
+      }
+
+      ClassData def = new ClassData();
+      try {
+        new ClassReader(read(entry)).accept(def, SKIP_ALL);
+      } catch (IOException err) {
+        throw new InvalidPluginException("Cannot auto-register", err);
+      } catch (RuntimeException err) {
+        PluginLoader.log.warn(String.format(
+            "Plugin %s has invaild class file %s inside of %s",
+            pluginName, entry.getName(), jarFile.getName()), err);
+        continue;
+      }
+
+      if (def.exportedAsName != null) {
+        if (def.isConcrete()) {
+          export(def);
+        } else {
+          PluginLoader.log.warn(String.format(
+              "Plugin %s tries to @Export(\"%s\") abstract class %s",
+              pluginName, def.exportedAsName, def.className));
+        }
+      } else if (def.listen) {
+        if (def.isConcrete()) {
+          listen(def);
+        } else {
+          PluginLoader.log.warn(String.format(
+              "Plugin %s tries to @Listen abstract class %s",
+              pluginName, def.className));
+        }
+      }
+    }
+  }
+
+  private void export(ClassData def) throws InvalidPluginException {
+    Class<?> clazz;
+    try {
+      clazz = Class.forName(def.className, false, classLoader);
+    } catch (ClassNotFoundException err) {
+      throw new InvalidPluginException(String.format(
+          "Cannot load %s with @Export(\"%s\")",
+          def.className, def.exportedAsName), err);
+    }
+
+    Export export = clazz.getAnnotation(Export.class);
+    if (export == null) {
+      PluginLoader.log.warn(String.format(
+          "In plugin %s asm incorrectly parsed %s with @Export(\"%s\")",
+          pluginName, clazz.getName(), def.exportedAsName));
+      return;
+    }
+
+    if (is("org.apache.sshd.server.Command", clazz)) {
+      if (sshGen != null) {
+        sshGen.export(export, clazz);
+      }
+    } else if (is("javax.servlet.http.HttpServlet", clazz)) {
+      if (httpGen != null) {
+        httpGen.export(export, clazz);
+        listen(clazz, clazz);
+      }
+    } else {
+      int cnt = sysListen.size();
+      listen(clazz, clazz);
+      if (cnt == sysListen.size()) {
+        // If no bindings were recorded, the extension isn't recognized.
+        throw new InvalidPluginException(String.format(
+            "Class %s with @Export(\"%s\") not supported",
+            clazz.getName(), export.value()));
+      }
+    }
+  }
+
+  private void listen(ClassData def) throws InvalidPluginException {
+    Class<?> clazz;
+    try {
+      clazz = Class.forName(def.className, false, classLoader);
+    } catch (ClassNotFoundException err) {
+      throw new InvalidPluginException(String.format(
+          "Cannot load %s with @Listen",
+          def.className), err);
+    }
+
+    Listen listen = clazz.getAnnotation(Listen.class);
+    if (listen != null) {
+      listen(clazz, clazz);
+    } else {
+      PluginLoader.log.warn(String.format(
+          "In plugin %s asm incorrectly parsed %s with @Listen",
+          pluginName, clazz.getName()));
+    }
+  }
+
+  private void listen(java.lang.reflect.Type type, Class<?> clazz)
+      throws InvalidPluginException {
+    while (type != null) {
+      Class<?> rawType;
+      if (type instanceof ParameterizedType) {
+        rawType = (Class<?>) ((ParameterizedType) type).getRawType();
+      } else if (type instanceof Class) {
+        rawType = (Class<?>) type;
+      } else {
+        return;
+      }
+
+      if (rawType.getAnnotation(ExtensionPoint.class) != null) {
+        TypeLiteral<?> tl = TypeLiteral.get(type);
+        if (env.hasDynamicSet(tl)) {
+          sysSingletons.add(clazz);
+          sysListen.put(tl, clazz);
+        } else if (env.hasDynamicMap(tl)) {
+          if (clazz.getAnnotation(Export.class) == null) {
+            throw new InvalidPluginException(String.format(
+                "Class %s requires @Export(\"name\") annotation for %s",
+                clazz.getName(), rawType.getName()));
+          }
+          sysSingletons.add(clazz);
+          sysListen.put(tl, clazz);
+        } else {
+          throw new InvalidPluginException(String.format(
+              "Cannot register %s, server does not accept %s",
+              clazz.getName(), rawType.getName()));
+        }
+        return;
+      }
+
+      java.lang.reflect.Type[] interfaces = rawType.getGenericInterfaces();
+      if (interfaces != null) {
+        for (java.lang.reflect.Type i : interfaces) {
+          listen(i, clazz);
+        }
+      }
+
+      type = rawType.getGenericSuperclass();
+    }
+  }
+
+  private static boolean skip(JarEntry entry) {
+    if (!entry.getName().endsWith(".class")) {
+      return true; // Avoid non-class resources.
+    }
+    if (entry.getSize() <= 0) {
+      return true; // Directories have 0 size.
+    }
+    if (entry.getSize() >= 1024 * 1024) {
+      return true; // Do not scan huge class files.
+    }
+    return false;
+  }
+
+  private byte[] read(JarEntry entry) throws IOException {
+    byte[] data = new byte[(int) entry.getSize()];
+    InputStream in = jarFile.getInputStream(entry);
+    try {
+      IO.readFully(in, data, 0, data.length);
+    } finally {
+      in.close();
+    }
+    return data;
+  }
+
+  private static class ClassData implements ClassVisitor {
+    private static final String EXPORT = Type.getType(Export.class).getDescriptor();
+    private static final String LISTEN = Type.getType(Listen.class).getDescriptor();
+
+    String className;
+    int access;
+    String exportedAsName;
+    boolean listen;
+
+    boolean isConcrete() {
+      return (access & Opcodes.ACC_ABSTRACT) == 0
+          && (access & Opcodes.ACC_INTERFACE) == 0;
+    }
+
+    @Override
+    public void visit(int version, int access, String name, String signature,
+        String superName, String[] interfaces) {
+      this.className = Type.getObjectType(name).getClassName();
+      this.access = access;
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+      if (visible && EXPORT.equals(desc)) {
+        return new AbstractAnnotationVisitor() {
+          @Override
+          public void visit(String name, Object value) {
+            exportedAsName = (String) value;
+          }
+        };
+      }
+      if (visible && LISTEN.equals(desc)) {
+        listen = true;
+        return null;
+      }
+      return null;
+    }
+
+    @Override
+    public void visitSource(String arg0, String arg1) {
+    }
+
+    @Override
+    public void visitOuterClass(String arg0, String arg1, String arg2) {
+    }
+
+    @Override
+    public MethodVisitor visitMethod(int arg0, String arg1, String arg2,
+        String arg3, String[] arg4) {
+      return null;
+    }
+
+    @Override
+    public void visitInnerClass(String arg0, String arg1, String arg2, int arg3) {
+    }
+
+    @Override
+    public FieldVisitor visitField(int arg0, String arg1, String arg2,
+        String arg3, Object arg4) {
+      return null;
+    }
+
+    @Override
+    public void visitEnd() {
+    }
+
+    @Override
+    public void visitAttribute(Attribute arg0) {
+    }
+  }
+
+  private static abstract class AbstractAnnotationVisitor implements
+      AnnotationVisitor {
+    @Override
+    public AnnotationVisitor visitAnnotation(String arg0, String arg1) {
+      return null;
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String arg0) {
+      return null;
+    }
+
+    @Override
+    public void visitEnum(String arg0, String arg1, String arg2) {
+    }
+
+    @Override
+    public void visitEnd() {
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
new file mode 100644
index 0000000..e18d840
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
@@ -0,0 +1,45 @@
+// 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.io.File;
+import java.io.IOException;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.jar.JarFile;
+
+class CleanupHandle extends WeakReference<ClassLoader> {
+  private final File tmpFile;
+  private final JarFile jarFile;
+
+  CleanupHandle(File tmpFile,
+      JarFile jarFile,
+      ClassLoader ref,
+      ReferenceQueue<ClassLoader> queue) {
+    super(ref, queue);
+    this.tmpFile = tmpFile;
+    this.jarFile = jarFile;
+  }
+
+  void cleanup() {
+    try {
+      jarFile.close();
+    } catch (IOException err) {
+    }
+    if (!tmpFile.delete() && tmpFile.exists()) {
+      PluginLoader.log.warn("Cannot delete " + tmpFile.getAbsolutePath());
+    }
+  }
+}
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/InvalidPluginException.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java
new file mode 100644
index 0000000..31be10c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java
@@ -0,0 +1,27 @@
+// 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 InvalidPluginException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public InvalidPluginException(String message) {
+    super(message);
+  }
+
+  public InvalidPluginException(String message, Throwable why) {
+    super(message, why);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
new file mode 100644
index 0000000..92e3b1d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
@@ -0,0 +1,26 @@
+// 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.extensions.annotations.Export;
+import com.google.inject.Module;
+
+public interface ModuleGenerator {
+  void setPluginName(String name);
+
+  void export(Export export, Class<?> type) throws InvalidPluginException;
+
+  Module create() throws InvalidPluginException;
+}
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..c47f370
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -0,0 +1,243 @@
+// 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.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
+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.Collections;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+import javax.annotation.Nullable;
+
+public class Plugin {
+  static {
+    // Guice logs warnings about multiple injectors being created.
+    // Silence this in case HTTP plugins are used.
+    java.util.logging.Logger.getLogger("com.google.inject.servlet.GuiceFilter")
+        .setLevel(java.util.logging.Level.OFF);
+  }
+
+  private final String name;
+  private final File srcJar;
+  private final FileSnapshot snapshot;
+  private final JarFile jarFile;
+  private final Manifest manifest;
+  private final ClassLoader classLoader;
+  private Class<? extends Module> sysModule;
+  private Class<? extends Module> sshModule;
+  private Class<? extends Module> httpModule;
+
+  private Injector sysInjector;
+  private Injector sshInjector;
+  private Injector httpInjector;
+  private LifecycleManager manager;
+  private List<ReloadableRegistrationHandle<?>> reloadableHandles;
+
+  public Plugin(String name,
+      File srcJar,
+      FileSnapshot snapshot,
+      JarFile jarFile,
+      Manifest manifest,
+      ClassLoader classLoader,
+      @Nullable Class<? extends Module> sysModule,
+      @Nullable Class<? extends Module> sshModule,
+      @Nullable Class<? extends Module> httpModule) {
+    this.name = name;
+    this.srcJar = srcJar;
+    this.snapshot = snapshot;
+    this.jarFile = jarFile;
+    this.manifest = manifest;
+    this.classLoader = classLoader;
+    this.sysModule = sysModule;
+    this.sshModule = sshModule;
+    this.httpModule = httpModule;
+  }
+
+  File getSrcJar() {
+    return srcJar;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public String getVersion() {
+    Attributes main = manifest.getMainAttributes();
+    return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+  }
+
+  boolean canReload() {
+    Attributes main = manifest.getMainAttributes();
+    String v = main.getValue("Gerrit-ReloadMode");
+    if (Strings.isNullOrEmpty(v) || "reload".equalsIgnoreCase(v)) {
+      return true;
+    } else if ("restart".equalsIgnoreCase(v)) {
+      return false;
+    } else {
+      PluginLoader.log.warn(String.format(
+          "Plugin %s has invalid Gerrit-ReloadMode %s; assuming restart",
+          name, v));
+      return false;
+    }
+  }
+
+  boolean isModified(File jar) {
+    return snapshot.lastModified() != jar.lastModified();
+  }
+
+  public void start(PluginGuiceEnvironment env) throws Exception {
+    Injector root = newRootInjector(env);
+    manager = new LifecycleManager();
+
+    AutoRegisterModules auto = null;
+    if (sysModule == null && sshModule == null && httpModule == null) {
+      auto = new AutoRegisterModules(name, env, jarFile, classLoader);
+      auto.discover();
+    }
+
+    if (sysModule != null) {
+      sysInjector = root.createChildInjector(root.getInstance(sysModule));
+      manager.add(sysInjector);
+    } else if (auto != null && auto.sysModule != null) {
+      sysInjector = root.createChildInjector(auto.sysModule);
+      manager.add(sysInjector);
+    } else {
+      sysInjector = root;
+    }
+
+    if (env.hasSshModule()) {
+      if (sshModule != null) {
+        sshInjector = sysInjector.createChildInjector(
+            env.getSshModule(),
+            sysInjector.getInstance(sshModule));
+        manager.add(sshInjector);
+      } else if (auto != null && auto.sshModule != null) {
+        sshInjector = sysInjector.createChildInjector(
+            env.getSshModule(),
+            auto.sshModule);
+        manager.add(sshInjector);
+      }
+    }
+
+    if (env.hasHttpModule()) {
+      if (httpModule != null) {
+        httpInjector = sysInjector.createChildInjector(
+            env.getHttpModule(),
+            sysInjector.getInstance(httpModule));
+        manager.add(httpInjector);
+      } else if (auto != null && auto.httpModule != null) {
+        httpInjector = sysInjector.createChildInjector(
+            env.getHttpModule(),
+            auto.httpModule);
+        manager.add(httpInjector);
+      }
+    }
+
+    manager.start();
+  }
+
+  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;
+      httpInjector = null;
+    }
+  }
+
+  public JarFile getJarFile() {
+    return jarFile;
+  }
+
+  public Injector getSysInjector() {
+    return sysInjector;
+  }
+
+  @Nullable
+  public Injector getSshInjector() {
+    return sshInjector;
+  }
+
+  @Nullable
+  public Injector getHttpInjector() {
+    return httpInjector;
+  }
+
+  public void add(final RegistrationHandle handle) {
+    if (handle instanceof ReloadableRegistrationHandle) {
+      if (reloadableHandles == null) {
+        reloadableHandles = Lists.newArrayList();
+      }
+      reloadableHandles.add((ReloadableRegistrationHandle<?>) handle);
+    }
+
+    add(new LifecycleListener() {
+      @Override
+      public void start() {
+      }
+
+      @Override
+      public void stop() {
+        handle.remove();
+      }
+    });
+  }
+
+  public void add(LifecycleListener listener) {
+    manager.add(listener);
+  }
+
+  List<ReloadableRegistrationHandle<?>> getReloadableHandles() {
+    if (reloadableHandles != null) {
+      return reloadableHandles;
+    }
+    return Collections.emptyList();
+  }
+
+  @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..1b94c0c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -0,0 +1,510 @@
+// 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.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
+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.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.internal.UniqueAnnotations;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.annotation.Nullable;
+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> onStart;
+  private final List<ReloadPluginListener> onReload;
+
+  private Module sysModule;
+  private Module sshModule;
+  private Module httpModule;
+
+  private Provider<ModuleGenerator> sshGen;
+  private Provider<ModuleGenerator> httpGen;
+
+  private Map<TypeLiteral<?>, DynamicSet<?>> sysSets;
+  private Map<TypeLiteral<?>, DynamicSet<?>> sshSets;
+  private Map<TypeLiteral<?>, DynamicSet<?>> httpSets;
+
+  private Map<TypeLiteral<?>, DynamicMap<?>> sysMaps;
+  private Map<TypeLiteral<?>, DynamicMap<?>> sshMaps;
+  private Map<TypeLiteral<?>, DynamicMap<?>> httpMaps;
+
+  @Inject
+  PluginGuiceEnvironment(Injector sysInjector, CopyConfigModule ccm) {
+    this.sysInjector = sysInjector;
+    this.copyConfigModule = ccm;
+
+    onStart = new CopyOnWriteArrayList<StartPluginListener>();
+    onStart.addAll(listeners(sysInjector, StartPluginListener.class));
+
+    onReload = new CopyOnWriteArrayList<ReloadPluginListener>();
+    onReload.addAll(listeners(sysInjector, ReloadPluginListener.class));
+
+    sysSets = dynamicSetsOf(sysInjector);
+    sysMaps = dynamicMapsOf(sysInjector);
+  }
+
+  boolean hasDynamicSet(TypeLiteral<?> type) {
+    return sysSets.containsKey(type)
+        || (sshSets != null && sshSets.containsKey(type))
+        || (httpSets != null && httpSets.containsKey(type));
+  }
+
+  boolean hasDynamicMap(TypeLiteral<?> type) {
+    return sysMaps.containsKey(type)
+        || (sshMaps != null && sshMaps.containsKey(type))
+        || (httpMaps != null && httpMaps.containsKey(type));
+  }
+
+  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 injector) {
+    sshModule = copy(injector);
+    sshGen = injector.getProvider(ModuleGenerator.class);
+    sshSets = dynamicSetsOf(injector);
+    sshMaps = dynamicMapsOf(injector);
+    onStart.addAll(listeners(injector, StartPluginListener.class));
+    onReload.addAll(listeners(injector, ReloadPluginListener.class));
+  }
+
+  boolean hasSshModule() {
+    return sshModule != null;
+  }
+
+  Module getSshModule() {
+    return sshModule;
+  }
+
+  ModuleGenerator newSshModuleGenerator() {
+    return sshGen.get();
+  }
+
+  public void setHttpInjector(Injector injector) {
+    httpModule = copy(injector);
+    httpGen = injector.getProvider(ModuleGenerator.class);
+    httpSets = dynamicSetsOf(injector);
+    httpMaps = dynamicMapsOf(injector);
+    onStart.addAll(listeners(injector, StartPluginListener.class));
+    onReload.addAll(listeners(injector, ReloadPluginListener.class));
+  }
+
+  boolean hasHttpModule() {
+    return httpModule != null;
+  }
+
+  Module getHttpModule() {
+    return httpModule;
+  }
+
+  ModuleGenerator newHttpModuleGenerator() {
+    return httpGen.get();
+  }
+
+  void onStartPlugin(Plugin plugin) {
+    for (StartPluginListener l : onStart) {
+      l.onStartPlugin(plugin);
+    }
+
+    attachSet(sysSets, plugin.getSysInjector(), plugin);
+    attachSet(sshSets, plugin.getSshInjector(), plugin);
+    attachSet(httpSets, plugin.getHttpInjector(), plugin);
+
+    attachMap(sysMaps, plugin.getSysInjector(), plugin);
+    attachMap(sshMaps, plugin.getSshInjector(), plugin);
+    attachMap(httpMaps, plugin.getHttpInjector(), plugin);
+  }
+
+  private void attachSet(Map<TypeLiteral<?>, DynamicSet<?>> sets,
+      @Nullable Injector src,
+      Plugin plugin) {
+    if (src != null && sets != null && !sets.isEmpty()) {
+      for (Map.Entry<TypeLiteral<?>, DynamicSet<?>> e : sets.entrySet()) {
+        @SuppressWarnings("unchecked")
+        TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+        @SuppressWarnings("unchecked")
+        DynamicSet<Object> set = (DynamicSet<Object>) e.getValue();
+
+        for (Binding<Object> b : bindings(src, type)) {
+          plugin.add(set.add(b.getKey(), b.getProvider().get()));
+        }
+      }
+    }
+  }
+
+  private void attachMap(Map<TypeLiteral<?>, DynamicMap<?>> maps,
+      @Nullable Injector src,
+      Plugin plugin) {
+    if (src != null && maps != null && !maps.isEmpty()) {
+      for (Map.Entry<TypeLiteral<?>, DynamicMap<?>> e : maps.entrySet()) {
+        @SuppressWarnings("unchecked")
+        TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+        @SuppressWarnings("unchecked")
+        PrivateInternals_DynamicMapImpl<Object> set =
+            (PrivateInternals_DynamicMapImpl<Object>) e.getValue();
+
+        for (Binding<Object> b : bindings(src, type)) {
+          plugin.add(set.put(
+              plugin.getName(),
+              b.getKey(),
+              b.getProvider().get()));
+        }
+      }
+    }
+  }
+
+  void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
+    for (ReloadPluginListener l : onReload) {
+      l.onReloadPlugin(oldPlugin, newPlugin);
+    }
+
+    // Index all old registrations by the raw type. These may be replaced
+    // during the reattach calls below. Any that are not replaced will be
+    // removed when the old plugin does its stop routine.
+    ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> old =
+        LinkedListMultimap.create();
+    for (ReloadableRegistrationHandle<?> h : oldPlugin.getReloadableHandles()) {
+      old.put(h.getKey().getTypeLiteral(), h);
+    }
+
+    reattachMap(old, sysMaps, newPlugin.getSysInjector(), newPlugin);
+    reattachMap(old, sshMaps, newPlugin.getSshInjector(), newPlugin);
+    reattachMap(old, httpMaps, newPlugin.getHttpInjector(), newPlugin);
+
+    reattachSet(old, sysSets, newPlugin.getSysInjector(), newPlugin);
+    reattachSet(old, sshSets, newPlugin.getSshInjector(), newPlugin);
+    reattachSet(old, httpSets, newPlugin.getHttpInjector(), newPlugin);
+  }
+
+  private void reattachMap(
+      ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> oldHandles,
+      Map<TypeLiteral<?>, DynamicMap<?>> maps,
+      @Nullable Injector src,
+      Plugin newPlugin) {
+    if (src == null || maps == null || maps.isEmpty()) {
+      return;
+    }
+
+    for (Map.Entry<TypeLiteral<?>, DynamicMap<?>> e : maps.entrySet()) {
+      @SuppressWarnings("unchecked")
+      TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+      @SuppressWarnings("unchecked")
+      PrivateInternals_DynamicMapImpl<Object> map =
+          (PrivateInternals_DynamicMapImpl<Object>) e.getValue();
+
+      Map<Annotation, ReloadableRegistrationHandle<?>> am = Maps.newHashMap();
+      for (ReloadableRegistrationHandle<?> h : oldHandles.get(type)) {
+        Annotation a = h.getKey().getAnnotation();
+        if (a != null && !UNIQUE_ANNOTATION.isInstance(a)) {
+          am.put(a, h);
+        }
+      }
+
+      for (Binding<?> binding : bindings(src, e.getKey())) {
+        @SuppressWarnings("unchecked")
+        Binding<Object> b = (Binding<Object>) binding;
+        Key<Object> key = b.getKey();
+
+        @SuppressWarnings("unchecked")
+        ReloadableRegistrationHandle<Object> h =
+            (ReloadableRegistrationHandle<Object>) am.remove(key.getAnnotation());
+        if (h != null) {
+          replace(newPlugin, h, b);
+          oldHandles.remove(type, h);
+        } else {
+          newPlugin.add(map.put(
+              newPlugin.getName(),
+              b.getKey(),
+              b.getProvider().get()));
+        }
+      }
+    }
+  }
+
+  /** Type used to declare unique annotations. Guice hides this, so extract it. */
+  private static final Class<?> UNIQUE_ANNOTATION =
+      UniqueAnnotations.create().getClass();
+
+  private void reattachSet(
+      ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> oldHandles,
+      Map<TypeLiteral<?>, DynamicSet<?>> sets,
+      @Nullable Injector src,
+      Plugin newPlugin) {
+    if (src == null || sets == null || sets.isEmpty()) {
+      return;
+    }
+
+    for (Map.Entry<TypeLiteral<?>, DynamicSet<?>> e : sets.entrySet()) {
+      @SuppressWarnings("unchecked")
+      TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+      @SuppressWarnings("unchecked")
+      DynamicSet<Object> set = (DynamicSet<Object>) e.getValue();
+
+      // Index all old handles that match this DynamicSet<T> keyed by
+      // annotations. Ignore the unique annotations, thereby favoring
+      // the @Named annotations or some other non-unique naming.
+      Map<Annotation, ReloadableRegistrationHandle<?>> am = Maps.newHashMap();
+      List<ReloadableRegistrationHandle<?>> old = oldHandles.get(type);
+      Iterator<ReloadableRegistrationHandle<?>> oi = old.iterator();
+      while (oi.hasNext()) {
+        ReloadableRegistrationHandle<?> h = oi.next();
+        Annotation a = h.getKey().getAnnotation();
+        if (a != null && !UNIQUE_ANNOTATION.isInstance(a)) {
+          am.put(a, h);
+          oi.remove();
+        }
+      }
+
+      // Replace old handles with new bindings, favoring cases where there
+      // is an exact match on an @Named annotation. If there is no match
+      // pick any handle and replace it. We generally expect only one
+      // handle of each DynamicSet type when using unique annotations, but
+      // possibly multiple ones if @Named was used. Plugin authors that want
+      // atomic replacement across reloads should use @Named annotations with
+      // stable names that do not change across plugin versions to ensure the
+      // handles are swapped correctly.
+      oi = old.iterator();
+      for (Binding<?> binding : bindings(src, type)) {
+        @SuppressWarnings("unchecked")
+        Binding<Object> b = (Binding<Object>) binding;
+        Key<Object> key = b.getKey();
+
+        @SuppressWarnings("unchecked")
+        ReloadableRegistrationHandle<Object> h1 =
+            (ReloadableRegistrationHandle<Object>) am.remove(key.getAnnotation());
+        if (h1 != null) {
+          replace(newPlugin, h1, b);
+        } else if (oi.hasNext()) {
+          @SuppressWarnings("unchecked")
+          ReloadableRegistrationHandle<Object> h2 =
+            (ReloadableRegistrationHandle<Object>) oi.next();
+          oi.remove();
+          replace(newPlugin, h2, b);
+        } else {
+          newPlugin.add(set.add(b.getKey(), b.getProvider().get()));
+        }
+      }
+    }
+  }
+
+  private static <T> void replace(Plugin newPlugin,
+      ReloadableRegistrationHandle<T> h, Binding<T> b) {
+    RegistrationHandle n = h.replace(b.getKey(), b.getProvider().get());
+    if (n != null){
+      newPlugin.add(n);
+    }
+  }
+
+  static <T> List<T> listeners(Injector src, Class<T> type) {
+    List<Binding<T>> bindings = bindings(src, TypeLiteral.get(type));
+    int cnt = bindings != null ? bindings.size() : 0;
+    List<T> found = Lists.newArrayListWithCapacity(cnt);
+    if (bindings != null) {
+      for (Binding<T> b : bindings) {
+        found.add(b.getProvider().get());
+      }
+    }
+    return found;
+  }
+
+  private static <T> List<Binding<T>> bindings(Injector src, TypeLiteral<T> type) {
+    return src.findBindingsByType(type);
+  }
+
+  private static Map<TypeLiteral<?>, DynamicSet<?>> dynamicSetsOf(Injector src) {
+    Map<TypeLiteral<?>, DynamicSet<?>> m = Maps.newHashMap();
+    for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+      TypeLiteral<?> type = e.getKey().getTypeLiteral();
+      if (type.getRawType() == DynamicSet.class) {
+        ParameterizedType p = (ParameterizedType) type.getType();
+        m.put(TypeLiteral.get(p.getActualTypeArguments()[0]),
+            (DynamicSet<?>) e.getValue().getProvider().get());
+      }
+    }
+    return m;
+  }
+
+  private static Map<TypeLiteral<?>, DynamicMap<?>> dynamicMapsOf(Injector src) {
+    Map<TypeLiteral<?>, DynamicMap<?>> m = Maps.newHashMap();
+    for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+      TypeLiteral<?> type = e.getKey().getTypeLiteral();
+      if (type.getRawType() == DynamicMap.class) {
+        ParameterizedType p = (ParameterizedType) type.getType();
+        m.put(TypeLiteral.get(p.getActualTypeArguments()[0]),
+            (DynamicMap<?>) e.getValue().getProvider().get());
+      }
+    }
+    return m;
+  }
+
+  private static Module copy(Injector src) {
+    Set<TypeLiteral<?>> dynamicTypes = Sets.newHashSet();
+    for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+      TypeLiteral<?> type = e.getKey().getTypeLiteral();
+      if (type.getRawType() == DynamicSet.class
+          || type.getRawType() == DynamicMap.class) {
+        ParameterizedType t = (ParameterizedType) type.getType();
+        dynamicTypes.add(TypeLiteral.get(t.getActualTypeArguments()[0]));
+      }
+    }
+
+    final Map<Key<?>, Binding<?>> bindings = Maps.newLinkedHashMap();
+    for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+      if (!dynamicTypes.contains(e.getKey().getTypeLiteral())
+          && 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 (LifecycleListener.class.isAssignableFrom(type)) {
+      return false;
+    }
+    if (StartPluginListener.class.isAssignableFrom(type)) {
+      return false;
+    }
+
+    if (type.getName().startsWith("com.google.inject.")) {
+      return false;
+    }
+
+    if (is("org.apache.sshd.server.Command", type)) {
+      return false;
+    }
+
+    if (is("javax.servlet.Filter", type)) {
+      return false;
+    }
+    if (is("javax.servlet.ServletContext", type)) {
+      return false;
+    }
+    if (is("javax.servlet.ServletRequest", type)) {
+      return false;
+    }
+    if (is("javax.servlet.ServletResponse", type)) {
+      return false;
+    }
+    if (is("javax.servlet.http.HttpServlet", type)) {
+      return false;
+    }
+    if (is("javax.servlet.http.HttpServletRequest", type)) {
+      return false;
+    }
+    if (is("javax.servlet.http.HttpServletResponse", type)) {
+      return false;
+    }
+    if (is("javax.servlet.http.HttpSession", type)) {
+      return false;
+    }
+    if (Map.class.isAssignableFrom(type)
+        && key.getAnnotationType() != null
+        && "com.google.inject.servlet.RequestParameters"
+            .equals(key.getAnnotationType().getName())) {
+      return false;
+    }
+    if (type.getName().startsWith("com.google.gerrit.httpd.GitOverHttpServlet$")) {
+      return false;
+    }
+    return true;
+  }
+
+  static boolean is(String name, Class<?> type) {
+    while (type != null) {
+      if (name.equals(type.getName())) {
+        return true;
+      }
+
+      Class<?>[] interfaces = type.getInterfaces();
+      if (interfaces != null) {
+        for (Class<?> i : interfaces) {
+          if (is(name, i)) {
+            return true;
+          }
+        }
+      }
+
+      type = type.getSuperclass();
+    }
+    return false;
+  }
+}
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..16cd78c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -0,0 +1,387 @@
+// 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.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.ReferenceQueue;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+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 {
+  static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
+
+  private final File pluginsDir;
+  private final File tmpDir;
+  private final PluginGuiceEnvironment env;
+  private final Map<String, Plugin> running;
+  private final Map<String, FileSnapshot> broken;
+  private final ReferenceQueue<ClassLoader> cleanupQueue;
+  private final ConcurrentMap<CleanupHandle, Boolean> cleanupHandles;
+  private final PluginScannerThread scanner;
+
+  @Inject
+  public PluginLoader(SitePaths sitePaths,
+      PluginGuiceEnvironment pe,
+      @GerritServerConfig Config cfg) {
+    pluginsDir = sitePaths.plugins_dir;
+    tmpDir = sitePaths.tmp_dir;
+    env = pe;
+    running = Maps.newHashMap();
+    broken = Maps.newHashMap();
+    cleanupQueue = new ReferenceQueue<ClassLoader>();
+    cleanupHandles = Maps.newConcurrentMap();
+
+    long checkFrequency = ConfigUtil.getTimeUnit(cfg,
+        "plugins", null, "checkFrequency",
+        TimeUnit.MINUTES.toMillis(1), TimeUnit.MILLISECONDS);
+    if (checkFrequency > 0) {
+      scanner = new PluginScannerThread(this, checkFrequency);
+    } else {
+      scanner = null;
+    }
+  }
+
+  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 = asTemp(in, ".next_" + name, ".zip", pluginsDir);
+    boolean clean = false;
+    synchronized (this) {
+      Plugin active = running.get(name);
+      if (active != null) {
+        log.info(String.format("Replacing plugin %s", name));
+        old.delete();
+        jar.renameTo(old);
+      }
+
+      new File(pluginsDir, name + ".jar.disabled").delete();
+      tmp.renameTo(jar);
+      try {
+        runPlugin(name, jar, active);
+        if (active == null) {
+          log.info(String.format("Installed plugin %s", name));
+        } else {
+          clean = true;
+        }
+      } catch (PluginInstallException e) {
+        jar.delete();
+        throw e;
+      }
+    }
+
+    if (clean) {
+      System.gc();
+      processPendingCleanups();
+    }
+  }
+
+  private static File asTemp(InputStream in,
+      String prefix, String suffix,
+      File dir) throws IOException {
+    File tmp = File.createTempFile(prefix, suffix, dir);
+    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 void disablePlugins(Set<String> names) {
+    boolean clean = false;
+    synchronized (this) {
+      for (String name : names) {
+        Plugin active = running.get(name);
+        if (active == null) {
+          continue;
+        }
+
+        log.info(String.format("Disabling plugin %s", name));
+        File off = new File(pluginsDir, active.getName() + ".jar.disabled");
+        active.getSrcJar().renameTo(off);
+
+        active.stop();
+        running.remove(name);
+        clean = true;
+      }
+    }
+    if (clean) {
+      System.gc();
+      processPendingCleanups();
+    }
+  }
+
+  @Override
+  public synchronized void start() {
+    log.info("Loading plugins from " + pluginsDir.getAbsolutePath());
+    rescan(false);
+    if (scanner != null) {
+      scanner.start();
+    }
+  }
+
+  @Override
+  public void stop() {
+    if (scanner != null) {
+      scanner.end();
+    }
+    synchronized (this) {
+      boolean clean = !running.isEmpty();
+      for (Plugin p : running.values()) {
+        p.stop();
+      }
+      running.clear();
+      broken.clear();
+      if (clean) {
+        System.gc();
+        processPendingCleanups();
+      }
+    }
+  }
+
+  public void rescan(boolean forceCleanup) {
+    if (rescanImp() || forceCleanup) {
+      System.gc();
+      processPendingCleanups();
+    }
+  }
+
+  private synchronized boolean rescanImp() {
+    List<File> jars = scanJarsInPluginsDirectory();
+    boolean clean = 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.info(String.format("Reloading plugin %s", name));
+      }
+
+      try {
+        runPlugin(name, jar, active);
+        if (active == null) {
+          log.info(String.format("Loaded plugin %s", name));
+        } else {
+          clean = true;
+        }
+      } catch (PluginInstallException e) {
+        log.warn(String.format("Cannot load plugin %s", name), e.getCause());
+      }
+    }
+    return clean;
+  }
+
+  private void runPlugin(String name, File jar, Plugin oldPlugin)
+      throws PluginInstallException {
+    FileSnapshot snapshot = FileSnapshot.save(jar);
+    try {
+      Plugin newPlugin = loadPlugin(name, jar, snapshot);
+      boolean reload = oldPlugin != null
+          && oldPlugin.canReload()
+          && newPlugin.canReload();
+      if (!reload && oldPlugin != null) {
+        oldPlugin.stop();
+        running.remove(name);
+      }
+      newPlugin.start(env);
+      if (reload) {
+        env.onReloadPlugin(oldPlugin, newPlugin);
+        oldPlugin.stop();
+      } else {
+        env.onStartPlugin(newPlugin);
+      }
+      running.put(name, newPlugin);
+      broken.remove(name);
+    } catch (Throwable err) {
+      broken.put(name, snapshot);
+      throw new PluginInstallException(err);
+    }
+  }
+
+  private boolean 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();
+    }
+    return !unload.isEmpty();
+  }
+
+  private synchronized void processPendingCleanups() {
+    CleanupHandle h;
+    while ((h = (CleanupHandle) cleanupQueue.poll()) != null) {
+      h.cleanup();
+      cleanupHandles.remove(h);
+    }
+  }
+
+  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, File srcJar, FileSnapshot snapshot)
+      throws IOException, ClassNotFoundException {
+    File tmp;
+    FileInputStream in = new FileInputStream(srcJar);
+    try {
+      tmp = asTemp(in, tempNameFor(name), ".jar", tmpDir);
+    } finally {
+      in.close();
+    }
+
+    JarFile jarFile = new JarFile(tmp);
+    boolean keep = false;
+    try {
+      Manifest manifest = jarFile.getManifest();
+      Attributes main = manifest.getMainAttributes();
+      String sysName = main.getValue("Gerrit-Module");
+      String sshName = main.getValue("Gerrit-SshModule");
+      String httpName = main.getValue("Gerrit-HttpModule");
+
+      URL[] urls = {tmp.toURI().toURL()};
+      ClassLoader parentLoader = PluginLoader.class.getClassLoader();
+      ClassLoader pluginLoader = new URLClassLoader(urls, parentLoader);
+      cleanupHandles.put(
+          new CleanupHandle(tmp, jarFile, pluginLoader, cleanupQueue),
+          Boolean.TRUE);
+
+      Class<? extends Module> sysModule = load(sysName, pluginLoader);
+      Class<? extends Module> sshModule = load(sshName, pluginLoader);
+      Class<? extends Module> httpModule = load(httpName, pluginLoader);
+      keep = true;
+      return new Plugin(name,
+          srcJar, snapshot,
+          jarFile, manifest,
+          pluginLoader,
+          sysModule, sshModule, httpModule);
+    } finally {
+      if (!keep) {
+        jarFile.close();
+      }
+    }
+  }
+
+  private static String tempNameFor(String name) {
+    SimpleDateFormat fmt = new SimpleDateFormat("yyMMdd_HHmm");
+    return "plugin_" + name + "_" + fmt.format(new Date()) + "_";
+  }
+
+  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/server/plugins/PluginModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
new file mode 100644
index 0000000..0431ee1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
@@ -0,0 +1,27 @@
+// 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.LifecycleModule;
+
+public class PluginModule extends LifecycleModule {
+  @Override
+  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/PluginScannerThread.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
new file mode 100644
index 0000000..b2e3fed
--- /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(false);
+    }
+  }
+
+  void end() {
+    done.countDown();
+    try {
+      join();
+    } catch (InterruptedException e) {
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java
new file mode 100644
index 0000000..72a499e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.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 reloaded. */
+public interface ReloadPluginListener {
+  public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin);
+}
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-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 7652bed..f232c5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -204,7 +204,8 @@
 
   /** Can this user rebase this change? */
   public boolean canRebase() {
-    return isOwner() || getRefControl().canSubmit();
+    return isOwner() || getRefControl().canSubmit()
+        || getRefControl().canRebase();
   }
 
   /** Can this user restore this change? */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index a865603..db370e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -125,6 +125,12 @@
         && canWrite();
   }
 
+  /** @return true if this user can rebase changes on this ref */
+  public boolean canRebase() {
+    return canPerform(Permission.REBASE)
+        && canWrite();
+  }
+
   /** @return true if this user can submit patch sets to this ref */
   public boolean canSubmit() {
     if (GitRepositoryManager.REF_CONFIG.equals(refName)) {
diff --git a/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java b/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
index ac74147..606e883 100644
--- a/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
+++ b/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
@@ -27,7 +27,6 @@
 import com.googlecode.prolog_cafe.lang.Term;
 
 abstract class AbstractCommitUserIdentityPredicate extends Predicate.P3 {
-  private static final long serialVersionUID = 1L;
   private static final SymbolTerm user = SymbolTerm.intern("user", 1);
   private static final SymbolTerm anonymous = SymbolTerm.intern("anonymous");
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
index b32d54c..0e556f3 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
@@ -73,6 +73,7 @@
   private GitRepositoryManager repoManager;
   private ReplicationQueue replication;
 
+  @SuppressWarnings("unchecked")
   @Override
   @Before
   public void setUp() throws Exception {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index 28e3c25..340db7e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -29,13 +29,11 @@
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.cache.ConcurrentHashMapCache;
@@ -44,7 +42,6 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 
@@ -375,8 +372,6 @@
   }
 
   private ProjectControl user(String name, AccountGroup.UUID... memberOf) {
-    SchemaFactory<ReviewDb> schema = null;
-    GroupCache groupCache = null;
     String canonicalWebUrl = "http://localhost";
 
     return new ProjectControl(Collections.<AccountGroup.UUID> emptySet(),
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
index c8e684f..93d86e5 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
@@ -224,7 +224,7 @@
             break;
           } else {
             expect(repoManager.list()).andReturn(
-                new TreeSet<Project.NameKey>(Collections.EMPTY_LIST));
+                new TreeSet<Project.NameKey>(Collections.<Project.NameKey> emptyList()));
           }
         }
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index 0327fb8..c5be624 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -508,6 +508,15 @@
     /**
      * Create a new failure.
      *
+     * @param msg message to also send to the client's stderr.
+     */
+    public UnloggedFailure(final String msg) {
+      this(1, msg);
+    }
+
+    /**
+     * Create a new failure.
+     *
      * @param exitCode exit code to return the client, which indicates the
      *        failure status of this command. Should be between 1 and 255,
      *        inclusive.
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..d70d32f 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.extensions.registration.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,33 @@
     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);
+      }
+    };
+  }
+
+  public RegistrationHandle replace(final CommandName name,
+      final Provider<Command> cmd) {
+    final ConcurrentMap<String, Provider<Command>> m = getMap();
+    m.put(name.value(), cmd);
+    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 +96,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 +107,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/SshAutoRegisterModuleGenerator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
new file mode 100644
index 0000000..b843893
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
@@ -0,0 +1,74 @@
+// 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.common.base.Preconditions;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+
+import org.apache.sshd.server.Command;
+
+import java.util.Map;
+
+class SshAutoRegisterModuleGenerator
+    extends AbstractModule
+    implements ModuleGenerator {
+  private final Map<String, Class<Command>> commands = Maps.newHashMap();
+  private CommandName command;
+
+  @Override
+  protected void configure() {
+    bind(Commands.key(command))
+        .toProvider(new DispatchCommandProvider(command));
+    for (Map.Entry<String, Class<Command>> e : commands.entrySet()) {
+      bind(Commands.key(command, e.getKey())).to(e.getValue());
+    }
+  }
+
+  public void setPluginName(String name) {
+    command = Commands.named(name);
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void export(Export export, Class<?> type)
+      throws InvalidPluginException {
+    Preconditions.checkState(command != null, "pluginName must be provided");
+    if (Command.class.isAssignableFrom(type)) {
+      Class<Command> old = commands.get(export.value());
+      if (old != null) {
+        throw new InvalidPluginException(String.format(
+            "@Export(\"%s\") has duplicate bindings:\n  %s\n  %s",
+            export.value(), old.getName(), type.getName()));
+      }
+      commands.put(export.value(), (Class<Command>) type);
+    } else {
+      throw new InvalidPluginException(String.format(
+          "Class %s with @Export(\"%s\") must extend %s or implement %s",
+          type.getName(), export.value(),
+          SshCommand.class.getName(), Command.class.getName()));
+    }
+  }
+
+  @Override
+  public Module create() throws InvalidPluginException {
+    Preconditions.checkState(command != null, "pluginName must be provided");
+    return this;
+  }
+}
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..bc094f9 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,16 @@
 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.ModuleGenerator;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
+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 +49,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 +95,16 @@
     install(new LifecycleModule() {
       @Override
       protected void configure() {
+        bind(ModuleGenerator.class).to(SshAutoRegisterModuleGenerator.class);
+        bind(SshPluginStarterCallback.class);
+        bind(StartPluginListener.class)
+          .annotatedWith(UniqueAnnotations.create())
+          .to(SshPluginStarterCallback.class);
+
+        bind(ReloadPluginListener.class)
+          .annotatedWith(UniqueAnnotations.create())
+          .to(SshPluginStarterCallback.class);
+
         listener().to(SshLog.class);
         listener().to(SshDaemon.class);
       }
@@ -120,6 +136,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..4f9fe33
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -0,0 +1,73 @@
+// 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.ReloadPluginListener;
+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, ReloadPluginListener {
+  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) {
+    Provider<Command> cmd = load(plugin);
+    if (cmd != null) {
+      plugin.add(root.register(Commands.named(plugin.getName()), cmd));
+    }
+  }
+
+  @Override
+  public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
+    Provider<Command> cmd = load(newPlugin);
+    if (cmd != null) {
+      newPlugin.add(root.replace(Commands.named(newPlugin.getName()), cmd));
+    }
+  }
+
+  private Provider<Command> load(Plugin plugin) {
+    if (plugin.getSshInjector() != null) {
+      Key<Command> key = Commands.key(plugin.getName());
+      try {
+        return plugin.getSshInjector().getProvider(key);
+      } catch (RuntimeException err) {
+        log.warn(String.format(
+            "Plugin %s did not define its top-level command",
+            plugin.getName()), err);
+      }
+    }
+    return null;
+  }
+}
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/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
index 34f64da..9eeaf74 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
@@ -36,5 +36,6 @@
     command(gerrit, "replicate").to(Replicate.class);
     command(gerrit, "set-project-parent").to(AdminSetParent.class);
     command(gerrit, "review").to(ReviewCommand.class);
+    command(gerrit, "set-account").to(SetAccountCommand.class);
   }
 }
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
new file mode 100644
index 0000000..28d267c
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginCommandModule.java
@@ -0,0 +1,49 @@
+// 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.Preconditions;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.sshd.CommandName;
+import com.google.gerrit.sshd.Commands;
+import com.google.gerrit.sshd.DispatchCommandProvider;
+import com.google.inject.AbstractModule;
+import com.google.inject.binder.LinkedBindingBuilder;
+
+import org.apache.sshd.server.Command;
+
+import javax.inject.Inject;
+
+public abstract class PluginCommandModule extends AbstractModule {
+  private CommandName command;
+
+  @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));
+    configureCommands();
+  }
+
+  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..4b76942
--- /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(true);
+  }
+}
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 8c27da0..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
@@ -39,6 +39,7 @@
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
@@ -180,8 +181,9 @@
     }
   }
 
-  private void approveOne(final PatchSet.Id patchSetId) throws
-      NoSuchChangeException, OrmException, EmailException, Failure {
+  private void approveOne(final PatchSet.Id patchSetId)
+      throws NoSuchChangeException, OrmException, EmailException, Failure,
+      RepositoryNotFoundException, IOException {
 
     if (changeComment == null) {
       changeComment = "";
@@ -200,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) {
@@ -261,6 +263,9 @@
         case GIT_ERROR:
           errMsg += "error writing change to git repository";
           break;
+        case DEST_BRANCH_NOT_FOUND:
+          errMsg += "destination branch not found";
+          break;
         default:
           errMsg += "failure in review";
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
new file mode 100644
index 0000000..9cf3586
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -0,0 +1,273 @@
+// 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.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Set a user's account settings. **/
+final class SetAccountCommand extends BaseCommand {
+
+  @Argument(index = 0, required = true, metaVar = "USER", usage = "full name, email-address, ssh username or account id")
+  private Account.Id id;
+
+  @Option(name = "--full-name", metaVar = "NAME", usage = "display name of the account")
+  private String fullName;
+
+  @Option(name = "--active", usage = "set account's state to active")
+  private boolean active;
+
+  @Option(name = "--inactive", usage = "set account's state to inactive")
+  private boolean inactive;
+
+  @Option(name = "--add-email", multiValued = true, metaVar = "EMAIL", usage = "email addresses to add to the account")
+  private List<String> addEmails = new ArrayList<String>();
+
+  @Option(name = "--delete-email", multiValued = true, metaVar = "EMAIL", usage = "email addresses to delete from the account")
+  private List<String> deleteEmails = new ArrayList<String>();
+
+  @Option(name = "--add-ssh-key", multiValued = true, metaVar = "-|KEY", usage = "public keys to add to the account")
+  private List<String> addSshKeys = new ArrayList<String>();
+
+  @Option(name = "--delete-ssh-key", multiValued = true, metaVar = "-|KEY", usage = "public keys to delete from the account")
+  private List<String> deleteSshKeys = new ArrayList<String>();
+
+  @Inject
+  private IdentifiedUser currentUser;
+
+  @Inject
+  private ReviewDb db;
+
+  @Inject
+  private AccountManager manager;
+
+  @Inject
+  private SshKeyCache sshKeyCache;
+
+  @Inject
+  private AccountCache byIdCache;
+
+  @Inject
+  private Realm realm;
+
+  @Override
+  public void start(final Environment env) {
+    startThread(new CommandRunnable() {
+      @Override
+      public void run() throws Exception {
+        if (!currentUser.getCapabilities().canAdministrateServer()) {
+          String msg =
+              String.format(
+                  "fatal: %s does not have \"Administrator\" capability.",
+                  currentUser.getUserName());
+          throw new UnloggedFailure(1, msg);
+        }
+        parseCommandLine();
+        validate();
+        setAccount();
+      }
+    });
+  }
+
+  private void validate() throws UnloggedFailure {
+    if (active && inactive) {
+      throw new UnloggedFailure(1,
+          "--active and --inactive options are mutually exclusive.");
+    }
+    if (addSshKeys.contains("-") && deleteSshKeys.contains("-")) {
+      throw new UnloggedFailure(1, "Only one option may use the stdin");
+    }
+    if (deleteSshKeys.contains("ALL")) {
+      deleteSshKeys = Collections.singletonList("ALL");
+    }
+    if (deleteEmails.contains("ALL")) {
+      deleteEmails = Collections.singletonList("ALL");
+    }
+  }
+
+  private void setAccount() throws OrmException, IOException, UnloggedFailure {
+
+    final Account account = db.accounts().get(id);
+    boolean accountUpdated = false;
+    boolean sshKeysUpdated = false;
+
+    for (String email : addEmails) {
+      link(id, email);
+    }
+
+    for (String email : deleteEmails) {
+      deleteMail(id, email);
+    }
+
+    if (fullName != null) {
+      if (realm.allowsEdit(FieldName.FULL_NAME)) {
+        account.setFullName(fullName);
+      } else {
+        throw new UnloggedFailure(1, "The realm doesn't allow editing names");
+      }
+    }
+
+    if (active) {
+      accountUpdated = true;
+      account.setActive(true);
+    } else if (inactive) {
+      accountUpdated = true;
+      account.setActive(false);
+    }
+
+    addSshKeys = readSshKey(addSshKeys);
+    if (!addSshKeys.isEmpty()) {
+      sshKeysUpdated = true;
+      addSshKeys(addSshKeys, account);
+    }
+
+    deleteSshKeys = readSshKey(deleteSshKeys);
+    if (!deleteSshKeys.isEmpty()) {
+      sshKeysUpdated = true;
+      deleteSshKeys(deleteSshKeys, account);
+    }
+
+    if (accountUpdated) {
+      db.accounts().update(Collections.singleton(account));
+      byIdCache.evict(id);
+    }
+
+    if (sshKeysUpdated) {
+      sshKeyCache.evict(account.getUserName());
+    }
+
+    db.close();
+  }
+
+  private void addSshKeys(final List<String> keys, final Account account)
+      throws OrmException, UnloggedFailure {
+    List<AccountSshKey> accountKeys = new ArrayList<AccountSshKey>();
+    int seq = db.accountSshKeys().byAccount(account.getId()).toList().size();
+    for (String key : keys) {
+      try {
+        seq++;
+        AccountSshKey accountSshKey = sshKeyCache.create(
+            new AccountSshKey.Id(account.getId(), seq), key.trim());
+        accountKeys.add(accountSshKey);
+      } catch (InvalidSshKeyException e) {
+        throw new UnloggedFailure(1, "fatal: invalid ssh key");
+      }
+    }
+    db.accountSshKeys().insert(accountKeys);
+  }
+
+  private void deleteSshKeys(final List<String> keys, final Account account)
+      throws OrmException {
+    ResultSet<AccountSshKey> allKeys = db.accountSshKeys().byAccount(account.getId());
+    if (keys.contains("ALL")) {
+      db.accountSshKeys().delete(allKeys);
+    } else {
+      List<AccountSshKey> accountKeys = new ArrayList<AccountSshKey>();
+      for (String key : keys) {
+        for (AccountSshKey accountSshKey : allKeys) {
+          if (key.trim().equals(accountSshKey.getSshPublicKey())
+              || accountSshKey.getComment().trim().equals(key)) {
+            accountKeys.add(accountSshKey);
+          }
+        }
+      }
+      db.accountSshKeys().delete(accountKeys);
+    }
+  }
+
+  private void deleteMail(Account.Id id, final String mailAddress)
+      throws UnloggedFailure, OrmException {
+    if (mailAddress.equals("ALL")) {
+      ResultSet<AccountExternalId> ids = db.accountExternalIds().byAccount(id);
+      for (AccountExternalId extId : ids) {
+        if (extId.isScheme(AccountExternalId.SCHEME_MAILTO)) {
+          unlink(id, extId.getEmailAddress());
+        }
+      }
+    } else {
+      AccountExternalId.Key key = new AccountExternalId.Key(
+          AccountExternalId.SCHEME_MAILTO, mailAddress);
+      AccountExternalId extId = db.accountExternalIds().get(key);
+      if (extId != null) {
+        unlink(id, mailAddress);
+      }
+    }
+  }
+
+  private void unlink(Account.Id id, final String mailAddress)
+      throws UnloggedFailure {
+    try {
+      manager.unlink(id, AuthRequest.forEmail(mailAddress));
+    } catch (AccountException ex) {
+      throw die(ex.getMessage());
+    }
+  }
+
+  private void link(Account.Id id, final String mailAddress)
+      throws UnloggedFailure {
+    try {
+      manager.link(id, AuthRequest.forEmail(mailAddress));
+    } catch (AccountException ex) {
+      throw die(ex.getMessage());
+    }
+  }
+
+  private List<String> readSshKey(final List<String> sshKeys)
+      throws UnsupportedEncodingException, IOException {
+    if (!sshKeys.isEmpty()) {
+      String sshKey = "";
+      int idx = sshKeys.indexOf("-");
+      if (idx >= 0) {
+        sshKey = "";
+        BufferedReader br =
+            new BufferedReader(new InputStreamReader(in, "UTF-8"));
+        String line;
+        while ((line = br.readLine()) != null) {
+          sshKey += line + "\n";
+        }
+        sshKeys.set(idx, sshKey);
+      }
+    }
+    return sshKeys;
+  }
+}
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 01b4a44..8db75e2 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.ehcache.EhcachePoolImpl;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
+import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.AuthType;
@@ -37,6 +38,8 @@
 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;
@@ -112,6 +115,11 @@
       sshInjector = createSshInjector();
       webInjector = createWebInjector();
 
+      PluginGuiceEnvironment env = sysInjector.getInstance(PluginGuiceEnvironment.class);
+      env.setCfgInjector(cfgInjector);
+      env.setSshInjector(sshInjector);
+      env.setHttpInjector(webInjector);
+
       // 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
@@ -197,6 +205,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() {
@@ -221,6 +230,7 @@
     modules.add(sshInjector.getInstance(WebSshGlueModule.class));
     modules.add(CacheBasedWebSession.module());
     modules.add(HttpContactStoreConnection.module());
+    modules.add(new HttpPluginModule());
 
     AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
     if (authConfig.getAuthType() == AuthType.OPENID) {
diff --git a/pom.xml b/pom.xml
index 8c14a87..f366c4d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -87,6 +87,9 @@
     <module>gerrit-gwtdebug</module>
     <module>gerrit-war</module>
 
+    <module>gerrit-extension-api</module>
+    <module>gerrit-plugin-api</module>
+
     <module>gerrit-gwtui</module>
   </modules>
 
@@ -333,7 +336,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-shade-plugin</artifactId>
-          <version>1.4</version>
+          <version>1.6</version>
         </plugin>
 
         <plugin>
@@ -425,6 +428,14 @@
         </configuration>
       </plugin>
     </plugins>
+
+    <extensions>
+      <extension>
+        <groupId>net.anzix.aws</groupId>
+        <artifactId>s3-maven-wagon</artifactId>
+        <version>3.2</version>
+      </extension>
+    </extensions>
   </build>
 
   <dependencies>
diff --git a/tools/deploy_api.sh b/tools/deploy_api.sh
new file mode 100755
index 0000000..eda841f
--- /dev/null
+++ b/tools/deploy_api.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+
+SRC=$(ls gerrit-plugin-api/target/gerrit-plugin-api-*-sources.jar)
+VER=${SRC#gerrit-plugin-api/target/gerrit-plugin-api-}
+VER=${VER%-sources.jar}
+
+type=release
+case $VER in
+*-SNAPSHOT)
+  echo >&2 "fatal: Cannot deploy $VER"
+  echo >&2 "       Use ./tools/version.sh --release && mvn clean package"
+  exit 1
+  ;;
+*-[0-9]*-g*) type=snapshot ;;
+esac
+URL=s3://gerrit-api@commondatastorage.googleapis.com/$type
+
+echo "Deploying API $VER to $URL"
+for module in gerrit-extension-api gerrit-plugin-api
+do
+  mvn deploy:deploy-file \
+    -DgroupId=com.google.gerrit \
+    -DartifactId=$module \
+    -Dversion=$VER \
+    -Dpackaging=jar \
+    -Dfile=$module/target/$module-$VER.jar \
+    -DrepositoryId=gerrit-api-repository \
+    -Durl=$URL
+
+  mvn deploy:deploy-file \
+    -DgroupId=com.google.gerrit \
+    -DartifactId=$module \
+    -Dversion=$VER \
+    -Dpackaging=java-source \
+    -Dfile=$module/target/$module-$VER-sources.jar \
+    -Djava-source=false \
+    -DrepositoryId=gerrit-api-repository \
+    -Durl=$URL
+done