Merge "Add notify section in project.config"
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-design.txt b/Documentation/dev-design.txt
index 59a5004..5cd62e3 100644
--- a/Documentation/dev-design.txt
+++ b/Documentation/dev-design.txt
@@ -87,7 +87,7 @@
Each Git commit created on the client desktop system is converted
into a unique change record which can be reviewed independently.
-Change records are stored in a database: PostgreSQL, MySql, or the
+Change records are stored in a database: PostgreSQL, MySQL, or the
built-in H2, where they can be queried to present customized user
dashboards, enumerating any pending changes.
@@ -669,11 +669,11 @@
Backups
~~~~~~~
-PostgreSQL can be configured to save its write-ahead-log (WAL)
-and ship these logs to other systems, where they are applied to
-a warm-standby backup in real time. Gerrit instances which care
-about reduduncy will setup this feature of PostgreSQL to ensure
-the warm-standby is reasonably current should the master go offline.
+PostgreSQL and MySQL can be configured to replicate their data to
+other systems, where they are applied to a warm-standby backup in
+real time. Gerrit instances which care about reduduncy will setup
+this feature of PostgreSQL or MySQL to ensure the warm-standby is
+reasonably current should the master go offline.
Gerrit can be configured to replicate changes made to the local
Git repositories over any standard Git transports. This can be
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..468e767
--- /dev/null
+++ b/Documentation/dev-plugins.txt
@@ -0,0 +1,62 @@
+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, download the sample maven project, which downloads the
+following dependencies:
+
+* gerrit-sdk.jar file that matches the war file to develop against
+
+
+Manifest
+--------
+
+Plugins need to include the following data in the jar manifest file:
+
+ Gerrit-Module = pkg.class
+
+Optionally include:
+
+ Gerrit-ReloadMode = 'reload' (default) or 'restart'
+
+If the plugin holds an exclusive resource that must be released before loading
+the plugin again, ReloadMode must be set to 'restart'. Otherwise 'reload' is
+sufficient.
+
+SSH Commands
+------------
+
+Plugins may provide commands that can be accessed through the SSH interface.
+These commands register themselves as a part of link:cmd-index.html[SSH Commands].
+
+Each of the plugin commands needs to extend SshCommand.
+
+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 must be overriden.
+
+Registering is done by calling:
+
+ command(String commandName).to(ClassName<? extends SshCommand> klass)
+
+Documentation
+-------------
+
+Place files into Documentation/ or static/ and package them into the plugin jar
+to access them in a browser via <canonicalWebURL>/plugins/<pluginName>/...
+
+Deployment
+----------
+
+Deploy plugins into <review_site>/plugins/. The file name in that directory will
+be the plugin name on the server.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-release-subproject.txt b/Documentation/dev-release-subproject.txt
new file mode 100644
index 0000000..a9d0553
--- /dev/null
+++ b/Documentation/dev-release-subproject.txt
@@ -0,0 +1,95 @@
+Making a Gerrit Sub Project Release
+===================================
+
+Preparing a New Gerrit Subproject Snapshot for Publishing
+---------------------------------------------------------
+
+* You will need to have the following in the pom.xml to make it deployable to:
+gerrit-maven-repository.googlecode.com
+----
+ <distributionManagement>
+ <snapshotRepository>
+ <id>gerrit-snapshot-repository</id>
+ <name>gerrit Snapshot Repository</name>
+ <url>dav:https://gerrit-maven-repository.googlecode.com/svn/</url>
+ <uniqueVersion>true</uniqueVersion>
+ </snapshotRepository>
+
+ <repository>
+ <id>gerrit-maven-repository</id>
+ <name>gerrit Maven Repository</name>
+ <url>dav:https://gerrit-maven-repository.googlecode.com/svn/</url>
+ <uniqueVersion>true</uniqueVersion>
+ </repository>
+ </distributionManagement>
+----
+
+
+* Since ubuntu maven is incomplete, also add this to the pom.xml:
+
+----
+ <build>
+ <extensions>
+ <extension>
+ <groupId>org.apache.maven.wagon</groupId>
+ <artifactId>wagon-webdav-jackrabbit</artifactId>
+ <version>1.0-beta-6</version>
+ </extension>
+ </extensions>
+ </build>
+----
+
+
+* Add your username and password to your ~/.m2/settings.xml file:
+
+----
+ <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
+ <servers>
+ <server>
+ <id>gerrit-maven-repository</id>
+ <username>JohnDoe@example.com</username>
+ <password>OpenSessame</password>
+ </server>
+
+ <server>
+ <id>gerrit-snapshot-repository</id>
+ <username>JohnDoe@example.com</username>
+ <password>OpenSessame</password>
+ </server>
+ </servers>
+ </settings>
+----
+
+
+Making a Gerrit Subproject Snapshot
+-----------------------------------
+
+* First build and deploy the latest snapshot and ensure that Gerrit builds
+with this snapshot
+
+* Deploy the snapshot:
+
+----
+ mvn deploy
+----
+
+
+Making a Gerrit Subproject Release
+----------------------------------
+
+* First deploy (and test) the latest snapshot for this subprojects
+
+* Update the top level pom.xml in the subproject to reflect the new project
+version (the exact value of the tag you will create below)
+
+* Commit the pom change and push to the project's repo refs/for/<master/stable>
+
+* Tag the version you just pushed (and push the tag)
+
+* Deploy the new release:
+
+----
+ mvn deploy
+----
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
new file mode 100644
index 0000000..83c47f3
--- /dev/null
+++ b/Documentation/dev-release.txt
@@ -0,0 +1,228 @@
+Making a Gerrit Release
+=======================
+
+[NOTE]
+========================================================================
+This document is meant primarily for Gerrit maintainers
+who have been given approval and submit status to the Gerrit
+projects. Additionally, maintainers should be given owner
+status to the Gerrit web site.
+========================================================================
+
+To make a Gerrit release involves a great deal of complex
+tasks and it is easy to miss a step so this document should
+hopefuly serve as both a how to for those new to the process
+and as a checklist for those already familiar with these
+tasks.
+
+
+Gerrit Release Type
+-------------------
+
+Here are some guidelines on release approaches depending on the
+type of release you want to make (stable-fix, stable, RC0, RC1...).
+
+Stable
+~~~~~~
+
+A stable release is generally built from the master branch and may need to
+undergo some stabilization before releasing the final release.
+
+* Propose the release with any plans/objectives to the mailing list
+
+* Create a Gerrit RC0
+
+* If needed create a Gerrit RC1
+
+[NOTE]
+========================================================================
+You may let in a few features to this release
+========================================================================
+
+* If needed create a Gerrit RC2
+
+[NOTE]
+========================================================================
+There should be no new features in this release, only bug fixes
+========================================================================
+
+* Finally create the stable release (no RC)
+
+
+Stable-Fix
+~~~~~~~~~~
+
+Stable-fix releases should likely only contain bug fixes and doc updates.
+
+* Propose the release with any plans/objectives to the mailing list
+
+* This type of release does not need any RCs, release when the objectives
+ are met
+
+
+
+Create the Actual Release
+---------------------------
+
+Prepare the Subprojects
+~~~~~~~~~~~~~~~~~~~~~~~
+
+* Publish the latest snapshot for all subprojects
+* Freeze all subprojects and link:dev-release-subproject.html[publish]
+ them!
+
+
+Prepare Gerrit
+~~~~~~~~~~~~~~
+
+* Update the top level pom in Gerrit to ensure that none of the Subprojects
+ point to snapshot releases
+
+* Update the poms for the Gerrit version, push for review, get merged
+
+====
+ tools/version.sh --snapshot=2.3
+====
+
+* Tag
+
+====
+ git tag -a -m "gerrit 2.2.2-rc0" v2.2.2-rc0
+ git tag -a -m "gerrit 2.2.2.1" v2.2.2.1
+====
+
+* Build
+
+====
+ ./tools/release.sh
+====
+
+* Sanity check WAR
+
+
+Publish to the Project Locations
+--------------------------------
+
+WAR File
+~~~~~~~~
+
+* Upload WAR to code.google.com/p/gerrit (manual web browser)
+** Go to http://code.google.com/p/gerrit/downloads/list
+** Use the "New Download" button
+
+* Update labels:
+** 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
+~~~
+
+* Push the New Tag
+
+====
+ git push google refs/tags/v2.2.2.1:refs/tags/v2.2.2.1
+====
+
+
+Docs
+~~~~
+
+====
+ make -C Documentation PRIOR=2.2.2 update
+ make -C ReleaseNotes update
+====
+
+(no +PRIOR=+... if updating the same release again during RCs)
+
+* Update Google Code project links
+** Go to http://code.google.com/p/gerrit/admin
+** Point the main page to the new docs
+** Point the main page to the new release notes
+
+[NOTE]
+========================================================================
+The docs makefile does an svn cp of the prior revision of the docs to branch
+the docs so you have less to upload on the new docs.
+
+User and password from here:
+
+ https://code.google.com/hosting/settings
+
+(requires overriding svn username on command line)
+========================================================================
+
+
+Issues
+~~~~~~
+
+====
+ How do the issues get updated? Do you run a script to do
+ this? When do you do it, after the final 2.2.2 is released?
+====
+
+By hand.
+
+Our current process is an issue should be updated to say Status =
+Submitted, FixedIn-2.2.2 once the change is submitted, but before the
+release.
+
+After the release is actually made, you can search in Google Code for
+``Status=Submitted FixedIn=2.2.2'' and then batch update these changes
+to say Status=Released. Make sure the pulldown says ``All Issues''
+because Status=Submitted is considered a closed issue.
+
+
+Mailing List
+~~~~~~~~~~~~
+
+* Send an email to the mailing list to annouce the release
+* Consider including some or all of the following in the email:
+** A link to the release and the release notes (if a final release)
+** A link to the docs
+** Describe the type of release (stable, bug fix, RC)
+
+----
+To: Repo and Gerrit Discussion <repo-discuss@googlegroups.com>
+Subject: Announce: Gerrit 2.2.2.1 (Stable bug fix update)
+
+I am pleased to announce Gerrit Code Review 2.2.2.1.
+
+Download:
+
+ http://code.google.com/p/gerrit/downloads/list
+
+
+This release is a stable bug fix release with some
+documentation updates including a new "Contributing to
+Gerrit" doc:
+
+ http://gerrit-documentation.googlecode.com/svn/Documentation/2.2.2/dev-contributing.html
+
+
+To read more about the bug fixes:
+
+ http://gerrit-documentation.googlecode.com/svn/ReleaseNotes/ReleaseNotes-2.2.2.1.html
+
+-Martin
+----
+
+
+Merging Stable Fixes to master
+------------------------------
+
+After every stable-fix release, stable should be merged to master to
+ensure that none of the fixes ever get lost.
+
+====
+ git config merge.summary true
+ git checkout master
+ git reset --hard origin/master
+ git branch -f stable origin/stable
+ git merge stable
+====
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 4c681bd..77904e5 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -51,6 +51,8 @@
* link:dev-contributing.html[Contributing to Gerrit]
* link:dev-design.html[System Design]
* link:i18n-readme.html[i18n Support]
+* link:dev-release.html[Developer Release]
+* link:dev-release-subproject.html[Developer Subproject Release]
Resources
---------
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/licenses.txt b/Documentation/licenses.txt
index 69018d8..e50979a 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -52,6 +52,7 @@
|JSR 305 | <<jsr305,New-Style BSD>>
|dk.brics.automaton | <<automaton,New-Style BSD>>
|Java Concurrency in Practice Annotations | <<jcip,Create Commons Attribution License>>
+|pegdown | <<apache2,Apache License 2.0>>
|======================================================================
Cryptography Notice
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..4811e40
--- /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("foo"))
+ * .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("foo"))
+ * .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/ChangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
index adacade..06c8e61 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
@@ -95,8 +95,8 @@
final native short _value()
/*-{
if (this.value) return this.value;
- if (this.recommended) return 1;
if (this.disliked) return -1;
+ if (this.recommended) return 1;
return 0;
}-*/;
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..a6b9429
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -0,0 +1,324 @@
+// 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.documentation.MarkdownFormatter;
+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 (file.startsWith("Documentation/") && !isValidEntry(entry)) {
+ entry = getRealFileEntry(jar, file);
+ if (isValidEntry(entry)) {
+ sendResource(jar, entry, res, holder.plugin.getName(), true);
+ return;
+ }
+ }
+ if (isValidEntry(entry)) {
+ sendResource(jar, entry, res, holder.plugin.getName());
+ return;
+ }
+ }
+
+ noCache(res);
+ res.sendError(HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ private JarEntry getRealFileEntry(JarFile jar, String file) {
+ // TODO: Replace with a loop iterating over possible formatters
+ return jar.getJarEntry(file.replaceAll("\\.html$", ".md"));
+ }
+
+ private boolean isValidEntry(JarEntry entry) {
+ return entry != null && entry.getSize() > 0;
+ }
+
+ private void sendResource(JarFile jar, JarEntry entry,
+ HttpServletResponse res, String pluginName) throws IOException {
+ sendResource(jar, entry, res, pluginName, false);
+ }
+
+ private void sendResource(JarFile jar, JarEntry entry,
+ HttpServletResponse res, String pluginName, boolean format)
+ throws IOException {
+ String entryName = entry.getName();
+ 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();
+ }
+ } else if (format == true) {
+ log.warn(String.format("Plugin '%s' file '%s' too large to format",
+ pluginName, entryName));
+ }
+
+ String contentType = null;
+ String charEnc = null;
+ Attributes atts = entry.getAttributes();
+ if (atts != null) {
+ contentType = Strings.emptyToNull(atts.getValue("Content-Type"));
+ charEnc = Strings.emptyToNull(atts.getValue("Character-Encoding"));
+ }
+
+ if (contentType == null) {
+ MimeType type = mimeUtil.getMimeType(entryName, data);
+ contentType = type.toString();
+ }
+
+ if (format && data != null) {
+ if (charEnc == null) {
+ charEnc = "UTF-8";
+ }
+ MarkdownFormatter fmter = new MarkdownFormatter();
+ data = fmter.getHtmlFromMarkdown(data, charEnc);
+ res.setHeader("Content-Length", Long.toString(data.length));
+ contentType = "text/html";
+ } else {
+ res.setHeader("Content-Length", Long.toString(entry.getSize()));
+ }
+
+ long time = entry.getTime();
+ if (0 < time) {
+ res.setDateHeader("Last-Modified", time);
+ }
+ res.setContentType(contentType);
+ if (charEnc != null) {
+ res.setCharacterEncoding(charEnc);
+ }
+ 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/ExportReviewNotes.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
index 5d7983b..5f0bc80 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
@@ -97,7 +97,6 @@
Scopes.SINGLETON);
bind(String.class).annotatedWith(CanonicalWebUrl.class)
.toProvider(CanonicalWebUrlProvider.class).in(Scopes.SINGLETON);
- bind(CachePool.class);
install(AccountCacheImpl.module());
install(GroupCacheImpl.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..ceb3c55 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>
@@ -164,6 +170,11 @@
<groupId>com.googlecode.prolog-cafe</groupId>
<artifactId>PrologCafe</artifactId>
</dependency>
+
+ <dependency>
+ <groupId>org.pegdown</groupId>
+ <artifactId>pegdown</artifactId>
+ </dependency>
</dependencies>
<build>
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 09c33c0..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
@@ -133,6 +133,8 @@
private final EventFactory eventFactory;
+ private final SitePaths sitePaths;
+
/**
* Create a new ChangeHookRunner.
*
@@ -149,7 +151,7 @@
final @AnonymousCowardName String anonymousCowardName,
final SitePaths sitePath, final ProjectCache projectCache,
final AccountCache accountCache, final ApprovalTypes approvalTypes,
- final EventFactory eventFactory) {
+ final EventFactory eventFactory, final SitePaths sitePaths) {
this.anonymousCowardName = anonymousCowardName;
this.repoManager = repoManager;
this.hookQueue = queue.createQueue(1, "hook");
@@ -157,6 +159,7 @@
this.accountCache = accountCache;
this.approvalTypes = approvalTypes;
this.eventFactory = eventFactory;
+ this.sitePaths = sitePath;
final File hooksPath = sitePath.resolve(getValue(config, "hooks", "path", sitePath.hooks_dir.getAbsolutePath()));
@@ -481,10 +484,12 @@
repo = openRepository(project);
}
+ final Map<String, String> env = pb.environment();
+ env.put("GERRIT_SITE", sitePaths.site_path.getAbsolutePath());
+
if (repo != null) {
pb.directory(repo.getDirectory());
- final Map<String, String> env = pb.environment();
env.put("GIT_DIR", repo.getDirectory().getAbsolutePath());
}
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/documentation/MarkdownFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
new file mode 100644
index 0000000..2f6b422
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -0,0 +1,35 @@
+// 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.documentation;
+
+import static org.pegdown.Extensions.ALL;
+
+import org.eclipse.jgit.util.RawParseUtils;
+import org.pegdown.PegDownProcessor;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+public class MarkdownFormatter {
+
+ public byte[] getHtmlFromMarkdown(byte[] data, String charEnc)
+ throws UnsupportedEncodingException {
+ String decodedData = RawParseUtils.decode(Charset.forName(charEnc), data);
+ String formatted = new PegDownProcessor(ALL).markdownToHtml(decodedData);
+ data = formatted.getBytes(charEnc);
+ return data;
+ }
+ // TODO: Add a cache
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
index 5bff0ad..05032b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
@@ -113,8 +113,8 @@
database = db;
replicationUserFactory = ruf;
gitRepositoryManager = grm;
- configs = allConfigs(site);
groupCache = gc;
+ configs = allConfigs(site);
}
@Override
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..92e3b1dd
--- /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 a926e77..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
@@ -14,6 +14,7 @@
package com.google.gerrit.sshd;
+import com.google.common.util.concurrent.Atomics;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gerrit.server.CurrentUser;
@@ -50,6 +51,7 @@
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicReference;
public abstract class BaseCommand implements Command {
private static final Logger log = LoggerFactory.getLogger(BaseCommand.class);
@@ -87,7 +89,7 @@
private Provider<SshScope.Context> contextProvider;
/** The task, as scheduled on a worker thread. */
- private Future<?> task;
+ private final AtomicReference<Future<?>> task;
/** Text of the command line which lead up to invoking this instance. */
private String commandName = "";
@@ -95,6 +97,10 @@
/** Unparsed command line options. */
private String[] argv;
+ public BaseCommand() {
+ task = Atomics.newReference();
+ }
+
public void setInputStream(final InputStream in) {
this.in = in;
}
@@ -121,8 +127,9 @@
@Override
public void destroy() {
- if (task != null && !task.isDone()) {
- task.cancel(true);
+ Future<?> future = task.getAndSet(null);
+ if (future != null && !future.isDone()) {
+ future.cancel(true);
}
}
@@ -243,25 +250,21 @@
* @param thunk the runnable to execute on the thread, performing the
* command's logic.
*/
- protected synchronized void startThread(final CommandRunnable thunk) {
+ protected void startThread(final CommandRunnable thunk) {
final TaskThunk tt = new TaskThunk(thunk);
- if (isAdminCommand() || (isAdminHighPriorityCommand()
- && userProvider.get().getCapabilities().canAdministrateServer())) {
+ if (isAdminHighPriorityCommand()
+ && userProvider.get().getCapabilities().canAdministrateServer()) {
// Admin commands should not block the main work threads (there
// might be an interactive shell there), nor should they wait
// for the main work threads.
//
new Thread(tt, tt.toString()).start();
} else {
- task = executor.submit(tt);
+ task.set(executor.submit(tt));
}
}
- private final boolean isAdminCommand() {
- return getClass().getAnnotation(AdminCommand.class) != null;
- }
-
private final boolean isAdminHighPriorityCommand() {
return getClass().getAnnotation(AdminHighPriorityCommand.class) != null;
}
@@ -505,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/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index fb17169..66e6add 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -14,6 +14,8 @@
package com.google.gerrit.sshd;
+import com.google.common.util.concurrent.Atomics;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.sshd.SshScope.Context;
@@ -36,6 +38,11 @@
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
/**
* Creates a CommandFactory using commands registered by {@link CommandModule}.
@@ -46,7 +53,8 @@
private final DispatchCommandProvider dispatcher;
private final SshLog log;
- private final Executor startExecutor;
+ private final ScheduledExecutorService startExecutor;
+ private final Executor destroyExecutor;
@Inject
CommandFactoryProvider(
@@ -58,6 +66,11 @@
int threads = cfg.getInt("sshd","commandStartThreads", 2);
startExecutor = workQueue.createQueue(threads, "SshCommandStart");
+ destroyExecutor = Executors.newSingleThreadExecutor(
+ new ThreadFactoryBuilder()
+ .setNameFormat("SshCommandDestroy-%s")
+ .setDaemon(true)
+ .build());
}
@Override
@@ -79,11 +92,14 @@
private Environment env;
private Context ctx;
private DispatchCommand cmd;
- private boolean logged;
+ private final AtomicBoolean logged;
+ private final AtomicReference<Future<?>> task;
Trampoline(final String cmdLine) {
commandLine = cmdLine;
argv = split(cmdLine);
+ logged = new AtomicBoolean();
+ task = Atomics.newReference();
}
public void setInputStream(final InputStream in) {
@@ -110,7 +126,7 @@
public void start(final Environment env) throws IOException {
this.env = env;
final Context ctx = this.ctx;
- startExecutor.execute(new Runnable() {
+ task.set(startExecutor.submit(new Runnable() {
public void run() {
try {
onStart();
@@ -124,7 +140,7 @@
public String toString() {
return "start (user " + ctx.getSession().getUsername() + ")";
}
- });
+ }));
}
private void onStart() throws IOException {
@@ -173,16 +189,26 @@
}
private void log(final int rc) {
- synchronized (this) {
- if (!logged) {
- log.onExecute(rc);
- logged = true;
- }
+ if (logged.compareAndSet(false, true)) {
+ log.onExecute(rc);
}
}
@Override
public void destroy() {
+ Future<?> future = task.getAndSet(null);
+ if (future != null) {
+ future.cancel(true);
+ destroyExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ onDestroy();
+ }
+ });
+ }
+ }
+
+ private void onDestroy() {
synchronized (this) {
if (cmd != null) {
final Context old = SshScope.set(ctx);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index 8daa7f4..351b2e7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -14,7 +14,9 @@
package com.google.gerrit.sshd;
+import com.google.common.util.concurrent.Atomics;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.sshd.args4j.SubcommandHandler;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -28,6 +30,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
/**
* Command that dispatches to a subcommand from its command table.
@@ -40,7 +43,7 @@
private final Provider<CurrentUser> currentUser;
private final String prefix;
private final Map<String, Provider<Command>> commands;
- private Command cmd;
+ private final AtomicReference<Command> atomicCmd;
@Argument(index = 0, required = true, metaVar = "COMMAND", handler = SubcommandHandler.class)
private String commandName;
@@ -54,6 +57,7 @@
currentUser = cu;
prefix = pfx;
commands = all;
+ atomicCmd = Atomics.newReference();
}
@Override
@@ -70,13 +74,7 @@
}
final Command cmd = p.get();
-
- if (isAdminCommand(cmd)
- && !currentUser.get().getCapabilities().canAdministrateServer()) {
- final String msg = "fatal: Not a Gerrit administrator";
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
-
+ checkRequiresCapability(cmd);
if (cmd instanceof BaseCommand) {
final BaseCommand bc = (BaseCommand) cmd;
if (prefix.isEmpty())
@@ -90,10 +88,7 @@
}
provideStateTo(cmd);
-
- synchronized (this) {
- this.cmd = cmd;
- }
+ atomicCmd.set(cmd);
cmd.start(env);
} catch (UnloggedFailure e) {
@@ -107,17 +102,25 @@
}
}
- private boolean isAdminCommand(final Command cmd) {
- return cmd.getClass().getAnnotation(AdminCommand.class) != null;
+ private void checkRequiresCapability(Command cmd) throws UnloggedFailure {
+ RequiresCapability rc = cmd.getClass().getAnnotation(RequiresCapability.class);
+ if (rc != null) {
+ CurrentUser user = currentUser.get();
+ CapabilityControl ctl = user.getCapabilities();
+ if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
+ String msg = String.format(
+ "fatal: %s does not have \"%s\" capability.",
+ user.getUserName(), rc.value());
+ throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+ }
+ }
}
@Override
public void destroy() {
- synchronized (this) {
- if (cmd != null) {
+ Command cmd = atomicCmd.getAndSet(null);
+ if (cmd != null) {
cmd.destroy();
- cmd = null;
- }
}
}
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/AdminCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/RequiresCapability.java
similarity index 68%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/AdminCommand.java
rename to gerrit-sshd/src/main/java/com/google/gerrit/sshd/RequiresCapability.java
index adaf646..cc41a79 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AdminCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/RequiresCapability.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 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.
@@ -21,12 +21,10 @@
import java.lang.annotation.Target;
/**
- * Annotation tagged on a concrete Command that requires administrator access.
- * <p>
- * Currently this annotation is only enforced by DispatchCommand after it has
- * created the command object, but before it populates it or starts execution.
+ * Annotation on {@link SshCommand} declaring a capability must be granted.
*/
-@Target( {ElementType.TYPE})
+@Target({ElementType.TYPE})
@Retention(RUNTIME)
-public @interface AdminCommand {
+public @interface RequiresCapability {
+ String value();
}
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/SshCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCommand.java
new file mode 100644
index 0000000..f6209ba
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCommand.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.sshd;
+
+import org.apache.sshd.server.Environment;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+public abstract class SshCommand extends BaseCommand {
+ protected PrintWriter stdout;
+ protected PrintWriter stderr;
+
+ @Override
+ public void start(Environment env) throws IOException {
+ startThread(new CommandRunnable() {
+ @Override
+ public void run() throws Exception {
+ parseCommandLine();
+ stdout = toPrintWriter(out);
+ stderr = toPrintWriter(err);
+ try {
+ SshCommand.this.run();
+ } finally {
+ stdout.flush();
+ stderr.flush();
+ }
+ }
+ });
+ }
+
+ protected abstract void run() throws UnloggedFailure, Failure, Exception;
+}
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/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
index 492966e..b4c59a8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -14,6 +14,7 @@
package com.google.gerrit.sshd;
+import com.google.common.util.concurrent.Atomics;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.CurrentUser;
@@ -32,6 +33,7 @@
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
/**
* Executes any other command as a different user identity.
@@ -57,7 +59,7 @@
@Argument(index = 0, multiValued = true, metaVar = "COMMAND")
private List<String> args = new ArrayList<String>();
- private Command cmd;
+ private final AtomicReference<Command> atomicCmd;
@Inject
SuExec(@CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
@@ -69,6 +71,7 @@
this.session = session;
this.userFactory = userFactory;
this.callingContext = callingContext;
+ atomicCmd = Atomics.newReference();
}
@Override
@@ -83,10 +86,7 @@
final BaseCommand cmd = dispatcher.get();
cmd.setArguments(args.toArray(new String[args.size()]));
provideStateTo(cmd);
-
- synchronized (this) {
- this.cmd = cmd;
- }
+ atomicCmd.set(cmd);
cmd.start(env);
} finally {
SshScope.set(old);
@@ -136,11 +136,9 @@
@Override
public void destroy() {
- synchronized (this) {
- if (cmd != null) {
+ Command cmd = atomicCmd.getAndSet(null);
+ if (cmd != null) {
cmd.destroy();
- cmd = 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/AdminQueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
index 545554c..5f1992c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
@@ -14,16 +14,18 @@
package com.google.gerrit.sshd.commands;
-import com.google.gerrit.sshd.AdminCommand;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.RequiresCapability;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Option;
/** Opens a query processor. */
-@AdminCommand
-final class AdminQueryShell extends BaseCommand {
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class AdminQueryShell extends SshCommand {
@Inject
private QueryShell.Factory factory;
@@ -34,19 +36,13 @@
private String query;
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- parseCommandLine();
- final QueryShell shell = factory.create(in, out);
- shell.setOutputFormat(format);
- if (query != null) {
- shell.execute(query);
- } else {
- shell.run();
- }
- }
- });
+ protected void run() {
+ final QueryShell shell = factory.create(in, out);
+ shell.setOutputFormat(format);
+ if (query != null) {
+ shell.execute(query);
+ } else {
+ shell.run();
+ }
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index 950bf34..047cdd4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -14,6 +14,7 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.MetaDataUpdate;
@@ -21,11 +22,10 @@
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.sshd.AdminCommand;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.RequiresCapability;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.kohsuke.args4j.Argument;
@@ -34,14 +34,13 @@
import org.slf4j.LoggerFactory;
import java.io.IOException;
-import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-@AdminCommand
-final class AdminSetParent extends BaseCommand {
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class AdminSetParent extends SshCommand {
private static final Logger log = LoggerFactory.getLogger(AdminSetParent.class);
@Option(name = "--parent", aliases = {"-p"}, metaVar = "NAME", usage = "new parent project")
@@ -68,26 +67,10 @@
@Inject
private AllProjectsName allProjectsName;
- private PrintWriter stdout;
private Project.NameKey newParentKey = null;
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- stdout = toPrintWriter(out);
- try {
- parseCommandLine();
- updateParents();
- } finally {
- stdout.flush();
- }
- }
- });
- }
-
- private void updateParents() throws Failure {
+ protected void run() throws Failure {
if (oldParent == null && children.isEmpty()) {
throw new UnloggedFailure(1, "fatal: child projects have to be specified as " +
"arguments or the --children-of option has to be set");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index fd58221..f13e1a6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -20,10 +20,9 @@
import com.google.gerrit.server.git.IncompleteUserInfoException;
import com.google.gerrit.server.git.MergeException;
import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.eclipse.jgit.lib.ObjectId;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -33,8 +32,7 @@
import java.util.ArrayList;
import java.util.List;
-public class BanCommitCommand extends BaseCommand {
-
+public class BanCommitCommand extends SshCommand {
@Option(name = "--reason", aliases = {"-r"}, metaVar = "REASON", usage = "reason for banning the commit")
private String reason;
@@ -50,45 +48,30 @@
private BanCommit.Factory banCommitFactory;
@Override
- public void start(final Environment env) throws IOException {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- parseCommandLine();
- BanCommitCommand.this.display();
- }
- });
- }
-
- private void display() throws Failure {
+ protected void run() throws Failure {
try {
final BanCommitResult result =
banCommitFactory.create().ban(projectControl, commitsToBan, reason);
- final PrintWriter stdout = toPrintWriter(out);
- try {
- final List<ObjectId> newlyBannedCommits =
- result.getNewlyBannedCommits();
- if (!newlyBannedCommits.isEmpty()) {
- stdout.print("The following commits were banned:\n");
- printCommits(stdout, newlyBannedCommits);
- }
+ final List<ObjectId> newlyBannedCommits =
+ result.getNewlyBannedCommits();
+ if (!newlyBannedCommits.isEmpty()) {
+ stdout.print("The following commits were banned:\n");
+ printCommits(stdout, newlyBannedCommits);
+ }
- final List<ObjectId> alreadyBannedCommits =
- result.getAlreadyBannedCommits();
- if (!alreadyBannedCommits.isEmpty()) {
- stdout.print("The following commits were already banned:\n");
- printCommits(stdout, alreadyBannedCommits);
- }
+ final List<ObjectId> alreadyBannedCommits =
+ result.getAlreadyBannedCommits();
+ if (!alreadyBannedCommits.isEmpty()) {
+ stdout.print("The following commits were already banned:\n");
+ printCommits(stdout, alreadyBannedCommits);
+ }
- final List<ObjectId> ignoredIds = result.getIgnoredObjectIds();
- if (!ignoredIds.isEmpty()) {
- stdout.print("The following ids do not represent commits"
- + " and were ignored:\n");
- printCommits(stdout, ignoredIds);
- }
- } finally {
- stdout.flush();
+ final List<ObjectId> ignoredIds = result.getIgnoredObjectIds();
+ if (!ignoredIds.isEmpty()) {
+ stdout.print("The following ids do not represent commits"
+ + " and were ignored:\n");
+ printCommits(stdout, ignoredIds);
}
} catch (PermissionDeniedException e) {
throw die(e);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
index 083759c..1e7c5b3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CacheCommand.java
@@ -15,7 +15,7 @@
package com.google.gerrit.sshd.commands;
import com.google.gerrit.ehcache.EhcachePoolImpl;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
import net.sf.ehcache.CacheManager;
@@ -25,7 +25,7 @@
import java.util.SortedSet;
import java.util.TreeSet;
-abstract class CacheCommand extends BaseCommand {
+abstract class CacheCommand extends SshCommand {
@Inject
protected EhcachePoolImpl cachePool;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index 38e0bcb..29f2294 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -14,6 +14,7 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -26,12 +27,12 @@
import com.google.gerrit.server.account.AccountByEmailCache;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.RequiresCapability;
+import com.google.gerrit.sshd.SshCommand;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -45,7 +46,8 @@
import java.util.List;
/** Create a new user account. **/
-final class CreateAccountCommand extends BaseCommand {
+@RequiresCapability(GlobalCapability.CREATE_ACCOUNT)
+final class CreateAccountCommand extends SshCommand {
@Option(name = "--group", aliases = {"-g"}, metaVar = "GROUP", usage = "groups to add account to")
private List<AccountGroup.Id> groups = new ArrayList<AccountGroup.Id>();
@@ -77,24 +79,7 @@
private AccountByEmailCache byEmailCache;
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- if (!currentUser.getCapabilities().canCreateAccount()) {
- String msg = String.format(
- "fatal: %s does not have \"Create Account\" capability.",
- currentUser.getUserName());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
-
- parseCommandLine();
- createAccount();
- }
- });
- }
-
- private void createAccount() throws OrmException, IOException,
+ protected void run() throws OrmException, IOException,
InvalidSshKeyException, UnloggedFailure {
if (!username.matches(Account.USER_NAME_PATTERN)) {
throw die("Username '" + username + "'"
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 98202e2..28b6f48 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -14,15 +14,17 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.common.errors.PermissionDeniedException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.account.PerformCreateGroup;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.RequiresCapability;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -34,7 +36,8 @@
* <p>
* Optionally, puts an initial set of user in the newly created group.
*/
-final class CreateGroupCommand extends BaseCommand {
+@RequiresCapability(GlobalCapability.CREATE_GROUP)
+final class CreateGroupCommand extends SshCommand {
@Option(name = "--owner", aliases = {"-o"}, metaVar = "GROUP", usage = "owning group, if not specified the group will be self-owning")
private AccountGroup.Id ownerGroupId;
@@ -65,25 +68,19 @@
private PerformCreateGroup.Factory performCreateGroupFactory;
@Override
- public void start(Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- parseCommandLine();
- try {
- performCreateGroupFactory.create().createGroup(groupName,
- groupDescription,
- visibleToAll,
- ownerGroupId,
- initialMembers,
- initialGroups);
- } catch (PermissionDeniedException e) {
- throw die(e);
+ protected void run() throws Failure, OrmException {
+ try {
+ performCreateGroupFactory.create().createGroup(groupName,
+ groupDescription,
+ visibleToAll,
+ ownerGroupId,
+ initialMembers,
+ initialGroups);
+ } catch (PermissionDeniedException e) {
+ throw die(e);
- } catch (NameAlreadyUsedException e) {
- throw die(e);
- }
- }
- });
+ } catch (NameAlreadyUsedException e) {
+ throw die(e);
+ }
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index a93cab1..1f5bc6f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -14,28 +14,28 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.errors.ProjectCreationFailedException;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.project.CreateProject;
import com.google.gerrit.server.project.CreateProjectArgs;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.SuggestParentCandidates;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.RequiresCapability;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.eclipse.jgit.lib.Constants;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
-import java.io.PrintWriter;
import java.util.List;
/** Create a new project. **/
-final class CreateProjectCommand extends BaseCommand {
+@RequiresCapability(GlobalCapability.CREATE_PROJECT)
+final class CreateProjectCommand extends SshCommand {
@Option(name = "--name", aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created (deprecated option)")
void setProjectNameFromOption(String name) {
if (projectName != null) {
@@ -96,64 +96,45 @@
}
@Inject
- private IdentifiedUser currentUser;
-
- @Inject
private CreateProject.Factory CreateProjectFactory;
@Inject
private SuggestParentCandidates.Factory suggestParentCandidatesFactory;
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- if (!currentUser.getCapabilities().canCreateProject()) {
- String msg =
- String.format(
- "fatal: %s does not have \"Create Project\" capability.",
- currentUser.getUserName());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+ protected void run() throws Exception {
+ try {
+ if (!suggestParent) {
+ if (projectName == null) {
+ throw new UnloggedFailure(1, "fatal: Project name is required.");
}
- PrintWriter p = toPrintWriter(out);
- parseCommandLine();
- try {
- if (!suggestParent) {
- if (projectName == null) {
- throw new UnloggedFailure(1, "fatal: Project name is required.");
- }
- final CreateProjectArgs args = new CreateProjectArgs();
- args.setProjectName(projectName);
- args.ownerIds = ownerIds;
- args.newParent = newParent;
- args.permissionsOnly = permissionsOnly;
- args.projectDescription = projectDescription;
- args.submitType = submitType;
- args.contributorAgreements = contributorAgreements;
- args.signedOffBy = signedOffBy;
- args.contentMerge = contentMerge;
- args.changeIdRequired = requireChangeID;
- args.branch = branch;
- args.createEmptyCommit = createEmptyCommit;
+ final CreateProjectArgs args = new CreateProjectArgs();
+ args.setProjectName(projectName);
+ args.ownerIds = ownerIds;
+ args.newParent = newParent;
+ args.permissionsOnly = permissionsOnly;
+ args.projectDescription = projectDescription;
+ args.submitType = submitType;
+ args.contributorAgreements = contributorAgreements;
+ args.signedOffBy = signedOffBy;
+ args.contentMerge = contentMerge;
+ args.changeIdRequired = requireChangeID;
+ args.branch = branch;
+ args.createEmptyCommit = createEmptyCommit;
- final CreateProject createProject =
- CreateProjectFactory.create(args);
- createProject.createProject();
- } else {
- List<Project.NameKey> parentCandidates =
- suggestParentCandidatesFactory.create().getNameKeys();
+ final CreateProject createProject =
+ CreateProjectFactory.create(args);
+ createProject.createProject();
+ } else {
+ List<Project.NameKey> parentCandidates =
+ suggestParentCandidatesFactory.create().getNameKeys();
- for (Project.NameKey parent : parentCandidates) {
- p.print(parent + "\n");
- }
- }
- } catch (ProjectCreationFailedException err) {
- throw new UnloggedFailure(1, "fatal: " + err.getMessage(), err);
- } finally {
- p.flush();
+ for (Project.NameKey parent : parentCandidates) {
+ stdout.print(parent + "\n");
}
}
- });
+ } catch (ProjectCreationFailedException err) {
+ throw new UnloggedFailure(1, "fatal: " + err.getMessage(), err);
+ }
}
}
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/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
index 639cc42..9ba20ed 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -14,21 +14,22 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.RequiresCapability;
import com.google.inject.Inject;
import net.sf.ehcache.Ehcache;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Option;
-import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;
/** Causes the caches to purge all entries and reload. */
+@RequiresCapability(GlobalCapability.FLUSH_CACHES)
final class FlushCaches extends CacheCommand {
private static final String WEB_SESSIONS = "web_sessions";
@@ -44,27 +45,8 @@
@Inject
IdentifiedUser currentUser;
- private PrintWriter p;
-
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- if (!currentUser.getCapabilities().canFlushCaches()) {
- String msg = String.format(
- "fatal: %s does not have \"Flush Caches\" capability.",
- currentUser.getUserName());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
-
- parseCommandLine();
- flush();
- }
- });
- }
-
- private void flush() throws Failure {
+ protected void run() throws Failure {
if (caches.contains(WEB_SESSIONS)
&& !currentUser.getCapabilities().canAdministrateServer()) {
String msg = String.format(
@@ -73,7 +55,6 @@
throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
}
- p = toPrintWriter(err);
if (list) {
if (all || caches.size() > 0) {
throw error("error: cannot use --list with --all or --cache");
@@ -106,10 +87,10 @@
private void doList() {
for (final String name : cacheNames()) {
- p.print(name);
- p.print('\n');
+ stderr.print(name);
+ stderr.print('\n');
}
- p.flush();
+ stderr.flush();
}
private void doBulkFlush() {
@@ -120,12 +101,12 @@
try {
c.removeAll();
} catch (Throwable e) {
- p.println("error: cannot flush cache \"" + name + "\": " + e);
+ stderr.println("error: cannot flush cache \"" + name + "\": " + e);
}
}
}
} finally {
- p.flush();
+ stderr.flush();
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
index 69018af..12ab225 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
@@ -14,25 +14,24 @@
package com.google.gerrit.sshd.commands;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.git.WorkQueue.Task;
import com.google.gerrit.server.util.IdGenerator;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.RequiresCapability;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
-import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Set;
/** Kill a task in the work queue. */
-final class KillCommand extends BaseCommand {
- @Inject
- private IdentifiedUser currentUser;
-
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.KILL_TASK)
+final class KillCommand extends SshCommand {
@Inject
private WorkQueue workQueue;
@@ -48,33 +47,14 @@
}
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- if (!currentUser.getCapabilities().canKillTask()) {
- String msg = String.format(
- "fatal: %s does not have \"Kill Task\" capability.",
- currentUser.getUserName());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
-
- parseCommandLine();
- KillCommand.this.commitMurder();
- }
- });
- }
-
- private void commitMurder() {
- final PrintWriter p = toPrintWriter(err);
+ protected void run() {
for (final Integer id : taskIds) {
final Task<?> task = workQueue.getTask(id);
if (task != null) {
task.cancel(true);
} else {
- p.print("kill: " + IdGenerator.format(id) + ": No such task\n");
+ stderr.print("kill: " + IdGenerator.format(id) + ": No such task\n");
}
}
- p.flush();
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index e0b988e..a729f43 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -22,20 +22,16 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.VisibleGroups;
import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Option;
-import java.io.IOException;
-import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
-public class ListGroupsCommand extends BaseCommand {
-
+public class ListGroupsCommand extends SshCommand {
@Inject
private VisibleGroups.Factory visibleGroupsFactory;
@@ -57,18 +53,7 @@
private Account.Id user;
@Override
- public void start(final Environment env) throws IOException {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- parseCommandLine();
- ListGroupsCommand.this.display();
- }
- });
- }
-
- private void display() throws Failure {
- final PrintWriter stdout = toPrintWriter(out);
+ protected void run() throws Failure {
try {
if (user != null && !projects.isEmpty()) {
throw new UnloggedFailure(1, "fatal: --user and --project options are not compatible.");
@@ -92,8 +77,6 @@
throw die(e);
} catch (NoSuchGroupException e) {
throw die(e);
- } finally {
- stdout.flush();
}
}
}
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/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
index d9a1c3f..2db13a4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
@@ -15,16 +15,15 @@
package com.google.gerrit.sshd.commands;
import com.google.gerrit.server.query.change.QueryProcessor;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import java.util.List;
-class Query extends BaseCommand {
+class Query extends SshCommand {
@Inject
private QueryProcessor processor;
@@ -75,19 +74,14 @@
private List<String> query;
@Override
- public void start(Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- processor.setOutput(out, QueryProcessor.OutputFormat.TEXT);
- parseCommandLine();
- verifyCommandLine();
- processor.query(join(query, " "));
- }
- });
+ protected void run() throws Exception {
+ processor.query(join(query, " "));
}
- private void verifyCommandLine() throws UnloggedFailure {
+ @Override
+ protected void parseCommandLine() throws UnloggedFailure {
+ processor.setOutput(out, QueryProcessor.OutputFormat.TEXT);
+ super.parseCommandLine();
if (processor.getIncludeFiles() &&
!(processor.getIncludePatchSets() || processor.getIncludeCurrentPatchSet())) {
throw new UnloggedFailure(1, "--files option needs --patch-sets or --current-patch-set");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
index 5b6cf39..ad9733d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -17,17 +17,13 @@
import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.server.account.PerformRenameGroup;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
-import java.io.IOException;
-
-public class RenameGroupCommand extends BaseCommand {
-
+public class RenameGroupCommand extends SshCommand {
@Argument(index = 0, required = true, metaVar = "GROUP", usage = "name of the group to be renamed")
private String groupName;
@@ -38,21 +34,15 @@
private PerformRenameGroup.Factory performRenameGroupFactory;
@Override
- public void start(final Environment env) throws IOException {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- parseCommandLine();
- try {
- performRenameGroupFactory.create().renameGroup(groupName, newGroupName);
- } catch (OrmException e) {
- throw die(e);
- } catch (NameAlreadyUsedException e) {
- throw die(e);
- } catch (NoSuchGroupException e) {
- throw die(e);
- }
- }
- });
+ protected void run() throws Failure {
+ try {
+ performRenameGroupFactory.create().renameGroup(groupName, newGroupName);
+ } catch (OrmException e) {
+ throw die(e);
+ } catch (NameAlreadyUsedException e) {
+ throw die(e);
+ } catch (NoSuchGroupException e) {
+ throw die(e);
+ }
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
index d56d1cd..0a3f336 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
@@ -14,15 +14,16 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.PushAllProjectsOp;
import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.RequiresCapability;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -31,7 +32,8 @@
import java.util.concurrent.TimeUnit;
/** Force a project to replicate, again. */
-final class Replicate extends BaseCommand {
+@RequiresCapability(GlobalCapability.START_REPLICATION)
+final class Replicate extends SshCommand {
@Option(name = "--all", usage = "push all known projects")
private boolean all;
@@ -54,24 +56,7 @@
private ProjectCache projectCache;
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- if (!currentUser.getCapabilities().canStartReplication()) {
- String msg = String.format(
- "fatal: %s does not have \"Start Replication\" capability.",
- currentUser.getUserName());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
-
- parseCommandLine();
- Replicate.this.schedule();
- }
- });
- }
-
- private void schedule() throws Failure {
+ protected void run() throws Failure {
if (all && projectNames.size() > 0) {
throw new UnloggedFailure(1, "error: cannot combine --all and PROJECT");
}
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 640adbf..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
@@ -33,13 +33,13 @@
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.gerrit.util.cli.CmdLineParser;
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.eclipse.jgit.errors.RepositoryNotFoundException;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
@@ -52,7 +52,7 @@
import java.util.List;
import java.util.Set;
-public class ReviewCommand extends BaseCommand {
+public class ReviewCommand extends SshCommand {
private static final Logger log =
LoggerFactory.getLogger(ReviewCommand.class);
@@ -130,67 +130,60 @@
private List<ApproveOption> optionList;
@Override
- public final void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Failure {
- initOptionList();
- parseCommandLine();
- if (abandonChange) {
- if (restoreChange) {
- throw error("abandon and restore actions are mutually exclusive");
- }
- if (submitChange) {
- throw error("abandon and submit actions are mutually exclusive");
- }
- if (publishPatchSet) {
- throw error("abandon and publish actions are mutually exclusive");
- }
- if (deleteDraftPatchSet) {
- throw error("abandon and delete actions are mutually exclusive");
- }
- }
- if (publishPatchSet) {
- if (restoreChange) {
- throw error("publish and restore actions are mutually exclusive");
- }
- if (submitChange) {
- throw error("publish and submit actions are mutually exclusive");
- }
- if (deleteDraftPatchSet) {
- throw error("publish and delete actions are mutually exclusive");
- }
- }
-
- boolean ok = true;
- for (final PatchSet.Id patchSetId : patchSetIds) {
- try {
- approveOne(patchSetId);
- } catch (UnloggedFailure e) {
- ok = false;
- writeError("error: " + e.getMessage() + "\n");
- } catch (NoSuchChangeException e) {
- ok = false;
- writeError("no such change " + patchSetId.getParentKey().get());
- } catch (Exception e) {
- ok = false;
- writeError("fatal: internal server error while approving "
- + patchSetId + "\n");
- log.error("internal error while approving " + patchSetId, e);
- }
- }
-
- if (!ok) {
- throw new UnloggedFailure(1, "one or more approvals failed;"
- + " review output above");
- }
-
+ protected void run() throws UnloggedFailure {
+ if (abandonChange) {
+ if (restoreChange) {
+ throw error("abandon and restore actions are mutually exclusive");
}
- });
+ if (submitChange) {
+ throw error("abandon and submit actions are mutually exclusive");
+ }
+ if (publishPatchSet) {
+ throw error("abandon and publish actions are mutually exclusive");
+ }
+ if (deleteDraftPatchSet) {
+ throw error("abandon and delete actions are mutually exclusive");
+ }
+ }
+ if (publishPatchSet) {
+ if (restoreChange) {
+ throw error("publish and restore actions are mutually exclusive");
+ }
+ if (submitChange) {
+ throw error("publish and submit actions are mutually exclusive");
+ }
+ if (deleteDraftPatchSet) {
+ throw error("publish and delete actions are mutually exclusive");
+ }
+ }
+
+ boolean ok = true;
+ for (final PatchSet.Id patchSetId : patchSetIds) {
+ try {
+ approveOne(patchSetId);
+ } catch (UnloggedFailure e) {
+ ok = false;
+ writeError("error: " + e.getMessage() + "\n");
+ } catch (NoSuchChangeException e) {
+ ok = false;
+ writeError("no such change " + patchSetId.getParentKey().get());
+ } catch (Exception e) {
+ ok = false;
+ writeError("fatal: internal server error while approving "
+ + patchSetId + "\n");
+ log.error("internal error while approving " + patchSetId, e);
+ }
+ }
+
+ if (!ok) {
+ throw new UnloggedFailure(1, "one or more approvals failed;"
+ + " review output above");
+ }
}
- 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 = "";
@@ -209,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) {
@@ -270,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";
}
@@ -344,7 +340,8 @@
return projectControl.getProject().getNameKey().equals(change.getProject());
}
- private void initOptionList() {
+ @Override
+ protected void parseCommandLine() throws UnloggedFailure {
optionList = new ArrayList<ApproveOption>();
for (ApprovalType type : approvalTypes.getApprovalTypes()) {
@@ -360,6 +357,8 @@
"--" + category.getName().toLowerCase().replace(' ', '-');
optionList.add(new ApproveOption(name, usage, type));
}
+
+ super.parseCommandLine();
}
private void writeError(final String msg) {
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-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 6e1a32b..f873824 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -25,12 +25,11 @@
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
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 org.slf4j.Logger;
@@ -43,7 +42,7 @@
import java.util.List;
import java.util.Set;
-public class SetReviewersCommand extends BaseCommand {
+public class SetReviewersCommand extends SshCommand {
private static final Logger log =
LoggerFactory.getLogger(SetReviewersCommand.class);
@@ -85,28 +84,21 @@
private Set<Change.Id> changes = new HashSet<Change.Id>();
@Override
- public final void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Failure {
- parseCommandLine();
-
- boolean ok = true;
- for (Change.Id changeId : changes) {
- try {
- ok &= modifyOne(changeId);
- } catch (Exception err) {
- ok = false;
- log.error("Error updating reviewers on change " + changeId, err);
- writeError("fatal", "internal error while updating " + changeId);
- }
- }
-
- if (!ok) {
- throw error("fatal: one or more updates failed; review output above");
- }
+ protected void run() throws UnloggedFailure {
+ boolean ok = true;
+ for (Change.Id changeId : changes) {
+ try {
+ ok &= modifyOne(changeId);
+ } catch (Exception err) {
+ ok = false;
+ log.error("Error updating reviewers on change " + changeId, err);
+ writeError("fatal", "internal error while updating " + changeId);
}
- });
+ }
+
+ if (!ok) {
+ throw error("fatal: one or more updates failed; review output above");
+ }
}
private boolean modifyOne(Change.Id changeId) throws Exception {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 4de10d6..f8d3c41f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -15,12 +15,12 @@
package com.google.gerrit.sshd.commands;
import com.google.gerrit.common.Version;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.lifecycle.LifecycleListener;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.git.WorkQueue.Task;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.RequiresCapability;
import com.google.gerrit.sshd.SshDaemon;
import com.google.inject.Inject;
@@ -30,13 +30,11 @@
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.session.IoSession;
-import org.apache.sshd.server.Environment;
import org.eclipse.jgit.storage.file.WindowCacheStatAccessor;
import org.kohsuke.args4j.Option;
import java.io.File;
import java.io.IOException;
-import java.io.PrintWriter;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.lang.management.RuntimeMXBean;
@@ -47,6 +45,7 @@
import java.util.Date;
/** Show the current cache states. */
+@RequiresCapability(GlobalCapability.VIEW_CACHES)
final class ShowCaches extends CacheCommand {
private static volatile long serverStarted;
@@ -68,9 +67,6 @@
private boolean showJVM;
@Inject
- private IdentifiedUser currentUser;
-
- @Inject
private WorkQueue workQueue;
@Inject
@@ -80,42 +76,21 @@
@SitePath
private File sitePath;
- private PrintWriter p;
-
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- if (!currentUser.getCapabilities().canViewCaches()) {
- String msg = String.format(
- "fatal: %s does not have \"View Caches\" capability.",
- currentUser.getUserName());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
-
- parseCommandLine();
- display();
- }
- });
- }
-
- private void display() {
- p = toPrintWriter(out);
-
+ protected void run() {
Date now = new Date();
- p.format(
+ stdout.format(
"%-25s %-20s now %16s\n",
"Gerrit Code Review",
Version.getVersion() != null ? Version.getVersion() : "",
new SimpleDateFormat("HH:mm:ss zzz").format(now));
- p.format(
+ stdout.format(
"%-25s %-20s uptime %16s\n",
"", "",
uptime(now.getTime() - serverStarted));
- p.print('\n');
+ stdout.print('\n');
- p.print(String.format(//
+ stdout.print(String.format(//
"%1s %-18s %-4s|%-20s| %-5s |%-14s|\n" //
, "" //
, "Name" //
@@ -124,7 +99,7 @@
, "AvgGet" //
, "Hit Ratio" //
));
- p.print(String.format(//
+ stdout.print(String.format(//
"%1s %-18s %-4s|%6s %6s %6s| %-5s |%-4s %-4s %-4s|\n" //
, "" //
, "" //
@@ -137,7 +112,7 @@
, "Mem" //
, "Agg" //
));
- p.print("------------------"
+ stdout.print("------------------"
+ "-------+--------------------+----------+--------------+\n");
for (final Ehcache cache : getAllCaches()) {
final CacheConfiguration cfg = cache.getCacheConfiguration();
@@ -146,7 +121,7 @@
final long total = stat.getCacheHits() + stat.getCacheMisses();
if (useDisk) {
- p.print(String.format(//
+ stdout.print(String.format(//
"D %-18s %-4s|%6s %6s %6s| %7s |%4s %4s %4s|\n" //
, cache.getName() //
, interval(cfg.getTimeToLiveSeconds()) //
@@ -159,7 +134,7 @@
, percent(stat.getCacheHits(), total) //
));
} else {
- p.print(String.format(//
+ stdout.print(String.format(//
" %-18s %-4s|%6s %6s %6s| %7s |%4s %4s %4s|\n" //
, cache.getName() //
, interval(cfg.getTimeToLiveSeconds()) //
@@ -171,7 +146,7 @@
));
}
}
- p.print('\n');
+ stdout.print('\n');
if (gc) {
System.gc();
@@ -187,7 +162,7 @@
jvmSummary();
}
- p.flush();
+ stdout.flush();
}
private void memSummary() {
@@ -200,17 +175,17 @@
final int jgitOpen = WindowCacheStatAccessor.getOpenFiles();
final long jgitBytes = WindowCacheStatAccessor.getOpenBytes();
- p.format("Mem: %s total = %s used + %s free + %s buffers\n",
+ stdout.format("Mem: %s total = %s used + %s free + %s buffers\n",
bytes(mTotal),
bytes(mInuse - jgitBytes),
bytes(mFree),
bytes(jgitBytes));
- p.format(" %s max\n", bytes(mMax));
- p.format(" %8d open files, %8d cpus available, %8d threads\n",
+ stdout.format(" %s max\n", bytes(mMax));
+ stdout.format(" %8d open files, %8d cpus available, %8d threads\n",
jgitOpen,
r.availableProcessors(),
ManagementFactory.getThreadMXBean().getThreadCount());
- p.print('\n');
+ stdout.print('\n');
}
private void taskSummary() {
@@ -224,7 +199,7 @@
case SLEEPING: tasksSleeping++; break;
}
}
- p.format(
+ stdout.format(
"Tasks: %4d total = %4d running + %4d ready + %4d sleeping\n",
tasksTotal,
tasksRunning,
@@ -245,7 +220,7 @@
oldest = Math.min(oldest, s.getCreationTime());
}
- p.format(
+ stdout.format(
"SSH: %4d users, oldest session started %s ago\n",
list.size(),
uptime(now - oldest));
@@ -254,22 +229,22 @@
private void jvmSummary() {
OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean();
- p.format("JVM: %s %s %s\n",
+ stdout.format("JVM: %s %s %s\n",
runtimeBean.getVmVendor(),
runtimeBean.getVmName(),
runtimeBean.getVmVersion());
- p.format(" on %s %s %s\n", "",
+ stdout.format(" on %s %s %s\n", "",
osBean.getName(),
osBean.getVersion(),
osBean.getArch());
try {
- p.format(" running as %s on %s\n",
+ stdout.format(" running as %s on %s\n",
System.getProperty("user.name"),
InetAddress.getLocalHost().getHostName());
} catch (UnknownHostException e) {
}
- p.format(" cwd %s\n", path(new File(".").getAbsoluteFile().getParentFile()));
- p.format(" site %s\n", path(sitePath));
+ stdout.format(" cwd %s\n", path(new File(".").getAbsoluteFile().getParentFile()));
+ stdout.format(" site %s\n", path(sitePath));
}
private String path(File file) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
index a72ce90..4085dcb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -14,21 +14,21 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.util.IdGenerator;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.RequiresCapability;
+import com.google.gerrit.sshd.SshCommand;
import com.google.gerrit.sshd.SshDaemon;
import com.google.gerrit.sshd.SshSession;
import com.google.inject.Inject;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.session.IoSession;
-import org.apache.sshd.server.Environment;
import org.apache.sshd.server.session.ServerSession;
import org.kohsuke.args4j.Option;
-import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
@@ -40,39 +40,16 @@
import java.util.List;
/** Show the current SSH connections. */
-final class ShowConnections extends BaseCommand {
+@RequiresCapability(GlobalCapability.VIEW_CONNECTIONS)
+final class ShowConnections extends SshCommand {
@Option(name = "--numeric", aliases = {"-n"}, usage = "don't resolve names")
private boolean numeric;
- private PrintWriter p;
-
- @Inject
- IdentifiedUser currentUser;
-
@Inject
private SshDaemon daemon;
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- if (!currentUser.getCapabilities().canViewConnections()) {
- String msg = String.format(
- "fatal: %s does not have \"View Connections\" capability.",
- currentUser.getUserName());
- throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
- }
-
- parseCommandLine();
- ShowConnections.this.display();
- }
- });
- }
-
- private void display() throws Failure {
- p = toPrintWriter(out);
-
+ protected void run() throws Failure {
final IoAcceptor acceptor = daemon.getIoAcceptor();
if (acceptor == null) {
throw new Failure(1, "fatal: sshd no longer running");
@@ -93,9 +70,9 @@
});
final long now = System.currentTimeMillis();
- p.print(String.format("%-8s %8s %8s %-15s %s\n", //
+ stdout.print(String.format("%-8s %8s %8s %-15s %s\n", //
"Session", "Start", "Idle", "User", "Remote Host"));
- p.print("--------------------------------------------------------------\n");
+ stdout.print("--------------------------------------------------------------\n");
for (final IoSession io : list) {
ServerSession s = (ServerSession) ServerSession.getSession(io, true);
SshSession sd = s != null ? s.getAttribute(SshSession.KEY) : null;
@@ -104,16 +81,14 @@
final long start = io.getCreationTime();
final long idle = now - io.getLastIoTime();
- p.print(String.format("%8s %8s %8s %-15.15s %.30s\n", //
+ stdout.print(String.format("%8s %8s %8s %-15.15s %.30s\n", //
id(sd), //
time(now, start), //
age(idle), //
username(sd), //
hostname(remoteAddress)));
}
- p.print("--\n");
-
- p.flush();
+ stdout.print("--\n");
}
private static String id(final SshSession sd) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
index e835ffe..f862484 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -23,13 +23,13 @@
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.util.IdGenerator;
import com.google.gerrit.sshd.AdminHighPriorityCommand;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Option;
-import java.io.PrintWriter;
+import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Comparator;
@@ -39,7 +39,7 @@
/** Display the current work queue. */
@AdminHighPriorityCommand
-final class ShowQueue extends BaseCommand {
+final class ShowQueue extends SshCommand {
@Option(name = "-w", usage = "display without line width truncation")
private boolean wide;
@@ -52,12 +52,11 @@
@Inject
private IdentifiedUser currentUser;
- private PrintWriter p;
private int columns = 80;
private int taskNameWidth;
@Override
- public void start(final Environment env) {
+ public void start(final Environment env) throws IOException {
String s = env.getEnv().get(Environment.ENV_COLUMNS);
if (s != null && !s.isEmpty()) {
try {
@@ -66,19 +65,11 @@
columns = 80;
}
}
-
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Exception {
- parseCommandLine();
- ShowQueue.this.display();
- }
- });
+ super.start(env);
}
- private void display() {
- p = toPrintWriter(out);
-
+ @Override
+ protected void run() {
final List<Task<?>> pending = workQueue.getTasks();
Collections.sort(pending, new Comparator<Task<?>>() {
public int compare(Task<?> a, Task<?> b) {
@@ -103,9 +94,9 @@
taskNameWidth = wide ? Integer.MAX_VALUE : columns - 8 - 12 - 8 - 4;
- p.print(String.format("%-8s %-12s %-8s %s\n", //
+ stdout.print(String.format("%-8s %-12s %-8s %s\n", //
"Task", "State", "", "Command"));
- p.print("----------------------------------------------"
+ stdout.print("----------------------------------------------"
+ "--------------------------------\n");
int numberOfPendingTasks = 0;
@@ -158,7 +149,7 @@
// Shows information about tasks depending on the user rights
if (viewAll || (!hasCustomizedPrint && regularUserCanSee)) {
- p.print(String.format("%8s %-12s %-8s %s\n", //
+ stdout.print(String.format("%8s %-12s %-8s %s\n", //
id(task.getTaskId()), start, "", format(task)));
} else if (regularUserCanSee) {
if (remoteName == null) {
@@ -167,20 +158,18 @@
remoteName = remoteName + "/" + projectName;
}
- p.print(String.format("%8s %-12s %-8s %s\n", //
+ stdout.print(String.format("%8s %-12s %-8s %s\n", //
id(task.getTaskId()), start, "", remoteName));
}
}
- p.print("----------------------------------------------"
+ stdout.print("----------------------------------------------"
+ "--------------------------------\n");
if (viewAll) {
numberOfPendingTasks = pending.size();
}
- p.print(" " + numberOfPendingTasks + " tasks\n");
-
- p.flush();
+ stdout.print(" " + numberOfPendingTasks + " tasks\n");
}
private static String id(final int id) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java
index 32ab2db..0e1a1fe 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java
@@ -27,11 +27,14 @@
command(gerrit, "approve").to(ErrorSlaveMode.class);
command(gerrit, "create-account").to(ErrorSlaveMode.class);
+ command(gerrit, "create-group").to(ErrorSlaveMode.class);
command(gerrit, "create-project").to(ErrorSlaveMode.class);
command(gerrit, "gsql").to(ErrorSlaveMode.class);
command(gerrit, "receive-pack").to(ErrorSlaveMode.class);
+ command(gerrit, "rename-group").to(ErrorSlaveMode.class);
command(gerrit, "replicate").to(ErrorSlaveMode.class);
command(gerrit, "review").to(ErrorSlaveMode.class);
command(gerrit, "set-project-parent").to(ErrorSlaveMode.class);
+ command(gerrit, "set-reviewers").to(ErrorSlaveMode.class);
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
index 001863b..addbb84 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
@@ -15,29 +15,16 @@
package com.google.gerrit.sshd.commands;
import com.google.gerrit.common.Version;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
-import org.apache.sshd.server.Environment;
-
-import java.io.PrintWriter;
-
-final class VersionCommand extends BaseCommand {
+final class VersionCommand extends SshCommand {
@Override
- public void start(final Environment env) {
- startThread(new CommandRunnable() {
- @Override
- public void run() throws Failure {
- parseCommandLine();
+ protected void run() throws Failure {
+ String v = Version.getVersion();
+ if (v == null) {
+ throw new Failure(1, "fatal: version unavailable");
+ }
- String v = Version.getVersion();
- if (v == null) {
- throw new Failure(1, "fatal: version unavailable");
- }
-
- final PrintWriter stdout = toPrintWriter(out);
- stdout.println("gerrit version " + v);
- stdout.flush();
- }
- });
+ stdout.println("gerrit version " + v);
}
}
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..78dcebb 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>
@@ -811,6 +822,12 @@
<artifactId>PrologCafe</artifactId>
<version>1.3</version>
</dependency>
+
+ <dependency>
+ <groupId>org.pegdown</groupId>
+ <artifactId>pegdown</artifactId>
+ <version>1.1.0</version>
+ </dependency>
</dependencies>
</dependencyManagement>
@@ -839,5 +856,10 @@
<id>clojars-repo</id>
<url>http://clojars.org/repo</url>
</repository>
+
+ <repository>
+ <id>scala-tools</id>
+ <url>http://scala-tools.org/repo-releases</url>
+ </repository>
</repositories>
</project>
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