Merge branch 'stable-2.4'
By Colby Ranger (4) and others
via gerrit code review (3) and Shawn O. Pearce (1)
* stable-2.4:
Do not execute rejected commands
Honor the sendmail.smtpUser from gerrit.config on upgrade
Fix deadlock on destroy of CommandFactoryProvider.
Use AtomicBoolean for "logged" in CommandFactoryProvider.
Fix lockup of '/COMMIT_MSG' file from patch set
Catch all exceptions when async emailing.
Fixed cleanup of propagated SshScopes
Conflicts:
gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
Change-Id: Ia67f33ee5bd8a33917221df5cf9c8287913bd99c
diff --git a/.gitignore b/.gitignore
index f318b65..465893d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@
/.settings/org.eclipse.jdt.core.prefs
/.settings/org.maven.ide.eclipse.prefs
/test_site
+/.idea
+/gerrit-parent.iml
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 8d89fa2..b9fb8b3 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -622,13 +622,19 @@
Push Merge Commits
~~~~~~~~~~~~~~~~~~~~
-The `Push Merge Commit` permits the user to upload merge commits.
-It's an addon to the <<category_push,Push>> access right, and so it
-won't be sufficient with only `Push Merge Commit` granted for a push
-to happen. Some projects wish to restrict merges to being created by
-Gerrit. By granting `Push` without `Push Merge Commit`, the only
+The `Push Merge Commit` access right permits the user to upload merge
+commits. It's an addon to the <<category_push,Push>> access right, and
+so it won't be sufficient with only `Push Merge Commit` granted for a
+push to happen. Some projects wish to restrict merges to being created
+by Gerrit. By granting `Push` without `Push Merge Commit`, the only
merges that enter the system will be those created by Gerrit.
+The reference name connected to a `Push Merge Commit` entry must always
+be prefixed with `refs/for/`, for example `refs/for/refs/heads/BRANCH`.
+This applies even for an entry that complements a `Push` entry for
+`refs/heads/BRANCH` that allows direct pushes of non-merge commits, and
+the intention of the `Push Merge Commit` entry is to allow direct pushes
+of merge commits.
[[category_push_annotated]]
Push Annotated Tag
@@ -1136,7 +1142,8 @@
Start Replication
~~~~~~~~~~~~~~~~~
-Allow access to execute link:cmd-replicate.html[the `gerrit replicate` command].
+Allow access to execute `replication start` command, if the
+replication plugin is installed on the server.
[[capability_viewCaches]]
diff --git a/Documentation/cmd-ban-commit.txt b/Documentation/cmd-ban-commit.txt
new file mode 100644
index 0000000..fb4a2ac
--- /dev/null
+++ b/Documentation/cmd-ban-commit.txt
@@ -0,0 +1,60 @@
+gerrit ban-commit
+=================
+
+NAME
+----
+gerrit ban-commit - Bans a commit from a project's repository.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit ban-commit'
+ [--reason <REASON>]
+ <PROJECT>
+ <COMMIT> ...
+
+DESCRIPTION
+-----------
+Marks a commit as banned for the specified repository. If a commit is
+banned Gerrit rejects every push that includes this commit with
+link:error-contains-banned-commit.html[contains banned commit ...].
+
+[NOTE]
+This command just marks the commit as banned, but it does not remove
+the commit from the history of any central branch. This needs to be
+done manually.
+
+ACCESS
+------
+Caller must be owner of the project or be a member of the privileged
+'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<PROJECT>::
+ Required; name of the project for which the commit should be
+ banned.
+
+<COMMIT>::
+ Required; commit(s) that should be banned.
+
+--reason::
+ Reason for banning the commit.
+
+EXAMPLES
+--------
+Ban commit `421919d015c062fd28901fe144a78a555d0b5984` from project
+`myproject`:
+
+====
+ $ ssh -p 29418 review.example.com gerrit ban-commit myproject \
+ 421919d015c062fd28901fe144a78a555d0b5984
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index f22141c..02aa078 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -19,7 +19,7 @@
[--use-signed-off-by | --so]
[--use-content-merge]
[--require-change-id | --id]
- [--branch <REF> | -b <REF>]
+ [[--branch <REF> | -b <REF>] ...]
[--empty-commit]
{ <NAME> | --name <NAME> }
@@ -59,8 +59,11 @@
--branch::
-b::
- Name of the initial branch in the newly created project.
- Defaults to 'master'.
+ Name of the initial branch(es) in the newly created project.
+ Several branches can be specified on the command line.
+ If several branches are specified then the first one becomes HEAD
+ of the project. If none branches are specified then default value
+ ('master') is used.
--owner::
-o::
@@ -163,7 +166,8 @@
REPLICATION
-----------
-The remote repository creation is performed by a Bourne shell script:
+If the replication plugin is installed, the plugin will attempt to
+perform remote repository creation by a Bourne shell script:
====
mkdir -p '/base/project.git' && cd '/base/project.git' && git init --bare && git update-ref HEAD refs/heads/master
@@ -174,10 +178,13 @@
environment variable. Administrators could also run this command line
by hand to establish a new empty repository.
+A custom extension or plugin may also be developed to implement the
+NewProjectCreatedListener extension point and handle custom logic
+for remote repository creation.
+
SEE ALSO
--------
-* link:config-replication.html[Git Replication/Mirroring]
* link:project-setup.html[Project Setup]
GERRIT
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index b09c3b3..936729e 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -54,6 +54,9 @@
'gerrit approve'::
'Deprecated alias for `gerrit review`.'
+link:cmd-ban-commit.html[gerrit ban-commit]::
+ Bans a commit from a project's repository.
+
link:cmd-ls-groups.html[gerrit ls-groups]::
List groups visible to the caller.
@@ -93,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.
@@ -105,9 +111,6 @@
link:cmd-gsql.html[gerrit gsql]::
Administrative interface to active database.
-link:cmd-replicate.html[gerrit replicate]::
- Manually trigger replication, to recover a node.
-
link:cmd-set-project-parent.html[gerrit set-project-parent]::
Change the project permissions are inherited from.
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index 7782aa8..c1b37fa 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -23,7 +23,7 @@
ACCESS
------
-Any user who has configured an SSH key.
+Any user who has configured an SSH key, or by an user over HTTP.
SCRIPTING
---------
@@ -64,6 +64,15 @@
`all`:: Any type of project.
--
+--format::
+ What output format to display the results in.
++
+--
+`text`:: Simple text based format.
+`json`:: Map of JSON objects describing each project.
+`json_compact`:: Minimized JSON output.
+--
+
--all::
Display all projects that are accessible by the calling user
account. Besides the projects that the calling user account has
@@ -72,12 +81,43 @@
the 'READ' access right is not assigned to the calling user
account).
+--limit::
+ Cap the number of results to the first N matches.
+
+HTTP
+----
+This command is also available over HTTP, as `/projects/` for
+anonymous access and `/a/projects/` for authenticated access.
+Named options are available as query parameters. Results can
+be limited to projects matching a prefix by supplying the prefix
+as part of the URL, for example `/projects/external/` lists only
+projects whose name start with the string `external/`.
+
+Over HTTP the `json_compact` output format is assumed if the client
+explicitly asks for JSON using HTTP header `Accept: application/json`.
+When any JSON output format is used on HTTP, readers must skip the
+first line produced. The first line is a garbage JSON string crafted
+to prevent a browser from executing the response in a script tag.
+
+Output will be gzip compressed if `Accept-Encoding: gzip` was used
+by the client in the request headers.
+
EXAMPLES
--------
List visible projects:
=====
$ ssh -p 29418 review.example.com gerrit ls-projects
+ platform/manifest
+ tools/gerrit
+ tools/gwtorm
+
+ $ curl http://review.example.com/projects/
+ platform/manifest
+ tools/gerrit
+ tools/gwtorm
+
+ $ curl http://review.example.com/projects/tools/
tools/gerrit
tools/gwtorm
=====
diff --git a/Documentation/cmd-replicate.txt b/Documentation/cmd-replicate.txt
deleted file mode 100644
index 7722027..0000000
--- a/Documentation/cmd-replicate.txt
+++ /dev/null
@@ -1,103 +0,0 @@
-gerrit replicate
-================
-
-NAME
-----
-gerrit replicate - Manually trigger replication, to recover a node
-
-SYNOPSIS
---------
-[verse]
-'ssh' -p <port> <host> 'gerrit replicate'
- [--url <PATTERN>]
- {--all | <PROJECT> ...}
-
-DESCRIPTION
------------
-Schedules replication of the specified projects to all configured
-replication destinations, or only those whose URLs match the pattern
-given on the command line.
-
-Normally Gerrit automatically schedules replication whenever it
-makes a change to a managed Git repository. However, there are
-other reasons why an administrator may wish to trigger replication:
-
-* Destination disappears, then later comes back online.
-+
-If a destination went offline for a period of time, when it comes
-back, it may be missing commits that it should have. Triggering a
-replication run for all projects against that URL will update it.
-
-* After repacking locally, and using `rsync` to distribute the new
- pack files to the destinations.
-+
-If the local server is repacked, and then the resulting pack files
-are sent to remote peers using `rsync -a --delete-after`, there
-is a chance that the rsync missed a change that was added during
-the rsync data transfer, and the rsync will remove that changes's
-data from the remote, even though the automatic replication pushed
-it there in parallel to the rsync.
-+
-Its a good idea to run replicate with `--all` to ensure all
-projects are consistent after the rsync is complete.
-
-* After deleting a ref by hand.
-+
-If a ref must be removed (e.g. to purge a change or patch set
-that shouldn't have been created, and that must be eradicated)
-that delete must be done by direct git access on the local,
-managed repository. Gerrit won't know about the delete, and is
-unable to replicate it automatically. Triggering replication on
-just the affected project can update the mirrors.
-
-ACCESS
-------
-Caller must be a member of the privileged 'Administrators' group,
-or have been granted
-link:access-control.html#capability_startReplication[the 'Start Replication' global capability].
-
-SCRIPTING
----------
-This command is intended to be used in scripts.
-
-OPTIONS
--------
---all::
- Schedule replicating for all projects.
-
---url <PATTERN>::
- Replicate only to replication destinations whose URL
- contains the substring <PATTERN>. This can be useful to
- replicate only to a previously down node, which has been
- brought back online.
-
-EXAMPLES
---------
-Replicate every project, to every configured remote:
-
-====
- $ ssh -p 29418 review.example.com gerrit replicate --all
-====
-
-Replicate only to `srv2` now that it is back online:
-
-====
- $ ssh -p 29418 review.example.com gerrit replicate --url srv2 --all
-====
-
-Replicate only the `tools/gerrit` project, after deleting a ref
-locally by hand:
-
-====
- $ git --git-dir=/home/git/tools/gerrit.git update-ref -d refs/changes/00/100/1
- $ ssh -p 29418 review.example.com gerrit replicate tools/gerrit
-====
-
-SEE ALSO
---------
-
-* link:config-replication.html[Git Replication/Mirroring]
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
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 54f7752..6e50ef4 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -354,8 +354,8 @@
[[cache.name.maxAge]]cache.<name>.maxAge::
+
-Maximum age to keep an entry in the cache. If an entry has not
-been accessed in this period of time, it is removed from the cache.
+Maximum age to keep an entry in the cache. Entries are removed from
+the cache and refreshed from source data every maxAge interval.
Values should use common unit suffixes to express their setting:
+
* s, sec, second, seconds
@@ -371,7 +371,7 @@
supplied, the maximum age is infinite and items are never purged
except when the cache is full.
+
-Default is `90 days` for most caches, except:
+Default is `0`, meaning store forever with no expire, except:
+
* `"adv_bases"`: default is `10 minutes`
* `"ldap_groups"`: default is `1 hour`
@@ -379,33 +379,42 @@
[[cache.name.memoryLimit]]cache.<name>.memoryLimit::
+
-Maximum number of cache items to retain in memory. Keep in mind
-this is total number of items, not bytes of heap used.
+The total cost of entries to retain in memory. The cost computation
+varies by the cache. For most caches where the in-memory size of each
+entry is relatively the same, memoryLimit is currently defined to be
+the number of entries held by the cache (each entry costs 1).
++
+For caches where the size of an entry can vary significantly between
+individual entries (notably `"diff"`, `"diff_intraline"`), memoryLimit
+is an approximation of the total number of bytes stored by the cache.
+Larger entries that represent bigger patch sets or longer source files
+will consume a bigger portion of the memoryLimit. For these caches the
+memoryLimit should be set to roughly the amount of RAM (in bytes) the
+administrator can dedicate to the cache.
+
Default is 1024 for most caches, except:
+
* `"adv_bases"`: default is `4096`
-* `"diff"`: default is `128`
-* `"diff_intraline"`: default is `128`
+* `"diff"`: default is `10m` (10 MiB of memory)
+* `"diff_intraline"`: default is `10m` (10 MiB of memory)
+* `"plugin_resources"`: default is 2m (2 MiB of memory)
+
++
+If set to 0 the cache is disabled. Entries are removed immediately
+after being stored by the cache. This is primarily useful for testing.
[[cache.name.diskLimit]]cache.<name>.diskLimit::
+
-Maximum number of cache items to retain on disk, if this cache
-supports storing its items to disk. Like memoryLimit, this is
-total number of items, not bytes of disk used. If 0, disk storage
-for this cache is disabled.
+Total size in bytes of the keys and values stored on disk. Caches that
+have grown bigger than this size are scanned daily at 1 AM local
+server time to trim the cache. Entries are removed in least recently
+accessed order until the cache fits within this limit. Caches may
+grow larger than this during the day, as the size check is only
+performed once every 24 hours.
+
-Default is 16384.
-
-[[cache.name.diskBuffer]]cache.<name>.diskBuffer::
+Default is 128 MiB per cache.
+
-Number of bytes to buffer in memory before writing less frequently
-accessed cache items to disk, if this cache supports storing its
-items to disk.
-+
-Default is 5 MiB.
-+
-Common unit suffixes of 'k', 'm', or 'g' are supported.
+If 0, disk storage for the cache is disabled.
[[cache_names]]Standard Caches
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -447,14 +456,10 @@
directory and file levels. Gerrit uses this cache to accelerate
the display of affected file names, as well as file contents.
+
-Entries in this cache are relatively large, so the memory limit
-should not be set incredibly high. Administrators should try to
-target cache.diff.memoryLimit to be roughly the number of changes
-which their users will process in a 1 or 2 day span.
-+
-Keeping entries for 90 days gives sufficient time for most changes
-to be submitted or abandoned before their relevant difference items
-expire out.
+Entries in this cache are relatively large, so memoryLimit is an
+estimate in bytes of memory used. Administrators should try to target
+cache.diff.memoryLimit to fit all changes users will view in a 1 or 2
+day span.
cache `"diff_intraline"`::
+
@@ -462,14 +467,10 @@
between two commits. Gerrit uses this cache to accelerate display of
intraline differences when viewing a file.
+
-Entries in this cache are relatively large, so the memory limit
-should not be set incredibly high. Administrators should try to
-target cache.diff.memoryLimit to be roughly the number of changes
-which their users will process in a 1 or 2 day span.
-+
-Keeping entries for 90 days gives sufficient time for most changes
-to be submitted or abandoned before their relevant difference items
-expire out.
+Entries in this cache are relatively large, so memoryLimit is an
+estimate in bytes of memory used. Administrators should try to target
+cache.diff.memoryLimit to fit all files users will view in a 1 or 2
+day span.
cache `"git_tags"`::
+
@@ -517,6 +518,12 @@
expressions are used, so this cache remembers the ordering for
each branch.
+cache `"plugin_resources"`::
++
+Caches formatted plugin resources, such as plugin documentation that
+has been converted from Markdown to HTML. The memoryLimit refers to
+the bytes of memory dedicated to storing the documentation.
+
cache `"projects"`::
+
Caches the project description records, from the `projects` table
@@ -550,8 +557,8 @@
unable to persist the session information. Enabling a disk cache
is strongly recommended.
+
-Session storage is relatively inexpensive, the average entry in
-this cache is approximately 248 bytes, depending on the JVM.
+Session storage is relatively inexpensive. The average entry in
+this cache is approximately 346 bytes.
See also link:cmd-flush-caches.html[gerrit flush-caches].
@@ -1069,11 +1076,6 @@
by the system administrator, and might not even be running on the
same host as Gerrit.
-[[gerrit.replicateOnStartup]]gerrit.replicateOnStartup::
-+
-If true, replicates to all remotes on startup to ensure they are
-in-sync with this server. By default, true.
-
[[gitweb]]Section gitweb
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1680,6 +1682,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
@@ -1898,6 +1916,23 @@
updated versions. If false, a server restart is required to change
any of these resources. Default is true, allowing automatic reloads.
+[[site.enableDeprecatedQuery]]site.enableDeprecatedQuery::
++
+If true the deprecated `/query` URL is available to return JSON
+and text results for changes. If false, the URL is disabled and
+returns 404 to clients. Default is true, enabling `/query`.
+
+[[ssh-alias]] Section ssh-alias
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Variables in section ssh-alias permit the site administrator to alias
+another command from Gerrit or a plugin into the `gerrit` command
+namespace. To alias `replication start` to `gerrit replicate`:
+
+----
+[ssh-alias]
+ replicate = replication start
+----
[[sshd]] Section sshd
~~~~~~~~~~~~~~~~~~~~~
@@ -2285,15 +2320,6 @@
password = s3kr3t
----
-File `etc/replication.config`
------------------------------
-
-The optional file `'$site_path'/etc/replication.config` controls how
-Gerrit automatically replicates changes it makes to any of the Git
-repositories under its control.
-
-* link:config-replication.html[Git Replication/Mirroring]
-
File `etc/peer_keys`
--------------------
@@ -2329,7 +2355,6 @@
Other files support site customization.
+
* link:config-headerfooter.html[Site Header/Footer]
-* link:config-replication.html[Git Replication/Mirroring]
GERRIT
------
diff --git a/Documentation/config-replication.txt b/Documentation/config-replication.txt
deleted file mode 100644
index 772282e..0000000
--- a/Documentation/config-replication.txt
+++ /dev/null
@@ -1,273 +0,0 @@
-Gerrit Code Review - Git Replication
-====================================
-
-Gerrit can automatically push any changes it makes to its managed Git
-repositories to another system. Usually this would be configured to
-provide mirroring of changes, for warm-standby backups, or a
-load-balanced public mirror farm.
-
-The replication runs on a short delay. This gives Gerrit a small
-time window to batch updates going to the same project, such as
-when a user uploads multiple changes at once.
-
-Typically replication should be done over SSH, with a passwordless
-public/private key pair. On a trusted network it is also possible to
-use replication over the insecure (but much faster) git:// protocol,
-by enabling the `receive-pack` service on the receiving system, but
-this configuration is not recommended. It is also possible to
-specify a local path as replication target. This makes e.g. sense if
-a network share is mounted to which the repositories should be
-replicated.
-
-Enabling Replication
---------------------
-
-If replicating over SSH (recommended), ensure the host key of the
-remote system(s) is already in the Gerrit user's `~/.ssh/known_hosts`
-file. The easiest way to add the host key is to connect once by hand
-with the command line:
-
-====
- sudo su -c 'ssh mirror1.us.some.org echo' gerrit2
-====
-
-Next, create `'$site_path'/etc/replication.config` as a Git-style
-config file, and restart Gerrit.
-
-Example `replication.config` to replicate in parallel to four
-different hosts:
-
-====
- [remote "host-one"]
- url = gerrit2@host-one.example.com:/some/path/${name}.git
-
- [remote "pubmirror"]
- url = mirror1.us.some.org:/pub/git/${name}.git
- url = mirror2.us.some.org:/pub/git/${name}.git
- url = mirror3.us.some.org:/pub/git/${name}.git
- push = +refs/heads/*:refs/heads/*
- push = +refs/tags/*:refs/tags/*
- threads = 3
- authGroup = Public Mirror Group
- authGroup = Second Public Mirror Group
-====
-
-To manually trigger replication at runtime, see
-link:cmd-replicate.html[gerrit replicate].
-
-[[replication_config]]File `replication.config`
------------------------------------------------
-
-The optional file `'$site_path'/etc/replication.config` is a
-Git-style config file that controls the replication settings for
-Gerrit.
-
-The file is composed of one or more `remote` sections, each remote
-section provides common configuration settings for one or more
-destination URLs.
-
-Each remote section uses its own thread pool. If pushing to
-multiple remotes, over differing types of network connections
-(e.g. LAN and also public Internet), its a good idea to put them
-into different remote sections, so that replication to the slower
-connection does not starve out the faster local one. The example
-file above does this.
-
-[[remote]]Section remote
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-In the keys below, the <name> portion is unused by Gerrit, but must be
-unique to distinguish the different sections if more than one remote
-section appears in the file.
-
-[[remote.name.url]]remote.<name>.url::
-+
-Address of the remote server to push to. Multiple URLs may
-be specified within a single remote block, listing different
-destinations which share the same settings. Assuming sufficient
-threads in the thread pool, Gerrit pushes to all URLs in parallel,
-using one thread per URL.
-+
-Within each URL value the magic placeholder `${name}` is replaced
-with the Gerrit project name. This is a Gerrit specific extension
-to the otherwise standard Git URL syntax and it must be included
-in each URL so that Gerrit can figure out where each project needs
-to be replicated.
-+
-See link:http://www.kernel.org/pub/software/scm/git/docs/git-push.html#URLS[GIT URLS]
-for details on Git URL syntax.
-
-[[remote.name.url]]remote.<name>.adminUrl::
-+
-Address of the alternative remote server only for repository creation. Multiple URLs may
-be specified within a single remote block, listing different
-destinations which share the same settings.
-+
-The adminUrl can be used as a ssh alternative to the url option, but only related to repository creation.
-If not specified, the repository creation tries to follow the default way through the url value specified.
-+
-It is useful when remote.<name>.url protocols does not allow repository creation
-although their usage are mandatory in the local environment.
-In that case, an alternative ssh url could be specified to repository creation.
-
-[[remote.name.receivepack]]remote.<name>.receivepack::
-+
-Path of the `git-receive-pack` executable on the remote system, if
-using the SSH transport.
-+
-Defaults to `git-receive-pack`.
-
-[[remote.name.uploadpack]]remote.<name>.uploadpack::
-+
-Path of the `git-upload-pack` executable on the remote system, if
-using the SSH transport.
-+
-Defaults to `git-upload-pack`.
-
-[[remote.name.push]]remote.<name>.push::
-+
-Standard Git refspec denoting what should be replicated. Setting this
-to `+refs/heads/*:refs/heads/*` would mirror only the active
-branches, but not the change refs under `refs/changes/`, or the tags
-under `refs/tags/`.
-+
-Multiple push keys can be supplied, to specify multiple patterns
-to match against. In the example file above, remote "pubmirror"
-uses two push keys to match both `refs/heads/*` and `refs/tags/*`,
-but excludes all others, including `refs/changes/*`.
-+
-Defaults to `+refs/*:refs/*` (all refs) if not specified.
-
-[[remote.name.timeout]]remote.<name>.timeout::
-+
-Number of seconds to wait for a network read or write to complete
-before giving up and declaring the remote side is not responding.
-If 0, there is no timeout, and the push client waits indefinitely.
-+
-A timeout should be large enough to mostly transfer the objects to
-the other side. 1 second may be too small for larger projects,
-especially over a WAN link, while 10-30 seconds is a much more
-reasonable timeout value.
-+
-Defaults to 0 seconds, wait indefinitely.
-
-[[remote.name.replicationDelay]]remote.<name>.replicationDelay::
-+
-Number of seconds to wait before scheduling a remote push operation.
-Setting the delay to 0 effectively disables the delay, causing the
-push to start as soon as possible.
-+
-This is a Gerrit specific extension to the Git remote block.
-+
-By default, 15 seconds.
-
-[[remote.name.replicationRetry]]remote.<name>.replicationRetry::
-+
-Number of minutes to wait before scheduling a remote push operation
-previously failed due to an offline remote server.
-+
-If a remote push operation fails because a remote server was
-offline, all push operations to the same destination URL are
-blocked, and the remote push is continuously retried.
-+
-This is a Gerrit specific extension to the Git remote block.
-+
-By default, 1 minute.
-
-[[remote.name.threads]]remote.<name>.threads::
-+
-Number of worker threads to dedicate to pushing to the repositories
-described by this remote. Each thread can push one project at a
-time, to one destination URL. Scheduling within the thread pool
-is done on a per-project basis. If a remote block describes 4
-URLs, allocating 4 threads in the pool will permit some level of
-parallel pushing.
-+
-By default, 1 thread.
-
-[[remote.name.authGroup]]remote.<name>.authGroup::
-+
-Specifies the name of a group that the remote should use to access
-the repositories. Multiple authGroups may be specified within a
-single remote block to signify a wider access right. In the project
-administration web interface the read access can be specified for
-this group to control if a project should be replicated or not to the
-remote.
-+
-By default, replicates without group control, i.e replicates
-everything to all remotes.
-
-[[remote.name.replicatePermissions]]remote.<name>.replicatePermissions::
-+
-If true, permissions-only projects and the refs/meta/config branch
-will also be replicated to the remote site. These projects and
-branches may be needed to keep a backup or slave server current.
-+
-By default, true, replicating everything.
-
-[[remote.name.mirror]]remote.<name>.mirror::
-+
-If true, replication will remove remote branches that absent locally
-or invisible to the replication (i.e. read access denied via 'authGroup'
-option).
-+
-By default, false, do not remove remote branches.
-
-
-[[secure_config]]File `secure.config`
------------------------------------------------
-
-The optional file `'$site_path'/secure.config` is a Git-style config
-file that provides secure values that should not be world-readable,
-such as passwords. Passwords for HTTP remotes can be obtained from
-this file.
-
-[[remote.name.username]]remote.<name>.username::
-+
-Username to use for HTTP authentication on this remote, if not given
-in the URL.
-
-[[remote.name.password]]remote.<name>.password::
-+
-Password to use for HTTP authentication on this remote.
-
-
-[[ssh_config]]File `~/.ssh/config`
-----------------------------------
-
-If present, Gerrit reads and caches `~/.ssh/config` at startup, and
-supports most SSH configuration options. For example:
-
-====
- Host host-one.example.com:
- IdentityFile ~/.ssh/id_hostone
- PreferredAuthentications publickey
-
- Host mirror*.us.some.org:
- User mirror-updater
- IdentityFile ~/.ssh/id_pubmirror
- PreferredAuthentications publickey
-====
-
-Supported options:
-
- * Host
- * Hostname
- * User
- * Port
- * IdentityFile
- * PreferredAuthentications
- * StrictHostKeyChecking
-
-SSH authentication must be by passwordless public key, as there is
-no facility to read passphases on startup or passwords during the
-SSH connection setup, and SSH agents are not supported from Java.
-
-Host keys for any destination SSH servers must appear in the user's
-`~/.ssh/known_hosts` file, and must be added in advance, before
-Gerrit starts. If a host key is not listed, Gerrit will be unable to
-connect to that destination, and replication to that URL will fail.
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
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 bf5ba73..103ede8 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.
@@ -191,7 +191,7 @@
* link:http://code.google.com/p/gerrit/[Project Homepage]
* link:http://code.google.com/p/gerrit/downloads/list[Release Versions]
-* link:http://code.google.com/p/gerrit/wiki/Source?tm=4[Source]
+* link:http://code.google.com/p/gerrit/source/checkout[Source]
* link:http://code.google.com/p/gerrit/issues/list[Issue Tracking]
* link:https://review.source.android.com/[Change Review]
@@ -669,17 +669,18 @@
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
-configured in `'$site_path'/etc/replication.conf` to send copies
-of all changes over SSH to other servers, or to the Amazon S3 blob
-storage service.
+Using the standard replication plugin, Gerrit can be configured
+to replicate changes made to the local Git repositories over any
+standard Git transports. After the plugin is installed, remote
+destinations can be configured in `'$site_path'/etc/replication.conf`
+to send copies of all changes over SSH to other servers, or to the
+Amazon S3 blob storage service.
Logging Plan
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..586ae07
--- /dev/null
+++ b/Documentation/dev-plugins.txt
@@ -0,0 +1,342 @@
+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 server internals. Plugins
+are tightly coupled to a specific major.minor server version and
+may require source code changes to compile against a different
+server version.
+
+An extension in Gerrit runs inside of the same JVM as Gerrit
+in the same way as a plugin, but has limited visibility to the
+server's internals. The limited visiblity reduces the extension's
+dependencies, enabling it to be compatiable across a wider range
+of server versions.
+
+Most of this documentation refers to either type as a plugin.
+
+Requirements
+------------
+
+To start development, clone the sample maven project:
+
+----
+$ git clone https://gerrit.googlesource.com/plugins/helloworld
+----
+
+This project includes the dependencies file that matches the war file to
+develop against. Dependencies are offered in two different formats:
+
+gerrit-extension-api.jar::
+ A stable but thin interface. Suitable for extensions that need
+ to be notified of events, but do not require tight coupling to
+ the internals of Gerrit. Extensions built against this API can
+ expect to be binary compatible across a wide range of server
+ versions.
+
+gerrit-plugin-api.jar::
+ The complete internals of the Gerrit server, permitting a
+ plugin to tightly couple itself and provide additional
+ functionality that is not possible as an extension. Plugins
+ built against this API are expected to break at the source
+ code level between every major.minor Gerrit release. A plugin
+ that compiles against 2.5 will probably need source code level
+ changes to work with 2.6, 2.7, and so on.
+
+Manifest
+--------
+
+Plugins may provide optional description information with standard
+manifest fields:
+
+====
+ Implementation-Title: Example plugin showing examples
+ Implementation-Version: 1.0
+ Implementation-Vendor: Example, Inc.
+ Implementation-URL: http://example.com/opensource/plugin-foo/
+====
+
+ApiType
+~~~~~~~
+
+Plugins using the tightly coupled `gerrit-plugin-api.jar` must
+declare this API dependency in the manifest to gain access to server
+internals. If no Gerrit-ApiType is specified the stable `extension`
+API will be assumed. This may cause ClassNotFoundExceptions when
+loading a plugin that needs the plugin API.
+
+====
+ Gerrit-ApiType: plugin
+====
+
+Explicit Registration
+~~~~~~~~~~~~~~~~~~~~~
+
+Plugins that use explicit Guice registration must name the Guice
+modules in the manifest. Up to three modules can be named in the
+manifest. Gerrit-Module supplies bindings to the core server;
+Gerrit-SshModule supplies SSH commands to the SSH server (if
+enabled); Gerrit-HttpModule supplies servlets and filters to the HTTP
+server (if enabled). If no modules are named automatic registration
+will be performed by scanning all classes in the plugin JAR for
+`@Listen` and `@Export("")` annotations.
+
+====
+ Gerrit-Module: tld.example.project.CoreModuleClassName
+ Gerrit-SshModule: tld.example.project.SshModuleClassName
+ Gerrit-HttpModule: tld.example.project.HttpModuleClassName
+====
+
+Reload Method
+~~~~~~~~~~~~~
+
+If a plugin holds an exclusive resource that must be released before
+loading the plugin again (for example listening on a network port or
+acquiring a file lock) the manifest must declare Gerrit-ReloadMode
+to be `restart`. Otherwise the preferred method of `reload` will
+be used, as it enables the server to hot-patch an updated plugin
+with no down time.
+
+====
+ Gerrit-ReloadMode: restart
+====
+
+In either mode ('restart' or 'reload') any plugin or extension can
+be updated without restarting the Gerrit server. The difference is
+how Gerrit handles the upgrade:
+
+restart::
+ The old plugin is completely stopped. All registrations of SSH
+ commands and HTTP servlets are removed. All registrations of any
+ extension points are removed. All registered LifecycleListeners
+ have their `stop()` method invoked in reverse order. The new
+ plugin is started, and registrations are made from the new
+ plugin. There is a brief window where neither the old nor the
+ new plugin is connected to the server. This means SSH commands
+ and HTTP servlets will return not found errors, and the plugin
+ will not be notified of events that occurred during the restart.
+
+reload::
+ The new plugin is started. Its LifecycleListeners are permitted
+ to perform their `start()` methods. All SSH and HTTP registrations
+ are atomically swapped out from the old plugin to the new plugin,
+ ensuring the server never returns a not found error. All extension
+ point listeners are atomically swapped out from the old plugin to
+ the new plugin, ensuring no events are missed (however some events
+ may still route to the old plugin if the swap wasn't complete yet).
+ The old plugin is stopped.
+
+Classpath
+---------
+
+Each plugin is loaded into its own ClassLoader, isolating plugins
+from each other. A plugin or extension inherits the Java runtime
+and the Gerrit API chosen by `Gerrit-ApiType` (extension or plugin)
+from the hosting server.
+
+Plugins are loaded from a single JAR file. If a plugin needs
+additional libraries, it must include those dependencies within
+its own JAR. Plugins built using Maven may be able to use the
+link:http://maven.apache.org/plugins/maven-shade-plugin/[shade plugin]
+to package additional dependencies. Relocating (or renaming) classes
+should not be necessary due to the ClassLoader isolation.
+
+SSH Commands
+------------
+
+Plugins may provide commands that can be accessed through the SSH
+interface (extensions do not have this option).
+
+Command implementations must extend the base class SshCommand:
+
+====
+ import com.google.gerrit.sshd.SshCommand;
+
+ class PrintHello extends SshCommand {
+ protected abstract void run() {
+ stdout.print("Hello\n");
+ }
+ }
+====
+
+If no Guice modules are declared in the manifest, SSH commands may
+use auto-registration by providing an @Export annotatation:
+
+====
+ import com.google.gerrit.extensions.annotations.Export;
+ import com.google.gerrit.sshd.SshCommand;
+
+ @Export("print")
+ class PrintHello extends SshCommand {
+ protected abstract void run() {
+ stdout.print("Hello\n");
+ }
+ }
+====
+
+If explicit registration is being used, a Guice module must be
+supplied to register the SSH command and declared in the manifest
+with the `Gerrit-SshModule` attribute:
+
+====
+ import com.google.gerrit.sshd.PluginCommandModule;
+
+ class MyCommands extends PluginCommandModule {
+ protected void configureCommands() {
+ command("print").to(PrintHello.class);
+ }
+ }
+====
+
+For a plugin installed as name `helloworld`, the command implemented
+by PrintHello class will be available to users as:
+
+----
+$ ssh -P 29418 review.example.com helloworld print
+----
+
+HTTP Servlets
+-------------
+
+Plugins or extensions may register additional HTTP servlets, and
+wrap them with HTTP filters.
+
+Servlets may use auto-registration to declare the URL they handle:
+
+====
+ import com.google.gerrit.extensions.annotations.Export;
+ import com.google.inject.Singleton;
+ import javax.servlet.http.HttpServlet;
+ import javax.servlet.http.HttpServletRequest;
+ import javax.servlet.http.HttpServletResponse;
+
+ @Export("/print")
+ @Singleton
+ class HelloServlet extends HttpServlet {
+ protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
+ res.setContentType("text/plain");
+ res.setCharacterEncoding("UTF-8");
+ res.getWriter().write("Hello");
+ }
+ }
+====
+
+If explicit registration is being used, a Guice ServletModule must
+be supplied to register the HTTP servlets, and the module must be
+declared in the manifest with the `Gerrit-HttpModule` attribute:
+
+====
+ import com.google.inject.servlet.ServletModule;
+
+ class MyWebUrls extends ServletModule {
+ protected void configureServlets() {
+ serve("/print").with(HelloServlet.class);
+ }
+ }
+====
+
+For a plugin installed as name `helloworld`, the servlet implemented
+by HelloServlet class will be available to users as:
+
+----
+$ curl http://review.example.com/plugins/helloworld/print
+----
+
+Documentation
+-------------
+
+If a plugin does not register a filter or servlet to handle URLs
+`/Documentation/*` or `/static/*`, the core Gerrit server will
+automatically export these resources over HTTP from the plugin JAR.
+
+Static resources under `static/` directory in the JAR will be
+available as `/plugins/helloworld/static/resource`.
+
+Documentation files under `Documentation/` directory in the JAR
+will be available as `/plugins/helloworld/Documentation/resource`.
+
+Documentation may be written in
+link:http://daringfireball.net/projects/markdown/[Markdown] style
+if the file name ends with `.md`. Gerrit will automatically convert
+Markdown to HTML if accessed with extension `.html`.
+
+Automatic Index
+~~~~~~~~~~~~~~~
+
+If a plugin does not handle its `/` URL itself, Gerrit will
+redirect clients to the plugin's `/Documentation/index.html`.
+Requests for `/Documentation/` (bare directory) will also redirect
+to `/Documentation/index.html`.
+
+If neither resource `Documentation/index.html` or
+`Documentation/index.md` exists in the plugin JAR, Gerrit will
+automatically generate an index page for the plugin's documentation
+tree by scanning every `*.md` and `*.html` file in the Documentation/
+directory.
+
+For any discovered Markdown (`*.md`) file, Gerrit will parse the
+header of the file and extract the first level one title. This
+title text will be used as display text for a link to the HTML
+version of the page.
+
+For any discovered HTML (`*.html`) file, Gerrit will use the name
+of the file, minus the `*.html` extension, as the link text. Any
+hyphens in the file name will be replaced with spaces.
+
+If a discovered file name beings with `cmd-` it will be clustered
+into a 'Commands' section of the generated index page. All other
+files are clustered under a 'Documentation' section.
+
+Some optional information from the manifest is extracted and
+displayed as part of the index page, if present in the manifest:
+
+[width="40%",options="header"]
+|===================================================
+|Field | Source Attribute
+|Name | Implementation-Title
+|Vendor | Implementation-Vendor
+|Version | Implementation-Version
+|URL | Implementation-URL
+|API Version | Gerrit-ApiVersion
+|===================================================
+
+Deployment
+----------
+
+Compiled plugins and extensions can be deployed to a
+running Gerrit server using the SSH interface by any user with
+link:access-control.html#capability_administrateServer[Administrate Server]
+capability. Binaries can be specified in three different formats:
+
+* Absolute file path on the server's host. The server will copy
+ the plugin from this location to its own site path.
++
+----
+$ ssh -P 29418 localhost gerrit plugin install -n name $(pwd)/my-plugin.jar
+----
+
+* Valid URL, including any HTTP or FTP site reachable by the
+ server. The server will download the plugin and save a copy in
+ its own site path.
++
+----
+$ ssh -P 29418 localhost gerrit plugin install -n name http://build-server/output/our-plugin.jar
+----
+
+* As piped input to the plugin install command. The server will
+ copy input until EOF, and save a copy under its own site path.
++
+----
+$ ssh -P 29418 localhost gerrit plugin install -n name - <target/name-0.1.jar
+----
+
+Plugins can also be copied directly into the server's
+directory at `$site_path/plugins/$name.jar`. The name of
+the JAR file, minus the `.jar` extension, will be used as the
+plugin name. Unless disabled, servers periodically scan this
+directory for updated plugins. The time can be adjusted by
+link:config-gerrit.html#plugins.checkFrequency[plugins.checkFrequency].
+
+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..f65f7fc
--- /dev/null
+++ b/Documentation/dev-release.txt
@@ -0,0 +1,239 @@
+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
+---------------------------
+
+In the example commands below we assume that the last release was '2.4' and that
+we are preparing '2.5' release.
+
+Prepare the Subprojects
+~~~~~~~~~~~~~~~~~~~~~~~
+
+* Publish the latest snapshot for all subprojects
+* Freeze all subprojects and link:dev-release-subproject.html[publish]
+ them!
+
+
+Prepare Gerrit
+~~~~~~~~~~~~~~
+
+* In the 'stable-2.5' branch: Update the top level pom in Gerrit to ensure that
+none of the Subprojects point to snapshot releases
+
+* In the 'master' branch: Update the poms for the Gerrit version, push for
+review, get merged
+
+====
+ tools/version.sh --snapshot=2.5
+====
+
+* Tag
+
+====
+ git tag -a -m "gerrit 2.5-rc0" v2.5-rc0
+ git tag -a -m "gerrit 2.5" v2.5
+====
+
+* 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_api.sh
+
+Tag
+~~~
+
+* Push the New Tag
+
+====
+ git push google refs/tags/v2.5-rc0:refs/tags/v2.5-rc0
+ git push google refs/tags/v2.5:refs/tags/v2.5
+====
+
+
+Docs
+~~~~
+
+====
+ make -C Documentation PRIOR=2.4 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. The link to the documentation has to be
+updated at two places: in the project description and also in the Links
+section.
+** 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
+
+If subversion assumes a different username than your google one and asks for a
+password right away simply hit enter. Subversion will fail and then ask for
+another username and password. This time enter the username and password from
+the page linked above. After that subversion should save the username/password
+somewhere under `~/.subversion/auth` folder.
+========================================================================
+
+
+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 5143bf7..2b53772 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -18,6 +18,8 @@
* link:user-signedoffby.html[Signed-off-by Lines]
* link:access-control.html[Access Controls]
* link:error-messages.html[Error Messages]
+* link:rest-api.html[REST API]
+* link:user-notify.html[Subscribing to Email Notifications]
* link:user-submodules.html[Subscribing to Git Submodules]
Installation
@@ -33,7 +35,6 @@
* link:config-gerrit.html[System Settings]
* link:config-contact.html[User Contact Information]
-* link:config-replication.html[Git Replication/Mirroring]
* link:config-gitweb.html[Gitweb Integration]
* link:config-headerfooter.html[Site Header/Footer]
* link:config-sso.html[Single Sign-On Systems]
@@ -49,6 +50,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
---------
@@ -56,5 +59,5 @@
* link:http://code.google.com/p/gerrit/[Homepage]
* link:http://code.google.com/p/gerrit/downloads/list[Downloads]
* link:http://code.google.com/p/gerrit/issues/list[Issue Tracking]
-* link:http://code.google.com/p/gerrit/wiki/Source?tm=4[Source Code]
+* link:http://code.google.com/p/gerrit/source/checkout[Source Code]
* link:http://code.google.com/p/gerrit/wiki/Background[A History of Gerrit Code Review]
diff --git a/Documentation/install.txt b/Documentation/install.txt
index b90bbce..9da09e7 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:
@@ -203,7 +203,6 @@
* link:config-reverseproxy.html[Reverse Proxy]
* link:config-sso.html[Single Sign-On Systems]
-* link:config-replication.html[Git Replication/Mirroring]
* link:config-headerfooter.html[Site Header/Footer]
* link:config-gitweb.html[Gitweb Integration]
* link:config-gerrit.html[Other System Settings]
@@ -222,6 +221,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..4186026 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -18,6 +18,7 @@
|Google Gson | <<apache2,Apache License 2.0>>
|Google Web Toolkit | <<apache2,Apache License 2.0>>
|Guice | <<apache2,Apache License 2.0>>
+|Guava Libraries | <<apache2,Apache License 2.0>>
|Apache Commons Codec | <<apache2,Apache License 2.0>>
|Apache Commons DBCP | <<apache2,Apache License 2.0>>
|Apache Commons Http Client | <<apache2,Apache License 2.0>>
@@ -33,7 +34,6 @@
|Apache Xerces | <<apache2,Apache License 2.0>>
|OpenID4Java | <<apache2,Apache License 2.0>>
|Neko HTML | <<apache2,Apache License 2.0>>
-|Ehcache | <<apache2,Apache License 2.0>>
|mime-util | <<apache2,Apache License 2.0>>
|Jetty | <<apache2,Apache License 2.0>>, or link:http://www.eclipse.org/legal/epl-v10.html[EPL]
|Prolog Cafe | <<prolog_cafe,EPL or GPL>>
@@ -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/pgm-init.txt b/Documentation/pgm-init.txt
index c53c57d..ae5e471 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -28,6 +28,11 @@
Run in batch mode, skipping interactive prompts. Reasonable
configuration defaults are chosen based on the whims of
the Gerrit developers.
++
+If during a schema migration unused objects (e.g. tables, columns)
+are detected they are *not* automatically dropped, but only a list of
+SQL statements to drop these objects is provided. To drop the unused
+objects these SQL statements have to be executed manually.
\--no-auto-start::
Don't automatically start the daemon after initializing a
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
new file mode 100644
index 0000000..f07b9a9
--- /dev/null
+++ b/Documentation/rest-api.txt
@@ -0,0 +1,134 @@
+Gerrit Code Review - REST API
+=============================
+
+Gerrit Code Review comes with a REST like API available over HTTP.
+The API is suitable for automated tools to build upon, as well as
+supporting some ad-hoc scripting use cases.
+
+Protocol Details
+----------------
+
+Authentication
+~~~~~~~~~~~~~~
+By default all REST endpoints assume anonymous access and filter
+results to correspond to what anonymous users can read (which may
+be nothing at all).
+
+Users (and programs) may authenticate using HTTP authentication by
+supplying the HTTP password from the user's account settings page.
+Gerrit by default uses HTTP digest authentication. To authenticate,
+prefix the endpoint URL with `/a/`. For example to authenticate to
+`/projects/` request URL `/a/projects/`.
+
+Output Format
+~~~~~~~~~~~~~
+Most APIs return text format by default. JSON can be requested
+by setting the `Accept` HTTP request header to include
+`application/json`, for example:
+
+----
+ GET /projects/ HTTP/1.0
+ Accept: application/json
+----
+
+JSON responses are encoded using UTF-8 and use content type
+`application/json`. The JSON response body starts with magic prefix
+line that must be stripped before feeding the rest of the response
+body to a JSON parser:
+
+----
+ )]}'
+ [ ... valid JSON ... ]
+----
+
+The default JSON format is `JSON_COMPACT`, which skips unnecessary
+whitespace. This is not the easiest format for a human to read. Many
+examples in this documentation use `format=JSON` as a query parameter
+to obtain pretty formatting in the response. Producing (and parsing)
+the compact format is more efficient, so most tools should prefer the
+default compact format.
+
+Responses will be gzip compressed by the server if the HTTP
+`Accept-Encoding` request header is set to `gzip`. This may
+save on network transfer time for larger responses.
+
+Endpoints
+---------
+
+[[accounts_self_capabilities]]
+/accounts/self/capabilities (Account Capabilities)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Returns the global capabilities (such as createProject or
+createGroup) that are enabled for the calling user. This can be used
+by UI tools to discover if administrative features are available
+to the caller, so they can hide (or show) relevant UI actions.
+
+----
+ GET /accounts/self/capabilities?format=JSON HTTP/1.0
+
+ )]}'
+ {
+ "queryLimit": {
+ "min": 0,
+ "max": 500
+ }
+ }
+----
+
+Administrator that has authenticated with digest authentication:
+----
+ GET /a/accounts/self/capabilities?format=JSON HTTP/1.0
+ Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
+
+ )]}'
+ {
+ "administrateServer": true,
+ "queryLimit": {
+ "min": 0,
+ "max": 500
+ },
+ "createAccount": true,
+ "createGroup": true,
+ "createProject": true,
+ "killTask": true,
+ "viewCaches": true,
+ "flushCaches": true,
+ "viewConnections": true,
+ "viewQueue": true,
+ "startReplication": true
+ }
+----
+
+[[projects]]
+/projects/ (List Projects)
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+Lists the projects accessible by the caller. This is the same as
+using the link:cmd-ls-projects.html[ls-projects] command over SSH,
+and accepts the same options as query parameters.
+
+----
+ GET /projects/?format=JSON&d HTTP/1.0
+
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json;charset=UTF-8
+
+ )]}'
+ {
+ "external/bison": {
+ "description": "GNU parser generator"
+ },
+ "external/gcc": {},
+ "external/openssl": {
+ "description": "encryption\ncrypto routines"
+ },
+ "test": {
+ "description": "\u003chtml\u003e is escaped"
+ }
+ }
+----
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
new file mode 100644
index 0000000..ae3c2d09
--- /dev/null
+++ b/Documentation/user-notify.txt
@@ -0,0 +1,127 @@
+Gerrit Code Review - Email Notifications
+========================================
+
+Description
+-----------
+
+Gerrit can automatically notify users by email when new changes are
+uploaded for review, after comments have been posted on a change,
+or after the change has been submitted to a branch.
+
+User Level Settings
+-------------------
+
+Individual users can configure email subscriptions by editing
+watched projects through Settings > Watched Projects with the web UI.
+
+Specific projects may be watched, or the special project
+`All-Projects` can be watched to watch all projects that
+are visible to the user.
+
+Change search expressions can be used to filter change notifications
+to specific subsets, for example `branch:master` to only see changes
+proposed for the master branch.
+
+Project Level Settings
+----------------------
+
+Project owners and site administrators can configure project level
+notifications, enabling Gerrit Code Review to automatically send
+emails to team mailing lists, or groups of users. Project settings
+are stored inside of the `refs/meta/config` branch of each Git
+repository, and are placed inside of the `project.config` file.
+
+To edit the project level notify settings, ensure the project owner
+has Push permission already granted for the `refs/meta/config`
+branch. Consult link:access-control.html[access controls] for
+details on how access permissions work.
+
+Initialize a temporary Git repository to edit the configuration:
+====
+ mkdir cfg_dir
+ cd cfg_dir
+ git init
+====
+
+Download the existing configuration from Gerrit:
+====
+ git fetch ssh://localhost:29418/project refs/meta/config
+ git checkout FETCH_HEAD
+====
+
+Enable notifications to an email address by adding to
+`project.config`, this can be done using the `git config` command:
+====
+ git config -f project.config --add notify.team.email team-address@example.com
+ git config -f project.config --add notify.team.email paranoid-manager@example.com
+====
+
+Examining the project.config file with any text editor should show
+a new notify section describing the email addresses to deliver to:
+----
+ [notify "team"]
+ email = team-address@example.com
+ email = paranoid-manager@example.com
+----
+
+Each notify section within a single project.config file must have a
+unique name. The section name itself does not matter and may later
+appear in the web UI. Naming a section after the email address or
+group it delivers to is typical. Multiple sections can be specified
+if different filters are needed.
+
+Commit the configuration change, and push it back:
+====
+ git commit -a -m "Notify team-address@example.com of changes"
+ git push ssh://localhost:29418/project HEAD:refs/meta/config
+====
+
+[[notify.name.email]]notify.<name>.email::
++
+List of email addresses to send matching notifications to. Each
+email address should be placed on its own line.
++
+Internal groups within Gerrit Code Review can also be named using
+`group NAME` syntax. If this format is used the group's UUID must
+also appear in the corresponding `groups` file. Gerrit will expand
+the group membership and BCC all current users.
+
+[[notify.name.type]]notify.<name>.type::
++
+Types of notifications to send. If not specified, all notifications
+are sent.
++
+* `new_changes`: Only newly created changes.
+* `all_comments`: Only comments on existing changes.
+* `submitted_changes`: Only changes that have been submitted.
+* `all`: All notifications.
+
++
+Like email, this variable may be a list of options.
+
+[[notify.name.filter]]notify.<name>.filter::
++
+link:user-search.html[Change search expression] to match changes that
+should be sent to the emails named in this section. Within a Git-style
+configuration file double quotes around complex operator values may
+need to be escaped, e.g. `filter = branch:\"^(maint|stable)-.*\"`.
+
+When sending email to a bare email address in a notify block, Gerrit
+Code Review ignores read access controls and assumes the administrator
+has set the filtering options correctly. Project owners can implement
+security filtering by adding the `visibleto:groupname` predicate to
+the filter expression, for example:
+
+====
+ [notify "Developers"]
+ email = team-address@example.com
+ filter = visibleto:Developers
+====
+
+When sending email to an internal group, the internal group's read
+access is automatically checked by Gerrit and therefore does not
+need to use the `visibleto:` operator in the filter.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 3aafe8c..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
|=================================================
@@ -75,7 +76,8 @@
[[owner]]
owner:'USER'::
+
-Changes originally submitted by 'USER'.
+Changes originally submitted by 'USER'. The special case of
+`owner:self` will find changes owned by the caller.
[[ownerin]]
ownerin:'GROUP'::
@@ -85,7 +87,9 @@
[[reviewer]]
reviewer:'USER'::
+
-Changes that have been, or need to be, reviewed by 'USER'.
+Changes that have been, or need to be, reviewed by 'USER'. The
+special case of `reviewer:self` will find changes where the caller
+has been added as a reviewer.
[[reviewerin]]
reviewerin:'GROUP'::
@@ -213,10 +217,24 @@
True if there is at least one non-zero score on the change, in any
approval category, by any user.
+is:owner::
++
+True on any change where the current user is the change owner.
+Same as `owner:self`.
+
+is:reviewer::
++
+True on any change where the current user is a reviewer.
+Same as `reviewer:self`.
+
is:open::
+
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.
@@ -373,16 +391,20 @@
starredby:'USER'::
+
Matches changes that have been starred by 'USER'.
+The special case `starredby:self` applies to the caller.
watchedby:'USER'::
+
Matches changes that 'USER' has configured watch filters for.
+The special case `watchedby:self` applies to the caller.
draftby:'USER'::
+
Matches changes that 'USER' has left unpublished drafts on.
Since the drafts are unpublished, it is not possible to see the
-draft text, or even how many drafts there are.
+draft text, or even how many drafts there are. The special case
+of `draftby:self` will find changes where the caller has created
+a draft comment.
limit:'CNT'::
+
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/ReleaseNotes/ReleaseNotes-2.5.txt b/ReleaseNotes/ReleaseNotes-2.5.txt
new file mode 100644
index 0000000..60c4f08
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.txt
@@ -0,0 +1,55 @@
+Release notes for Gerrit 2.5
+============================
+
+Gerrit 2.5 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.war]
+
+Upgrade Warnings
+----------------
+
+Replication
+~~~~~~~~~~~
+
+Gerrit 2.5 no longer includes replication support out of the box.
+Servers that reply upon `replication.config` to copy Git repository
+data to other locations must also install the replication plugin.
+
+Cache Configuration
+~~~~~~~~~~~~~~~~~~~
+
+Disk caches are now backed by individual H2 databases, rather than
+Ehcache's own private format. Administrators are encouraged to clear
+the `'$site_path'/cache` directory before starting the new server.
+
+The `cache.NAME.diskLimit` configuration variable is now expressed in
+bytes of disk used. This is a change from previous versions of Gerrit,
+which expressed the limit as the number of entries rather than bytes.
+Bytes of disk is a more accurate way to size what is held. Admins that
+set this variable must update their configurations, as the old values
+are too small. For example a setting of `diskLimit = 65535` will only
+store 64 KiB worth of data on disk and can no longer hold 65,000 patch
+sets. It is recommended to delete the diskLimit variable (if set) and
+rely on the built-in default of `128m`.
+
+The `cache.diff.memoryLimit` and `cache.diff_intraline.memoryLimit`
+configuration variables are now expressed in bytes of memory used,
+rather than number of entries in the cache. This is a change from
+previous versions of Gerrit and gives administrators more control over
+how memory is partioned within a server. Admins that set this variable
+must update their configurations, as the old values are too small.
+For example a setting of `memoryLimit = 1024` now means only 1 KiB of
+data (which may not even hold 1 patch set), not 1024 patch sets. It
+is recommended to set these to `10m` for 10 MiB of memory, and
+increase as necessary.
+
+The `cache.NAME.maxAge` variable now means the maximum amount of time
+that can elapse between reads of the source data into the cache, no
+matter how often it is being accessed. In prior versions it meant how
+long an item could be held without being requested by a client before
+it was discarded. The new meaning of elapsed time before consulting
+the source data is more useful, as it enables a strict bound on how
+stale the cached data can be. This is especially useful for slave
+servers account and permission data, or the `ldap_groups` cache, where
+updates are often made to the source without telling Gerrit to reload
+the cache.
diff --git a/SUBMITTING_PATCHES b/SUBMITTING_PATCHES
index e766ef1..553ab34 100644
--- a/SUBMITTING_PATCHES
+++ b/SUBMITTING_PATCHES
@@ -5,7 +5,7 @@
- Make sure all code is under the Apache License, 2.0.
- Publish your changes for review:
- git push https://gerrit-review.googlesource.com/gerrit HEAD:refs/for/master
+ git push https://gerrit.googlesource.com/gerrit HEAD:refs/for/master
Long Version:
@@ -70,7 +70,7 @@
Push your patches over HTTPS to the review server, possibly through
a remembered remote to make this easier in the future:
- git config remote.review.url https://google-review.googlesource.com/gerrit
+ git config remote.review.url https://gerrit.googlesource.com/gerrit
git config remote.review.push HEAD:refs/for/master
git push review
diff --git a/gerrit-antlr/.gitignore b/gerrit-antlr/.gitignore
index 194bedc..fb047af 100644
--- a/gerrit-antlr/.gitignore
+++ b/gerrit-antlr/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-antlr.iml
\ No newline at end of file
diff --git a/gerrit-antlr/pom.xml b/gerrit-antlr/pom.xml
index aa0d7fd..34cb46f 100644
--- a/gerrit-antlr/pom.xml
+++ b/gerrit-antlr/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-antlr</artifactId>
diff --git a/gerrit-ehcache/.gitignore b/gerrit-cache-h2/.gitignore
similarity index 83%
copy from gerrit-ehcache/.gitignore
copy to gerrit-cache-h2/.gitignore
index 20251d4..cb430b8 100644
--- a/gerrit-ehcache/.gitignore
+++ b/gerrit-cache-h2/.gitignore
@@ -1,5 +1,6 @@
/target
/.classpath
/.project
-/.settings/org.eclipse.m2e.core.prefs
/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-cache-h2.iml
diff --git a/gerrit-cache-h2/.settings/org.eclipse.core.resources.prefs b/gerrit-cache-h2/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..fc11c3f
--- /dev/null
+++ b/gerrit-cache-h2/.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-ehcache/.settings/org.eclipse.core.runtime.prefs b/gerrit-cache-h2/.settings/org.eclipse.core.runtime.prefs
similarity index 100%
rename from gerrit-ehcache/.settings/org.eclipse.core.runtime.prefs
rename to gerrit-cache-h2/.settings/org.eclipse.core.runtime.prefs
diff --git a/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs b/gerrit-cache-h2/.settings/org.eclipse.jdt.core.prefs
similarity index 99%
rename from gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
rename to gerrit-cache-h2/.settings/org.eclipse.jdt.core.prefs
index e89c048..470942d 100644
--- a/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-cache-h2/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Thu Jan 19 12:55:44 PST 2012
+#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
diff --git a/gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs b/gerrit-cache-h2/.settings/org.eclipse.jdt.ui.prefs
similarity index 100%
rename from gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs
rename to gerrit-cache-h2/.settings/org.eclipse.jdt.ui.prefs
diff --git a/gerrit-ehcache/pom.xml b/gerrit-cache-h2/pom.xml
similarity index 73%
rename from gerrit-ehcache/pom.xml
rename to gerrit-cache-h2/pom.xml
index 839c52b0..4d4303c 100644
--- a/gerrit-ehcache/pom.xml
+++ b/gerrit-cache-h2/pom.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
-Copyright (C) 2010 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.
@@ -22,26 +22,31 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
- <artifactId>gerrit-ehcache</artifactId>
- <name>Gerrit Code Review - Ehcache Bindings</name>
+ <artifactId>gerrit-cache-h2</artifactId>
+ <name>Gerrit Code Review - Guava + H2 caching</name>
<description>
- Bindings to Ehcache
+ Implementation of caching backed by Guava and H2
</description>
<dependencies>
<dependency>
- <groupId>net.sf.ehcache</groupId>
- <artifactId>ehcache-core</artifactId>
- </dependency>
-
- <dependency>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-server</artifactId>
<version>${project.version}</version>
</dependency>
+
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.h2database</groupId>
+ <artifactId>h2</artifactId>
+ </dependency>
</dependencies>
</project>
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
new file mode 100644
index 0000000..8bb0709
--- /dev/null
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
@@ -0,0 +1,121 @@
+// 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.cache.h2;
+
+import com.google.common.base.Strings;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.cache.CacheBinding;
+import com.google.gerrit.server.cache.MemoryCacheFactory;
+import com.google.gerrit.server.cache.PersistentCacheFactory;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.concurrent.TimeUnit;
+
+public class DefaultCacheFactory implements MemoryCacheFactory {
+ public static class Module extends LifecycleModule {
+ @Override
+ protected void configure() {
+ bind(DefaultCacheFactory.class);
+ bind(MemoryCacheFactory.class).to(DefaultCacheFactory.class);
+ bind(PersistentCacheFactory.class).to(H2CacheFactory.class);
+ listener().to(H2CacheFactory.class);
+ }
+ }
+
+ private final Config cfg;
+
+ @Inject
+ public DefaultCacheFactory(@GerritServerConfig Config config) {
+ this.cfg = config;
+ }
+
+ @Override
+ public <K, V> Cache<K, V> build(CacheBinding<K, V> def) {
+ return create(def, false).build();
+ }
+
+ @Override
+ public <K, V> LoadingCache<K, V> build(
+ CacheBinding<K, V> def,
+ CacheLoader<K, V> loader) {
+ return create(def, false).build(loader);
+ }
+
+ @SuppressWarnings("unchecked")
+ <K, V> CacheBuilder<K, V> create(
+ CacheBinding<K, V> def,
+ boolean unwrapValueHolder) {
+ CacheBuilder<K,V> builder = newCacheBuilder();
+ builder.recordStats();
+ builder.maximumWeight(cfg.getLong(
+ "cache", def.name(), "memoryLimit",
+ def.maximumWeight()));
+
+ Weigher<K, V> weigher = def.weigher();
+ if (weigher != null && unwrapValueHolder) {
+ final Weigher<K, V> impl = weigher;
+ weigher = (Weigher<K, V>) new Weigher<K, ValueHolder<V>> () {
+ @Override
+ public int weigh(K key, ValueHolder<V> value) {
+ return impl.weigh(key, value.value);
+ }
+ };
+ } else if (weigher == null) {
+ weigher = unitWeight();
+ }
+ builder.weigher(weigher);
+
+ Long age = def.expireAfterWrite(TimeUnit.SECONDS);
+ if (has(def.name(), "maxAge")) {
+ builder.expireAfterWrite(ConfigUtil.getTimeUnit(cfg,
+ "cache", def.name(), "maxAge",
+ age != null ? age : 0,
+ TimeUnit.SECONDS), TimeUnit.SECONDS);
+ } else if (age != null) {
+ builder.expireAfterWrite(age, TimeUnit.SECONDS);
+ }
+
+ return builder;
+ }
+
+ private boolean has(String name, String var) {
+ return !Strings.isNullOrEmpty(cfg.getString("cache", name, var));
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static <K, V> CacheBuilder<K, V> newCacheBuilder() {
+ CacheBuilder builder = CacheBuilder.newBuilder();
+ return builder;
+ }
+
+ private static <K, V> Weigher<K, V> unitWeight() {
+ return new Weigher<K, V>() {
+ @Override
+ public int weigh(K key, V value) {
+ return 1;
+ }
+ };
+ }
+}
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
new file mode 100644
index 0000000..27da20f
--- /dev/null
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -0,0 +1,198 @@
+// 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.cache.h2;
+
+import com.google.common.base.Preconditions;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.cache.CacheBinding;
+import com.google.gerrit.server.cache.PersistentCacheFactory;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+class H2CacheFactory implements PersistentCacheFactory, LifecycleListener {
+ static final Logger log = LoggerFactory.getLogger(H2CacheFactory.class);
+
+ private final DefaultCacheFactory defaultFactory;
+ private final Config config;
+ private final File cacheDir;
+ private final List<H2CacheImpl<?, ?>> caches;
+ private final ExecutorService executor;
+ private final ScheduledExecutorService cleanup;
+ private volatile boolean started;
+
+ @Inject
+ H2CacheFactory(
+ DefaultCacheFactory defaultCacheFactory,
+ @GerritServerConfig Config cfg,
+ SitePaths site) {
+ defaultFactory = defaultCacheFactory;
+ config = cfg;
+
+ File loc = site.resolve(cfg.getString("cache", null, "directory"));
+ if (loc == null) {
+ cacheDir = null;
+ } else if (loc.exists() || loc.mkdirs()) {
+ if (loc.canWrite()) {
+ log.info("Enabling disk cache " + loc.getAbsolutePath());
+ cacheDir = loc;
+ } else {
+ log.warn("Can't write to disk cache: " + loc.getAbsolutePath());
+ cacheDir = null;
+ }
+ } else {
+ log.warn("Can't create disk cache: " + loc.getAbsolutePath());
+ cacheDir = null;
+ }
+
+ caches = Lists.newLinkedList();
+
+ if (cacheDir != null) {
+ executor = Executors.newFixedThreadPool(
+ 1,
+ new ThreadFactoryBuilder()
+ .setNameFormat("DiskCache-Store-%d")
+ .build());
+ cleanup = Executors.newScheduledThreadPool(
+ 1,
+ new ThreadFactoryBuilder()
+ .setNameFormat("DiskCache-Prune-%d")
+ .setDaemon(true)
+ .build());
+ } else {
+ executor = null;
+ cleanup = null;
+ }
+ }
+
+ @Override
+ public void start() {
+ started = true;
+ if (executor != null) {
+ for (final H2CacheImpl<?, ?> cache : caches) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ cache.start();
+ }
+ });
+
+ cleanup.schedule(new Runnable() {
+ @Override
+ public void run() {
+ cache.prune(cleanup);
+ }
+ }, 30, TimeUnit.SECONDS);
+ }
+ }
+ }
+
+ @Override
+ public void stop() {
+ if (executor != null) {
+ try {
+ cleanup.shutdownNow();
+
+ List<Runnable> pending = executor.shutdownNow();
+ if (executor.awaitTermination(15, TimeUnit.MINUTES)) {
+ if (pending != null && !pending.isEmpty()) {
+ log.info(String.format("Finishing %d disk cache updates", pending.size()));
+ for (Runnable update : pending) {
+ update.run();
+ }
+ }
+ } else {
+ log.info("Timeout waiting for disk cache to close");
+ }
+ } catch (InterruptedException e) {
+ log.warn("Interrupted waiting for disk cache to shutdown");
+ }
+ }
+ for (H2CacheImpl<?, ?> cache : caches) {
+ cache.stop();
+ }
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes", "cast"})
+ @Override
+ public <K, V> Cache<K, V> build(CacheBinding<K, V> def) {
+ Preconditions.checkState(!started, "cache must be built before start");
+ long limit = config.getLong("cache", def.name(), "diskLimit", 128 << 20);
+
+ if (cacheDir == null || limit <= 0) {
+ return defaultFactory.build(def);
+ }
+
+ SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit);
+ H2CacheImpl<K, V> cache = new H2CacheImpl<K, V>(
+ executor, store, def.keyType(),
+ (Cache<K, ValueHolder<V>>) defaultFactory.create(def, true).build());
+ caches.add(cache);
+ return cache;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <K, V> LoadingCache<K, V> build(
+ CacheBinding<K, V> def,
+ CacheLoader<K, V> loader) {
+ Preconditions.checkState(!started, "cache must be built before start");
+ long limit = config.getLong("cache", def.name(), "diskLimit", 128 << 20);
+
+ if (cacheDir == null || limit <= 0) {
+ return defaultFactory.build(def, loader);
+ }
+
+ SqlStore<K, V> store = newSqlStore(def.name(), def.keyType(), limit);
+ Cache<K, ValueHolder<V>> mem = (Cache<K, ValueHolder<V>>)
+ defaultFactory.create(def, true)
+ .build((CacheLoader<K, V>) new H2CacheImpl.Loader<K, V>(
+ executor, store, loader));
+ H2CacheImpl<K, V> cache = new H2CacheImpl<K, V>(
+ executor, store, def.keyType(), mem);
+ caches.add(cache);
+ return cache;
+ }
+
+ private <V, K> SqlStore<K, V> newSqlStore(
+ String name,
+ TypeLiteral<K> keyType,
+ long maxSize) {
+ File db = new File(cacheDir, name).getAbsoluteFile();
+ String url = "jdbc:h2:" + db.toURI().toString();
+ return new SqlStore<K, V>(url, keyType, maxSize);
+ }
+}
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
new file mode 100644
index 0000000..ad437b7
--- /dev/null
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -0,0 +1,709 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.gerrit.server.cache.h2;
+
+import com.google.common.cache.AbstractLoadingCache;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.CacheStats;
+import com.google.common.cache.LoadingCache;
+import com.google.common.hash.BloomFilter;
+import com.google.common.hash.Funnel;
+import com.google.common.hash.Funnels;
+import com.google.common.hash.PrimitiveSink;
+import com.google.inject.TypeLiteral;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Map;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Hybrid in-memory and database backed cache built on H2.
+ * <p>
+ * This cache can be used as either a recall cache, or a loading cache if a
+ * CacheLoader was supplied to its constructor at build time. Before creating an
+ * entry the in-memory cache is checked for the item, then the database is
+ * checked, and finally the CacheLoader is used to construct the item. This is
+ * mostly useful for CacheLoaders that are computationally intensive, such as
+ * the PatchListCache.
+ * <p>
+ * Cache stores and invalidations are performed on a background thread, hiding
+ * the latency associated with serializing the key and value pairs and writing
+ * them to the database log.
+ * <p>
+ * A BloomFilter is used around the database to reduce the number of SELECTs
+ * issued against the database for new cache items that have not been seen
+ * before, a common operation for the PatchListCache. The BloomFilter is sized
+ * when the cache starts to be 64,000 entries or double the number of items
+ * currently in the database table.
+ * <p>
+ * This cache does not export its items as a ConcurrentMap.
+ *
+ * @see H2CacheFactory
+ */
+public class H2CacheImpl<K, V> extends AbstractLoadingCache<K, V> {
+ private static final Logger log = LoggerFactory.getLogger(H2CacheImpl.class);
+
+ private final Executor executor;
+ private final SqlStore<K, V> store;
+ private final TypeLiteral<K> keyType;
+ private final Cache<K, ValueHolder<V>> mem;
+
+ H2CacheImpl(Executor executor,
+ SqlStore<K, V> store,
+ TypeLiteral<K> keyType,
+ Cache<K, ValueHolder<V>> mem) {
+ this.executor = executor;
+ this.store = store;
+ this.keyType = keyType;
+ this.mem = mem;
+ }
+
+ @Override
+ public V getIfPresent(Object objKey) {
+ if (!keyType.getRawType().isInstance(objKey)) {
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ K key = (K) objKey;
+
+ ValueHolder<V> h = mem.getIfPresent(key);
+ if (h != null) {
+ return h.value;
+ }
+
+ if (store.mightContain(key)) {
+ h = store.getIfPresent(key);
+ if (h != null) {
+ mem.put(key, h);
+ return h.value;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public V get(K key) throws ExecutionException {
+ if (mem instanceof LoadingCache) {
+ return ((LoadingCache<K, ValueHolder<V>>) mem).get(key).value;
+ }
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void put(final K key, V val) {
+ final ValueHolder<V> h = new ValueHolder<V>(val);
+ h.created = System.currentTimeMillis();
+ mem.put(key, h);
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ store.put(key, h);
+ }
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void invalidate(final Object key) {
+ if (keyType.getRawType().isInstance(key) && store.mightContain((K) key)) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ store.invalidate((K) key);
+ }
+ });
+ }
+ mem.invalidate(key);
+ }
+
+ @Override
+ public void invalidateAll() {
+ store.invalidateAll();
+ mem.invalidateAll();
+ }
+
+ @Override
+ public long size() {
+ return mem.size();
+ }
+
+ @Override
+ public CacheStats stats() {
+ return mem.stats();
+ }
+
+ public DiskStats diskStats() {
+ return store.diskStats();
+ }
+
+ void start() {
+ store.open();
+ }
+
+ void stop() {
+ for (Map.Entry<K, ValueHolder<V>> e : mem.asMap().entrySet()) {
+ ValueHolder<V> h = e.getValue();
+ if (!h.clean) {
+ store.put(e.getKey(), h);
+ }
+ }
+ store.close();
+ }
+
+ void prune(final ScheduledExecutorService service) {
+ store.prune(mem);
+
+ Calendar cal = Calendar.getInstance();
+ cal.set(Calendar.HOUR_OF_DAY, 01);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.add(Calendar.DAY_OF_MONTH, 1);
+
+ long delay = cal.getTimeInMillis() - System.currentTimeMillis();
+ service.schedule(new Runnable() {
+ @Override
+ public void run() {
+ prune(service);
+ }
+ }, delay, TimeUnit.MILLISECONDS);
+ }
+
+ public static class DiskStats {
+ long size;
+ long space;
+ long hitCount;
+ long missCount;
+
+ public long size() {
+ return size;
+ }
+
+ public long space() {
+ return space;
+ }
+
+ public long hitCount() {
+ return hitCount;
+ }
+
+ public long requestCount() {
+ return hitCount + missCount;
+ }
+ }
+
+ static class ValueHolder<V> {
+ final V value;
+ long created;
+ volatile boolean clean;
+
+ ValueHolder(V value) {
+ this.value = value;
+ }
+ }
+
+ static class Loader<K, V> extends CacheLoader<K, ValueHolder<V>> {
+ private final Executor executor;
+ private final SqlStore<K, V> store;
+ private final CacheLoader<K, V> loader;
+
+ Loader(Executor executor, SqlStore<K, V> store, CacheLoader<K, V> loader) {
+ this.executor = executor;
+ this.store = store;
+ this.loader = loader;
+ }
+
+ @Override
+ public ValueHolder<V> load(final K key) throws Exception {
+ if (store.mightContain(key)) {
+ ValueHolder<V> h = store.getIfPresent(key);
+ if (h != null) {
+ return h;
+ }
+ }
+
+ final ValueHolder<V> h = new ValueHolder<V>(loader.load(key));
+ h.created = System.currentTimeMillis();
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ store.put(key, h);
+ }
+ });
+ return h;
+ }
+ }
+
+ private static class KeyType<K> {
+ String columnType() {
+ return "OTHER";
+ }
+
+ @SuppressWarnings("unchecked")
+ K get(ResultSet rs, int col) throws SQLException {
+ return (K) rs.getObject(col);
+ }
+
+ void set(PreparedStatement ps, int col, K value) throws SQLException {
+ ps.setObject(col, value);
+ }
+
+ Funnel<K> funnel() {
+ return new Funnel<K>() {
+ @Override
+ public void funnel(K from, PrimitiveSink into) {
+ try {
+ ObjectOutputStream ser =
+ new ObjectOutputStream(new SinkOutputStream(into));
+ ser.writeObject(from);
+ ser.flush();
+ } catch (IOException err) {
+ throw new RuntimeException("Cannot hash as Serializable", err);
+ }
+ }
+ };
+ }
+
+ @SuppressWarnings("unchecked")
+ static <K> KeyType<K> create(TypeLiteral<K> type) {
+ if (type.getRawType() == String.class) {
+ return (KeyType<K>) STRING;
+ }
+ return (KeyType<K>) OTHER;
+ }
+
+ static final KeyType<?> OTHER = new KeyType<Object>();
+ static final KeyType<String> STRING = new KeyType<String>() {
+ @Override
+ String columnType() {
+ return "VARCHAR(4096)";
+ }
+
+ @Override
+ String get(ResultSet rs, int col) throws SQLException {
+ return rs.getString(col);
+ }
+
+ @Override
+ void set(PreparedStatement ps, int col, String value)
+ throws SQLException {
+ ps.setString(col, value);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ Funnel<String> funnel() {
+ Funnel<?> s = Funnels.stringFunnel();
+ return (Funnel<String>) s;
+ }
+ };
+ }
+
+ static class SqlStore<K, V> {
+ private final String url;
+ private final KeyType<K> keyType;
+ private final long maxSize;
+ private final BlockingQueue<SqlHandle> handles;
+ private final AtomicLong hitCount = new AtomicLong();
+ private final AtomicLong missCount = new AtomicLong();
+ private volatile BloomFilter<K> bloomFilter;
+ private int estimatedSize;
+
+ SqlStore(String jdbcUrl, TypeLiteral<K> keyType, long maxSize) {
+ this.url = jdbcUrl;
+ this.keyType = KeyType.create(keyType);
+ this.maxSize = maxSize;
+
+ int cores = Runtime.getRuntime().availableProcessors();
+ int keep = Math.min(cores, 16);
+ this.handles = new ArrayBlockingQueue<SqlHandle>(keep);
+ }
+
+ synchronized void open() {
+ if (bloomFilter == null) {
+ bloomFilter = buildBloomFilter();
+ }
+ }
+
+ void close() {
+ SqlHandle h;
+ while ((h = handles.poll()) != null) {
+ h.close();
+ }
+ }
+
+ boolean mightContain(K key) {
+ BloomFilter<K> b = bloomFilter;
+ if (b == null) {
+ synchronized (this) {
+ b = bloomFilter;
+ if (b == null) {
+ b = buildBloomFilter();
+ bloomFilter = b;
+ }
+ }
+ }
+ return b == null || b.mightContain(key);
+ }
+
+ private BloomFilter<K> buildBloomFilter() {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ Statement s = c.conn.createStatement();
+ try {
+ ResultSet r;
+ if (estimatedSize <= 0) {
+ r = s.executeQuery("SELECT COUNT(*) FROM data");
+ try {
+ estimatedSize = r.next() ? r.getInt(1) : 0;
+ } finally {
+ r.close();
+ }
+ }
+
+ BloomFilter<K> b = newBloomFilter();
+ r = s.executeQuery("SELECT k FROM data");
+ try {
+ while (r.next()) {
+ b.put(keyType.get(r, 1));
+ }
+ } finally {
+ r.close();
+ }
+ return b;
+ } finally {
+ s.close();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot build BloomFilter for " + url, e);
+ c = close(c);
+ return null;
+ } finally {
+ release(c);
+ }
+ }
+
+ ValueHolder<V> getIfPresent(K key) {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ if (c.get == null) {
+ c.get = c.conn.prepareStatement("SELECT v FROM data WHERE k=?");
+ }
+ keyType.set(c.get, 1, key);
+ ResultSet r = c.get.executeQuery();
+ try {
+ if (!r.next()) {
+ missCount.incrementAndGet();
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ V val = (V) r.getObject(1);
+ ValueHolder<V> h = new ValueHolder<V>(val);
+ h.clean = true;
+ hitCount.incrementAndGet();
+ touch(c, key);
+ return h;
+ } finally {
+ r.close();
+ c.get.clearParameters();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot read cache " + url + " for " + key, e);
+ c = close(c);
+ return null;
+ } finally {
+ release(c);
+ }
+ }
+
+ private void touch(SqlHandle c, K key) throws SQLException {
+ if (c.touch == null) {
+ c.touch =c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=?");
+ }
+ try {
+ c.touch.setTimestamp(1, new Timestamp(System.currentTimeMillis()));
+ keyType.set(c.touch, 2, key);
+ c.touch.executeUpdate();
+ } finally {
+ c.touch.clearParameters();
+ }
+ }
+
+ void put(K key, ValueHolder<V> holder) {
+ if (holder.clean) {
+ return;
+ }
+
+ BloomFilter<K> b = bloomFilter;
+ if (b != null) {
+ b.put(key);
+ bloomFilter = b;
+ }
+
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ if (c.put == null) {
+ c.put = c.conn.prepareStatement("MERGE INTO data VALUES(?,?,?,?)");
+ }
+ try {
+ keyType.set(c.put, 1, key);
+ c.put.setObject(2, holder.value);
+ c.put.setTimestamp(3, new Timestamp(holder.created));
+ c.put.setTimestamp(4, new Timestamp(System.currentTimeMillis()));
+ c.put.executeUpdate();
+ holder.clean = true;
+ } finally {
+ c.put.clearParameters();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot put into cache " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ void invalidate(K key) {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ invalidate(c, key);
+ } catch (SQLException e) {
+ log.warn("Cannot invalidate cache " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ private void invalidate(SqlHandle c, K key) throws SQLException {
+ if (c.invalidate == null) {
+ c.invalidate = c.conn.prepareStatement("DELETE FROM data WHERE k=?");
+ }
+ try {
+ keyType.set(c.invalidate, 1, key);
+ c.invalidate.executeUpdate();
+ } finally {
+ c.invalidate.clearParameters();
+ }
+ }
+
+ void invalidateAll() {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ Statement s = c.conn.createStatement();
+ try {
+ s.executeUpdate("DELETE FROM data");
+ } finally {
+ s.close();
+ }
+ bloomFilter = newBloomFilter();
+ } catch (SQLException e) {
+ log.warn("Cannot invalidate cache " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ void prune(Cache<K, ?> mem) {
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ Statement s = c.conn.createStatement();
+ try {
+ long used = 0;
+ ResultSet r = s.executeQuery("SELECT"
+ + " SUM(OCTET_LENGTH(k) + OCTET_LENGTH(v))"
+ + " FROM data");
+ try {
+ used = r.next() ? r.getLong(1) : 0;
+ } finally {
+ r.close();
+ }
+ if (used <= maxSize) {
+ return;
+ }
+
+ r = s.executeQuery("SELECT"
+ + " k"
+ + ",OCTET_LENGTH(k) + OCTET_LENGTH(v)"
+ + " FROM data"
+ + " ORDER BY accessed");
+ try {
+ while (maxSize < used && r.next()) {
+ K key = keyType.get(r, 1);
+ if (mem.getIfPresent(key) != null) {
+ touch(c, key);
+ } else {
+ invalidate(c, key);
+ used -= r.getLong(2);
+ }
+ }
+ } finally {
+ r.close();
+ }
+ } finally {
+ s.close();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot prune cache " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ }
+
+ DiskStats diskStats() {
+ DiskStats d = new DiskStats();
+ d.hitCount = hitCount.get();
+ d.missCount = missCount.get();
+ SqlHandle c = null;
+ try {
+ c = acquire();
+ Statement s = c.conn.createStatement();
+ try {
+ ResultSet r = s.executeQuery("SELECT"
+ + " COUNT(*)"
+ + ",SUM(OCTET_LENGTH(k) + OCTET_LENGTH(v))"
+ + " FROM data");
+ try {
+ if (r.next()) {
+ d.size = r.getLong(1);
+ d.space = r.getLong(2);
+ }
+ } finally {
+ r.close();
+ }
+ } finally {
+ s.close();
+ }
+ } catch (SQLException e) {
+ log.warn("Cannot get DiskStats for " + url, e);
+ c = close(c);
+ } finally {
+ release(c);
+ }
+ return d;
+ }
+
+ private SqlHandle acquire() throws SQLException {
+ SqlHandle h = handles.poll();
+ return h != null ? h : new SqlHandle(url, keyType);
+ }
+
+ private void release(SqlHandle h) {
+ if (h != null && !handles.offer(h)) {
+ h.close();
+ }
+ }
+
+ private SqlHandle close(SqlHandle h) {
+ if (h != null) {
+ h.close();
+ }
+ return null;
+ }
+
+ private BloomFilter<K> newBloomFilter() {
+ int cnt = Math.max(64 * 1024, 2 * estimatedSize);
+ return BloomFilter.create(keyType.funnel(), cnt);
+ }
+ }
+
+ static class SqlHandle {
+ private final String url;
+ Connection conn;
+ PreparedStatement get;
+ PreparedStatement put;
+ PreparedStatement touch;
+ PreparedStatement invalidate;
+
+ SqlHandle(String url, KeyType<?> type) throws SQLException {
+ this.url = url;
+ this.conn = org.h2.Driver.load().connect(url, null);
+ Statement stmt = conn.createStatement();
+ try {
+ stmt.execute("CREATE TABLE IF NOT EXISTS data"
+ + "(k " + type.columnType() + " NOT NULL PRIMARY KEY HASH"
+ + ",v OTHER NOT NULL"
+ + ",created TIMESTAMP NOT NULL"
+ + ",accessed TIMESTAMP NOT NULL"
+ + ")");
+ } finally {
+ stmt.close();
+ }
+ }
+
+ void close() {
+ get = closeStatement(get);
+ put = closeStatement(put);
+ touch = closeStatement(touch);
+ invalidate = closeStatement(invalidate);
+
+ if (conn != null) {
+ try {
+ conn.close();
+ } catch (SQLException e) {
+ log.warn("Cannot close connection to " + url, e);
+ } finally {
+ conn = null;
+ }
+ }
+ }
+
+ private PreparedStatement closeStatement(PreparedStatement ps) {
+ if (ps != null) {
+ try {
+ ps.close();
+ } catch (SQLException e) {
+ log.warn("Cannot close statement for " + url, e);
+ }
+ }
+ return null;
+ }
+ }
+
+ private static class SinkOutputStream extends OutputStream {
+ private final PrimitiveSink sink;
+
+ SinkOutputStream(PrimitiveSink sink) {
+ this.sink = sink;
+ }
+
+ @Override
+ public void write(int b) {
+ sink.putByte((byte)b);
+ }
+
+ @Override
+ public void write(byte[] b, int p, int n) {
+ sink.putBytes(b, p, n);
+ }
+ }
+}
diff --git a/gerrit-common/.gitignore b/gerrit-common/.gitignore
index 194bedc..759f12c 100644
--- a/gerrit-common/.gitignore
+++ b/gerrit-common/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-common.iml
\ No newline at end of file
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
index e7933ea..9b3fe5f 100644
--- a/gerrit-common/pom.xml
+++ b/gerrit-common/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-common</artifactId>
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..c0c317c 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
@@ -14,13 +14,11 @@
package com.google.gerrit.common;
-import com.google.gerrit.common.data.AccountInfo;
import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gwtorm.client.KeyUtil;
public class PageLinks {
@@ -59,12 +57,9 @@
return "/admin/projects/" + p.get() + ",access";
}
- public static String toAccountDashboard(final AccountInfo acct) {
- return toAccountDashboard(acct.getId());
- }
-
- public static String toAccountDashboard(final Account.Id acct) {
- return "/dashboard/" + acct.toString();
+ public static String toAccountQuery(final String fullname) {
+ String query = op("owner", fullname) + " status:open";
+ return toChangeQuery(query, TOP);
}
public static String toChangeQuery(final String query) {
@@ -72,8 +67,7 @@
}
public static String toChangeQuery(String query, String page) {
- query = KeyUtil.encode(query).replaceAll("%3[Aa]", ":");
- return "/q/" + query + "," + page;
+ return "/q/" + KeyUtil.encode(query) + "," + page;
}
public static String projectQuery(Project.NameKey proj, Status status) {
@@ -91,11 +85,18 @@
}
}
- public static String op(String name, String value) {
- if (value.indexOf(' ') >= 0) {
- return name + ":\"" + value + "\"";
+ public static String op(String op, String value) {
+ if (isSingleWord(value)) {
+ return op + ":" + value;
}
- return name + ":" + value;
+ return op + ":\"" + value + "\"";
+ }
+
+ private static boolean isSingleWord(String value) {
+ if (value.startsWith("-")) {
+ return false;
+ }
+ return value.matches("[^\u0000-\u0020!\"#$%&'():;?\\[\\]{}~]+");
}
protected PageLinks() {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
index 21aca69..4de576a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
@@ -19,7 +19,6 @@
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.client.ContactInformation;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.RemoteJsonService;
import com.google.gwtjsonrpc.common.RpcImpl;
@@ -67,7 +66,7 @@
ContactInformation info, AsyncCallback<Account> callback);
@SignInRequired
- void enterAgreement(ContributorAgreement.Id id,
+ void enterAgreement(String agreementName,
AsyncCallback<VoidResult> callback);
@SignInRequired
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java
index 0c6f6b7..7464bd1 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java
@@ -14,30 +14,21 @@
package com.google.gerrit.common.data;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
-
import java.util.List;
import java.util.Map;
public class AgreementInfo {
- public List<AccountAgreement> userAccepted;
- public List<AccountGroupAgreement> groupAccepted;
- public Map<ContributorAgreement.Id, ContributorAgreement> agreements;
+ public List<String> accepted;
+ public Map<String, ContributorAgreement> agreements;
public AgreementInfo() {
}
- public void setUserAccepted(List<AccountAgreement> a) {
- userAccepted = a;
+ public void setAccepted(List<String> a) {
+ accepted = a;
}
- public void setGroupAccepted(List<AccountGroupAgreement> a) {
- groupAccepted = a;
- }
-
- public void setAgreements(Map<ContributorAgreement.Id, ContributorAgreement> a) {
+ public void setAgreements(Map<String, ContributorAgreement> a) {
agreements = a;
}
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ContributorAgreement.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ContributorAgreement.java
new file mode 100644
index 0000000..e02d9d3
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ContributorAgreement.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.data;
+
+import com.google.gerrit.reviewdb.client.Project;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Portion of a {@link Project} describing a single contributor agreement. */
+public class ContributorAgreement implements Comparable<ContributorAgreement> {
+ protected String name;
+ protected String description;
+ protected List<PermissionRule> accepted;
+ protected boolean requireContactInformation;
+ protected GroupReference autoVerify;
+ protected String agreementUrl;
+
+ protected ContributorAgreement() {
+ }
+
+ public ContributorAgreement(String name) {
+ setName(name);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public List<PermissionRule> getAccepted() {
+ if (accepted == null) {
+ accepted = new ArrayList<PermissionRule>();
+ }
+ return accepted;
+ }
+
+ public void setAccepted(List<PermissionRule> accepted) {
+ this.accepted = accepted;
+ }
+
+ public boolean isRequireContactInformation() {
+ return requireContactInformation;
+ }
+
+ public void setRequireContactInformation(boolean requireContactInformation) {
+ this.requireContactInformation = requireContactInformation;
+ }
+
+ public GroupReference getAutoVerify() {
+ return autoVerify;
+ }
+
+ public void setAutoVerify(GroupReference autoVerify) {
+ this.autoVerify = autoVerify;
+ }
+
+ public String getAgreementUrl() {
+ return agreementUrl;
+ }
+
+ public void setAgreementUrl(String agreementUrl) {
+ this.agreementUrl = agreementUrl;
+ }
+
+ @Override
+ public int compareTo(ContributorAgreement o) {
+ return getName().compareTo(o.getName());
+ }
+
+ @Override
+ public String toString() {
+ return "ContributorAgreement[" + getName() + "]";
+ }
+
+ public ContributorAgreement forUi() {
+ ContributorAgreement ca = new ContributorAgreement(name);
+ ca.description = description;
+ ca.accepted = Collections.emptyList();
+ ca.requireContactInformation = requireContactInformation;
+ if (autoVerify != null) {
+ ca.autoVerify = new GroupReference();
+ }
+ ca.agreementUrl = agreementUrl ;
+ return ca;
+ }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
index 456ffb4..07a8534 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
@@ -16,9 +16,10 @@
import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
import com.google.gwtexpui.safehtml.client.RegexFindReplace;
import java.util.List;
@@ -38,6 +39,7 @@
protected String gitDaemonUrl;
protected String gitHttpUrl;
protected String sshdAddress;
+ protected String editFullNameUrl;
protected Project.NameKey wildProject;
protected ApprovalTypes approvalTypes;
protected Set<Account.FieldName> editableAccountFields;
@@ -54,6 +56,14 @@
registerUrl = u;
}
+ public String getEditFullNameUrl() {
+ return editFullNameUrl;
+ }
+
+ public void setEditFullNameUrl(String u) {
+ editFullNameUrl = u;
+ }
+
public String getHttpPasswordUrl() {
return httpPasswordUrl;
}
@@ -199,4 +209,13 @@
public void setAnonymousCowardName(final String anonymousCowardName) {
this.anonymousCowardName = anonymousCowardName;
}
+
+ public boolean siteHasUsernames() {
+ if (getAuthType() == AuthType.CUSTOM_EXTENSION
+ && getHttpPasswordUrl() != null
+ && !canEdit(FieldName.USER_NAME)) {
+ return false;
+ }
+ return true;
+ }
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index d3d2a4d..64444b4 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -15,6 +15,8 @@
package com.google.gerrit.common.data;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
/** Server wide capabilities. Represented as {@link Permission} objects. */
@@ -73,23 +75,34 @@
/** Can view all pending tasks in the queue (not just the filtered set). */
public static final String VIEW_QUEUE = "viewQueue";
+ private static final List<String> NAMES_ALL;
private static final List<String> NAMES_LC;
static {
- NAMES_LC = new ArrayList<String>();
- NAMES_LC.add(ADMINISTRATE_SERVER.toLowerCase());
- NAMES_LC.add(CREATE_ACCOUNT.toLowerCase());
- NAMES_LC.add(CREATE_GROUP.toLowerCase());
- NAMES_LC.add(CREATE_PROJECT.toLowerCase());
- NAMES_LC.add(EMAIL_REVIEWERS.toLowerCase());
- NAMES_LC.add(FLUSH_CACHES.toLowerCase());
- NAMES_LC.add(KILL_TASK.toLowerCase());
- NAMES_LC.add(PRIORITY.toLowerCase());
- NAMES_LC.add(QUERY_LIMIT.toLowerCase());
- NAMES_LC.add(START_REPLICATION.toLowerCase());
- NAMES_LC.add(VIEW_CACHES.toLowerCase());
- NAMES_LC.add(VIEW_CONNECTIONS.toLowerCase());
- NAMES_LC.add(VIEW_QUEUE.toLowerCase());
+ NAMES_ALL = new ArrayList<String>();
+ NAMES_ALL.add(ADMINISTRATE_SERVER);
+ NAMES_ALL.add(CREATE_ACCOUNT);
+ NAMES_ALL.add(CREATE_GROUP);
+ NAMES_ALL.add(CREATE_PROJECT);
+ NAMES_ALL.add(EMAIL_REVIEWERS);
+ NAMES_ALL.add(FLUSH_CACHES);
+ NAMES_ALL.add(KILL_TASK);
+ NAMES_ALL.add(PRIORITY);
+ NAMES_ALL.add(QUERY_LIMIT);
+ NAMES_ALL.add(START_REPLICATION);
+ NAMES_ALL.add(VIEW_CACHES);
+ NAMES_ALL.add(VIEW_CONNECTIONS);
+ NAMES_ALL.add(VIEW_QUEUE);
+
+ NAMES_LC = new ArrayList<String>(NAMES_ALL.size());
+ for (String name : NAMES_ALL) {
+ NAMES_LC.add(name.toLowerCase());
+ }
+ }
+
+ /** @return all valid capability names. */
+ public static Collection<String> getAllNames() {
+ return Collections.unmodifiableList(NAMES_ALL);
}
/** @return true if the name is recognized as a capability name. */
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
index f385e27..ffa1e3e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
@@ -24,7 +24,6 @@
import com.google.gwtjsonrpc.common.VoidResult;
import com.google.gwtjsonrpc.common.RpcImpl.Version;
-import java.util.List;
import java.util.Set;
@RpcImpl(version = Version.V2_0)
@@ -60,14 +59,6 @@
AsyncCallback<VoidResult> callback);
@SignInRequired
- void changeExternalGroup(AccountGroup.Id groupId,
- AccountGroup.ExternalNameKey bindTo, AsyncCallback<VoidResult> callback);
-
- @SignInRequired
- void searchExternalGroups(String searchFilter,
- AsyncCallback<List<AccountGroup.ExternalNameKey>> callback);
-
- @SignInRequired
void addGroupMember(AccountGroup.Id groupId, String nameOrEmail,
AsyncCallback<GroupDetail> callback);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
new file mode 100644
index 0000000..828bf24
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.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.common.data;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+/**
+ * Group methods exposed by the GroupBackend.
+ */
+public class GroupDescription {
+ /**
+ * The Basic information required to be exposed by any Group.
+ */
+ public interface Basic {
+ /** @return the non-null UUID of the group. */
+ AccountGroup.UUID getGroupUUID();
+
+ /** @return the non-null name of the group. */
+ String getName();
+
+ /** @return whether the group is visible to all accounts. */
+ boolean isVisibleToAll();
+ }
+
+ /**
+ * The extended information exposed by internal groups backed by an
+ * AccountGroup.
+ */
+ public interface Internal extends Basic {
+ /** @return the backing AccountGroup. */
+ AccountGroup getAccountGroup();
+ }
+
+ private GroupDescription() {
+ }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
new file mode 100644
index 0000000..e0bc7d8
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.data;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility class for building GroupDescription objects.
+ */
+public class GroupDescriptions {
+
+ @Nullable
+ public static AccountGroup toAccountGroup(GroupDescription.Basic group) {
+ if (group instanceof GroupDescription.Internal) {
+ return ((GroupDescription.Internal) group).getAccountGroup();
+ }
+ return null;
+ }
+
+ public static GroupDescription.Internal forAccountGroup(final AccountGroup group) {
+ return new GroupDescription.Internal() {
+ @Override
+ public AccountGroup.UUID getGroupUUID() {
+ return group.getGroupUUID();
+ }
+
+ @Override
+ public String getName() {
+ return group.getName();
+ }
+
+ @Override
+ public boolean isVisibleToAll() {
+ return group.isVisibleToAll();
+ }
+
+ @Override
+ public AccountGroup getAccountGroup() {
+ return group;
+ }
+ };
+ }
+
+ private GroupDescriptions() {
+ }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
index 65723f7..01c7985 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
@@ -26,7 +26,7 @@
public AccountGroup group;
public List<AccountGroupMember> members;
public List<AccountGroupInclude> includes;
- public AccountGroup ownerGroup;
+ public GroupReference ownerGroup;
public boolean canModify;
public GroupDetail() {
@@ -52,7 +52,7 @@
includes = i;
}
- public void setOwnerGroup(AccountGroup g) {
+ public void setOwnerGroup(GroupReference g) {
ownerGroup = g;
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
index f05d1b9..c261fdd 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
@@ -23,6 +23,10 @@
return new GroupReference(group.getGroupUUID(), group.getName());
}
+ public static GroupReference forGroup(GroupDescription.Basic group) {
+ return new GroupReference(group.getGroupUUID(), group.getName());
+ }
+
protected String uuid;
protected String name;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
index 68676cf..2a70d6c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
@@ -15,6 +15,7 @@
package com.google.gerrit.common.data;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -62,28 +63,9 @@
raw.append(pattern.substring(i, b));
ops.add(new Constant(pattern.substring(i, b)));
- String expr = pattern.substring(b + 2, e);
- String parameterName = "";
- List<Function> functions = new ArrayList<Function>();
- if (!expr.contains(".")) {
- parameterName = expr;
- } else {
- int firstDot = expr.indexOf('.');
- parameterName = expr.substring(0, firstDot);
- String actionsStr = expr.substring(firstDot + 1);
- String[] actions = actionsStr.split("\\.");
+ // "${parameter[.functions...]}" -> "parameter[.functions...]"
+ final Parameter p = new Parameter(pattern.substring(b + 2, e));
- for (String action : actions) {
- Function function = FUNCTIONS.get(action);
- if (function == null) {
- function = NOOP;
- }
- functions.add(function);
- }
- }
-
- final Parameter p =
- new Parameter(parameterName, Collections.unmodifiableList(functions));
raw.append("{" + prs.size() + "}");
prs.add(p);
ops.add(p);
@@ -184,9 +166,25 @@
private final String name;
private final List<Function> functions;
- Parameter(final String name, final List<Function> functions) {
- this.name = name;
- this.functions = functions;
+ Parameter(final String parameter) {
+ // "parameter[.functions...]" -> (parameter, functions...)
+ final List<String> names = Arrays.asList(parameter.split("\\."));
+ final List<Function> functs = new ArrayList<Function>(names.size());
+
+ if (names.isEmpty()) {
+ name = "";
+ } else {
+ name = names.get(0);
+
+ for (String fname : names.subList(1, names.size())) {
+ final Function function = FUNCTIONS.get(fname);
+ if (function != null) {
+ functs.add(function);
+ }
+ }
+ }
+
+ functions = Collections.unmodifiableList(functs);
}
@Override
@@ -207,12 +205,6 @@
}
private static final Map<String, Function> FUNCTIONS = initFunctions();
- private static final Function NOOP = new Function() {
- @Override
- String apply(String a) {
- return a;
- }
- };
private static Map<String, Function> initFunctions() {
final HashMap<String, Function> m = new HashMap<String, Function>();
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-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
index 5049ba4..365f6a9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
@@ -68,6 +68,12 @@
NEED,
/**
+ * The label may be set, but it's neither necessary for submission
+ * nor does it block submission if set.
+ */
+ MAY,
+
+ /**
* The label is required for submission, but is impossible to complete.
* The likely cause is access has not been granted correctly by the
* project owner or site administrator.
@@ -78,5 +84,34 @@
public String label;
public Status status;
public Account.Id appliedBy;
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(label).append(": ").append(status);
+ if (appliedBy != null) {
+ sb.append(" by ").append(appliedBy);
+ }
+ return sb.toString();
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(status);
+ if (status == Status.RULE_ERROR && errorMessage != null) {
+ sb.append('(').append(errorMessage).append(')');
+ }
+ sb.append('[');
+ if (labels != null) {
+ String delimiter = "";
+ for (Label label : labels) {
+ sb.append(delimiter).append(label);
+ delimiter = ", ";
+ }
+ }
+ sb.append(']');
+ return sb.toString();
}
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
index 85518b2..7205b74 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
@@ -26,15 +26,19 @@
@RpcImpl(version = Version.V2_0)
public interface SuggestService extends RemoteJsonService {
- void suggestProjectNameKey(String query, int limit,
- AsyncCallback<List<Project.NameKey>> callback);
-
void suggestAccount(String query, Boolean enabled, int limit,
AsyncCallback<List<AccountInfo>> callback);
+ /**
+ * @see #suggestAccountGroup(com.google.gerrit.reviewdb.client.Project.NameKey, String, int, AsyncCallback)
+ */
+ @Deprecated
void suggestAccountGroup(String query, int limit,
AsyncCallback<List<GroupReference>> callback);
+ void suggestAccountGroupForProject(Project.NameKey project, String query,
+ int limit, AsyncCallback<List<GroupReference>> callback);
+
/**
* @see #suggestChangeReviewer(com.google.gerrit.reviewdb.client.Change.Id, String, int, AsyncCallback)
*/
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
index 78ccca1..4a45350 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
@@ -15,7 +15,6 @@
package com.google.gerrit.common.data;
import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
import com.google.gwtjsonrpc.common.RemoteJsonService;
diff --git a/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs b/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs
deleted file mode 100644
index 82eb859..0000000
--- a/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs
+++ /dev/null
@@ -1,3 +0,0 @@
-#Tue Sep 02 16:59:24 PDT 2008
-eclipse.preferences.version=1
-encoding/<project>=UTF-8
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
deleted file mode 100644
index c25c381..0000000
--- a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
+++ /dev/null
@@ -1,271 +0,0 @@
-// Copyright (C) 2009 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.ehcache;
-
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import com.google.gerrit.lifecycle.LifecycleListener;
-import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.CachePool;
-import com.google.gerrit.server.cache.CacheProvider;
-import com.google.gerrit.server.cache.EntryCreator;
-import com.google.gerrit.server.cache.EvictionPolicy;
-import com.google.gerrit.server.cache.ProxyCache;
-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.Singleton;
-
-import net.sf.ehcache.CacheManager;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.config.CacheConfiguration;
-import net.sf.ehcache.config.Configuration;
-import net.sf.ehcache.config.DiskStoreConfiguration;
-import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
-
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Pool of all declared caches created by {@link CacheModule}s. */
-@Singleton
-public class EhcachePoolImpl implements CachePool {
- private static final Logger log =
- LoggerFactory.getLogger(EhcachePoolImpl.class);
-
- public static class Module extends LifecycleModule {
- @Override
- protected void configure() {
- bind(CachePool.class).to(EhcachePoolImpl.class);
- bind(EhcachePoolImpl.class);
- listener().to(EhcachePoolImpl.Lifecycle.class);
- }
- }
-
- public static class Lifecycle implements LifecycleListener {
- private final EhcachePoolImpl cachePool;
-
- @Inject
- Lifecycle(final EhcachePoolImpl cachePool) {
- this.cachePool = cachePool;
- }
-
- @Override
- public void start() {
- cachePool.start();
- }
-
- @Override
- public void stop() {
- cachePool.stop();
- }
- }
-
- private final Config config;
- private final SitePaths site;
-
- private final Object lock = new Object();
- private final Map<String, CacheProvider<?, ?>> caches;
- private CacheManager manager;
-
- @Inject
- EhcachePoolImpl(@GerritServerConfig final Config cfg, final SitePaths site) {
- this.config = cfg;
- this.site = site;
- this.caches = new HashMap<String, CacheProvider<?, ?>>();
- }
-
- private void start() {
- synchronized (lock) {
- if (manager != null) {
- throw new IllegalStateException("Cache pool has already been started");
- }
-
- try {
- System.setProperty("net.sf.ehcache.skipUpdateCheck", "" + true);
- } catch (SecurityException e) {
- // Ignore it, the system is just going to ping some external page
- // using a background thread and there's not much we can do about
- // it now.
- }
-
- manager = new CacheManager(new Factory().toConfiguration());
- for (CacheProvider<?, ?> p : caches.values()) {
- Ehcache eh = manager.getEhcache(p.getName());
- EntryCreator<?, ?> c = p.getEntryCreator();
- if (c != null) {
- p.bind(new PopulatingCache(eh, c));
- } else {
- p.bind(new SimpleCache(eh));
- }
- }
- }
- }
-
- private void stop() {
- synchronized (lock) {
- if (manager != null) {
- manager.shutdown();
- }
- }
- }
-
- /** <i>Discouraged</i> Get the underlying cache descriptions, for statistics. */
- public CacheManager getCacheManager() {
- synchronized (lock) {
- return manager;
- }
- }
-
- public <K, V> ProxyCache<K, V> register(final CacheProvider<K, V> provider) {
- synchronized (lock) {
- if (manager != null) {
- throw new IllegalStateException("Cache pool has already been started");
- }
-
- final String n = provider.getName();
- if (caches.containsKey(n) && caches.get(n) != provider) {
- throw new IllegalStateException("Cache \"" + n + "\" already defined");
- }
- caches.put(n, provider);
- return new ProxyCache<K, V>();
- }
- }
-
- private class Factory {
- private static final int MB = 1024 * 1024;
- private final Configuration mgr = new Configuration();
-
- Configuration toConfiguration() {
- configureDiskStore();
- configureDefaultCache();
-
- for (CacheProvider<?, ?> p : caches.values()) {
- final String name = p.getName();
- final CacheConfiguration c = newCache(name);
- c.setMemoryStoreEvictionPolicyFromObject(toPolicy(p.evictionPolicy()));
-
- c.setMaxElementsInMemory(getInt(name, "memorylimit", p.memoryLimit()));
-
- c.setTimeToIdleSeconds(0);
- c.setTimeToLiveSeconds(getSeconds(name, "maxage", p.maxAge()));
- c.setEternal(c.getTimeToLiveSeconds() == 0);
-
- if (p.disk() && mgr.getDiskStoreConfiguration() != null) {
- c.setMaxElementsOnDisk(getInt(name, "disklimit", p.diskLimit()));
-
- int v = c.getDiskSpoolBufferSizeMB() * MB;
- v = getInt(name, "diskbuffer", v) / MB;
- c.setDiskSpoolBufferSizeMB(Math.max(1, v));
- c.setOverflowToDisk(c.getMaxElementsOnDisk() > 0);
- c.setDiskPersistent(c.getMaxElementsOnDisk() > 0);
- }
-
- mgr.addCache(c);
- }
-
- return mgr;
- }
-
- private MemoryStoreEvictionPolicy toPolicy(final EvictionPolicy policy) {
- switch (policy) {
- case LFU:
- return MemoryStoreEvictionPolicy.LFU;
-
- case LRU:
- return MemoryStoreEvictionPolicy.LRU;
-
- default:
- throw new IllegalArgumentException("Unsupported " + policy);
- }
- }
-
- private int getInt(String n, String s, int d) {
- return config.getInt("cache", n, s, d);
- }
-
- private long getSeconds(String n, String s, long d) {
- d = MINUTES.convert(d, SECONDS);
- long m = ConfigUtil.getTimeUnit(config, "cache", n, s, d, MINUTES);
- return SECONDS.convert(m, MINUTES);
- }
-
- private void configureDiskStore() {
- boolean needDisk = false;
- for (CacheProvider<?, ?> p : caches.values()) {
- if (p.disk()) {
- needDisk = true;
- break;
- }
- }
- if (!needDisk) {
- return;
- }
-
- File loc = site.resolve(config.getString("cache", null, "directory"));
- if (loc == null) {
- } else if (loc.exists() || loc.mkdirs()) {
- if (loc.canWrite()) {
- final DiskStoreConfiguration c = new DiskStoreConfiguration();
- c.setPath(loc.getAbsolutePath());
- mgr.addDiskStore(c);
- log.info("Enabling disk cache " + loc.getAbsolutePath());
- } else {
- log.warn("Can't write to disk cache: " + loc.getAbsolutePath());
- }
- } else {
- log.warn("Can't create disk cache: " + loc.getAbsolutePath());
- }
- }
-
- private CacheConfiguration newConfiguration() {
- CacheConfiguration c = new CacheConfiguration();
-
- c.setMaxElementsInMemory(1024);
- c.setMemoryStoreEvictionPolicyFromObject(MemoryStoreEvictionPolicy.LFU);
-
- c.setTimeToIdleSeconds(0);
- c.setTimeToLiveSeconds(0 /* infinite */);
- c.setEternal(true);
-
- if (mgr.getDiskStoreConfiguration() != null) {
- c.setMaxElementsOnDisk(16384);
- c.setOverflowToDisk(false);
- c.setDiskPersistent(false);
-
- c.setDiskSpoolBufferSizeMB(5);
- c.setDiskExpiryThreadIntervalSeconds(60 * 60);
- }
- return c;
- }
-
- private void configureDefaultCache() {
- mgr.setDefaultCacheConfiguration(newConfiguration());
- }
-
- private CacheConfiguration newCache(final String name) {
- CacheConfiguration c = newConfiguration();
- c.setName(name);
- return c;
- }
- }
-}
diff --git a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java
deleted file mode 100644
index f5c6c45..0000000
--- a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright (C) 2008 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.ehcache;
-
-import com.google.gerrit.server.cache.Cache;
-import com.google.gerrit.server.cache.EntryCreator;
-
-import net.sf.ehcache.CacheException;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Element;
-import net.sf.ehcache.constructs.blocking.CacheEntryFactory;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A decorator for {@link Cache} which automatically constructs missing entries.
- * <p>
- * On a cache miss {@link EntryCreator#createEntry(Object)} is invoked, allowing
- * the application specific subclass to compute the entry and return it for
- * caching. During a miss the cache takes a lock related to the missing key,
- * ensuring that at most one thread performs the creation work, and other
- * threads wait for the result. Concurrent creations are possible if two
- * different keys miss and hash to different locks in the internal lock table.
- *
- * @param <K> type of key used to name cache entries.
- * @param <V> type of value stored within a cache entry.
- */
-class PopulatingCache<K, V> implements Cache<K, V> {
- private static final Logger log =
- LoggerFactory.getLogger(PopulatingCache.class);
-
- private final net.sf.ehcache.constructs.blocking.SelfPopulatingCache self;
- private final EntryCreator<K, V> creator;
-
- PopulatingCache(Ehcache s, EntryCreator<K, V> entryCreator) {
- creator = entryCreator;
- final CacheEntryFactory f = new CacheEntryFactory() {
- @SuppressWarnings("unchecked")
- @Override
- public Object createEntry(Object key) throws Exception {
- return creator.createEntry((K) key);
- }
- };
- self = new net.sf.ehcache.constructs.blocking.SelfPopulatingCache(s, f);
- }
-
- /**
- * Get the element from the cache, or {@link EntryCreator#missing(Object)} if not found.
- * <p>
- * The {@link EntryCreator#missing(Object)} method is only invoked if:
- * <ul>
- * <li>{@code key == null}, in which case the application should return a
- * suitable return value that callers can accept, or throw a RuntimeException.
- * <li>{@code createEntry(key)} threw an exception, in which case the entry
- * was not stored in the cache. An entry was recorded in the application log,
- * but a return value is still required.
- * <li>The cache has been shutdown, and access is forbidden.
- * </ul>
- *
- * @param key key to locate.
- * @return either the cached entry, or {@code missing(key)} if not found.
- */
- @SuppressWarnings("unchecked")
- public V get(final K key) {
- if (key == null) {
- return creator.missing(key);
- }
-
- final Element m;
- try {
- m = self.get(key);
- } catch (IllegalStateException err) {
- log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
- return creator.missing(key);
- } catch (CacheException err) {
- log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
- return creator.missing(key);
- }
- return m != null ? (V) m.getObjectValue() : creator.missing(key);
- }
-
- public void remove(final K key) {
- if (key != null) {
- self.remove(key);
- }
- }
-
- /** Remove all cached items, forcing them to be created again on demand. */
- public void removeAll() {
- self.removeAll();
- }
-
- public void put(K key, V value) {
- self.put(new Element(key, value));
- }
-
- @Override
- public String toString() {
- return "Cache[" + self.getName() + "]";
- }
-}
diff --git a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java
deleted file mode 100644
index e4428e3..0000000
--- a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (C) 2009 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.ehcache;
-
-import com.google.gerrit.server.cache.Cache;
-
-import net.sf.ehcache.CacheException;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Element;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * A fast in-memory and/or on-disk based cache.
- *
- * @type <K> type of key used to lookup entries in the cache.
- * @type <V> type of value stored within each cache entry.
- */
-final class SimpleCache<K, V> implements Cache<K, V> {
- private static final Logger log = LoggerFactory.getLogger(SimpleCache.class);
-
- private final Ehcache self;
-
- SimpleCache(final Ehcache self) {
- this.self = self;
- }
-
- Ehcache getEhcache() {
- return self;
- }
-
- @SuppressWarnings("unchecked")
- public V get(final K key) {
- if (key == null) {
- return null;
- }
- final Element m;
- try {
- m = self.get(key);
- } catch (IllegalStateException err) {
- log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
- return null;
- } catch (CacheException err) {
- log.error("Cannot lookup " + key + " in \"" + self.getName() + "\"", err);
- return null;
- }
- return m != null ? (V) m.getObjectValue() : null;
- }
-
- public void put(final K key, final V value) {
- self.put(new Element(key, value));
- }
-
- public void remove(final K key) {
- if (key != null) {
- self.remove(key);
- }
- }
-
- public void removeAll() {
- self.removeAll();
- }
-
- @Override
- public String toString() {
- return "Cache[" + self.getName() + "]";
- }
-}
diff --git a/gerrit-ehcache/.gitignore b/gerrit-extension-api/.gitignore
similarity index 80%
copy from gerrit-ehcache/.gitignore
copy to gerrit-extension-api/.gitignore
index 20251d4..4e1ec9c 100644
--- a/gerrit-ehcache/.gitignore
+++ b/gerrit-extension-api/.gitignore
@@ -1,5 +1,6 @@
/target
/.classpath
/.project
-/.settings/org.eclipse.m2e.core.prefs
/.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-ehcache/.settings/org.eclipse.core.runtime.prefs b/gerrit-extension-api/.settings/org.eclipse.core.runtime.prefs
similarity index 100%
copy from gerrit-ehcache/.settings/org.eclipse.core.runtime.prefs
copy to gerrit-extension-api/.settings/org.eclipse.core.runtime.prefs
diff --git a/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs b/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs
similarity index 99%
copy from gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
copy to gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs
index e89c048..470942d 100644
--- a/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Thu Jan 19 12:55:44 PST 2012
+#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
diff --git a/gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs b/gerrit-extension-api/.settings/org.eclipse.jdt.ui.prefs
similarity index 100%
copy from gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs
copy to gerrit-extension-api/.settings/org.eclipse.jdt.ui.prefs
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
new file mode 100644
index 0000000..ff672d5
--- /dev/null
+++ b/gerrit-extension-api/pom.xml
@@ -0,0 +1,73 @@
+<?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>
+ <shadedArtifactAttached>true</shadedArtifactAttached>
+ <shadedClassifierName>all</shadedClassifierName>
+ </configuration>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java
new file mode 100644
index 0000000..4811e407
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation applied to auto-registered, exported types.
+ * <p>
+ * Plugins or extensions using auto-registration should apply this annotation to
+ * any non-abstract class they want exported for access.
+ * <p>
+ * For SSH commands the @Export annotation names the subcommand:
+ *
+ * <pre>
+ * @Export("print")
+ * class MyCommand extends SshCommand {
+ * </pre>
+ *
+ * For HTTP servlets, the @Export annotation names the URL the servlet is bound
+ * to, relative to the plugin or extension's namespace within the Gerrit
+ * container.
+ *
+ * <pre>
+ * @Export("/index.html")
+ * class ShowIndexHtml extends HttpServlet {
+ * </pre>
+ */
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface Export {
+ String value();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java
new file mode 100644
index 0000000..a3e72bc
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+import java.io.Serializable;
+import java.lang.annotation.Annotation;
+
+final class ExportImpl implements Export, Serializable {
+ private static final long serialVersionUID = 0;
+ private final String value;
+
+ ExportImpl(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public Class<? extends Annotation> annotationType() {
+ return Export.class;
+ }
+
+ @Override
+ public String value() {
+ return value;
+ }
+
+ @Override
+ public int hashCode() {
+ return (127 * "value".hashCode()) ^ value.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof Export && value.equals(((Export) o).value());
+ }
+
+ @Override
+ public String toString() {
+ return "@" + Export.class.getName() + "(value=" + value + ")";
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
similarity index 60%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
index 43039e1..c48bcfb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.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.
@@ -12,11 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+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);
+ }
-/** Configure a cache declared within a {@link CacheModule} instance. */
-public interface UnnamedCacheBinding<K, V> {
- /** Set the name of the cache. */
- public NamedCacheBinding<K, V> name(String cacheName);
+ 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/PluginData.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java
new file mode 100644
index 0000000..bf2b09a
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.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;
+
+/**
+ * Local path where a plugin can store its own private data.
+ * <p>
+ * A plugin or extension may receive this string by Guice injection to discover
+ * a directory where it can store configuration or other data that is private:
+ *
+ * <pre>
+ * @Inject
+ * MyType(@PluginData java.io.File myDir) {
+ * new FileInputStream(new File(myDir, "my.config"));
+ * }
+ * </pre>
+ */
+@Target({ElementType.PARAMETER, ElementType.FIELD})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface PluginData {
+}
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/events/GitReferenceUpdatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
new file mode 100644
index 0000000..438500d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
@@ -0,0 +1,34 @@
+// 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.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.util.List;
+
+/** Notified when one or more references are modified. */
+@ExtensionPoint
+public interface GitReferenceUpdatedListener {
+ public interface Update {
+ String getRefName();
+ }
+
+ public interface Event {
+ String getProjectName();
+ List<Update> getUpdates();
+ }
+
+ void onGitReferenceUpdated(Event event);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
similarity index 87%
rename from gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleListener.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
index e6b06ef..93da347 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
@@ -12,11 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.lifecycle;
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
import java.util.EventListener;
/** Listener interested in server startup and shutdown events. */
+@ExtensionPoint
public interface LifecycleListener extends EventListener {
/** Invoke when the server is starting. */
public void start();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java
new file mode 100644
index 0000000..7eed7d4
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+
+/** Notified whenever a project is created on the master. */
+@ExtensionPoint
+public interface NewProjectCreatedListener {
+ public interface Event {
+ String getProjectName();
+ String getHeadName();
+ }
+
+ void onNewProjectCreated(Event event);
+}
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..40bbb80
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -0,0 +1,163 @@
+// 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.Provider;
+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. Maps store Providers
+ * internally, and resolve the provider to an instance on demand. This enables
+ * registrations to decide between singleton and non-singleton members.
+ */
+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, Provider<T>> items;
+
+ DynamicMap() {
+ items = new ConcurrentHashMap<NamePair, Provider<T>>(
+ 16 /* initial size */,
+ 0.75f /* load factor */,
+ 1 /* concurrency level of 1, load/unload is single threaded */);
+ }
+
+ /**
+ * 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.
+ * @throws ProvisionException if the registered provider is unable to obtain
+ * an instance of the requested implementation.
+ */
+ public T get(String pluginName, String exportName) {
+ Provider<T> p = items.get(new NamePair(pluginName, exportName));
+ return p != null ? p.get() : null;
+ }
+
+ /**
+ * 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, Provider<T>> byPlugin(String pluginName) {
+ SortedMap<String, Provider<T>> r = new TreeMap<String, Provider<T>>();
+ for (Map.Entry<NamePair, Provider<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..c6e4701
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.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.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) {
+ if (b.getKey().getAnnotation() != null) {
+ m.put("gerrit", b.getKey(), b.getProvider());
+ }
+ }
+ }
+ 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..ec34887
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -0,0 +1,253 @@
+// 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.Provider;
+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.Providers;
+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. Sets store Providers
+ * internally, and resolve the provider to an instance on demand. This enables
+ * registrations to decide between singleton and non-singleton members.
+ */
+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<Provider<T>>> items;
+
+ DynamicSet(Collection<AtomicReference<Provider<T>>> base) {
+ items = new CopyOnWriteArrayList<AtomicReference<Provider<T>>>(base);
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ final Iterator<AtomicReference<Provider<T>>> itr = items.iterator();
+ return new Iterator<T>() {
+ private T next;
+
+ @Override
+ public boolean hasNext() {
+ while (next == null && itr.hasNext()) {
+ Provider<T> p = itr.next().get();
+ if (p != null) {
+ try {
+ next = p.get();
+ } catch (RuntimeException e) {
+ // TODO Log failed member of DynamicSet.
+ }
+ }
+ }
+ 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) {
+ return add(Providers.of(item));
+ }
+
+ /**
+ * 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 Provider<T> item) {
+ final AtomicReference<Provider<T>> ref =
+ new AtomicReference<Provider<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, Provider<T> item) {
+ AtomicReference<Provider<T>> ref = new AtomicReference<Provider<T>>(item);
+ items.add(ref);
+ return new ReloadableHandle(ref, key, item);
+ }
+
+ private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
+ private final AtomicReference<Provider<T>> ref;
+ private final Key<T> key;
+ private final Provider<T> item;
+
+ ReloadableHandle(AtomicReference<Provider<T>> ref,
+ Key<T> key,
+ Provider<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, Provider<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..6c21553
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
@@ -0,0 +1,65 @@
+// 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 com.google.inject.internal.UniqueAnnotations;
+
+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 static final Class<?> UNIQUE_ANNOTATION =
+ UniqueAnnotations.create().getClass();
+ 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<Provider<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<Provider<T>>> r = newList(cnt);
+ for (Binding<T> b : bindings) {
+ if (b.getKey().getAnnotation() != null) {
+ r.add(new AtomicReference<Provider<T>>(b.getProvider()));
+ }
+ }
+ return r;
+ }
+
+ private static <T> List<AtomicReference<Provider<T>>> newList(int cnt) {
+ return new ArrayList<AtomicReference<Provider<T>>>(cnt);
+ }
+}
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..3558794
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
@@ -0,0 +1,97 @@
+// 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;
+import com.google.inject.Provider;
+
+/** <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 Provider<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,
+ Provider<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 Provider<T> item;
+
+ ReloadableHandle(NamePair np, Key<T> key, Provider<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, Provider<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/PrivateInternals_DynamicTypes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
new file mode 100644
index 0000000..66dd45d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
@@ -0,0 +1,175 @@
+// 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.events.LifecycleListener;
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+
+import java.lang.reflect.ParameterizedType;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** <b>DO NOT USE</b> */
+public class PrivateInternals_DynamicTypes {
+ public static Map<TypeLiteral<?>, DynamicSet<?>> dynamicSetsOf(Injector src) {
+ Map<TypeLiteral<?>, DynamicSet<?>> m = 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());
+ }
+ }
+ if (m.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ return Collections.unmodifiableMap(m);
+ }
+
+ public static Map<TypeLiteral<?>, DynamicMap<?>> dynamicMapsOf(Injector src) {
+ Map<TypeLiteral<?>, DynamicMap<?>> m = 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());
+ }
+ }
+ if (m.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ return Collections.unmodifiableMap(m);
+ }
+
+ public static List<RegistrationHandle> attachSets(
+ Injector src,
+ Map<TypeLiteral<?>, DynamicSet<?>> sets) {
+ if (src == null || sets == null || sets.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ List<RegistrationHandle> handles = new ArrayList<RegistrationHandle>(4);
+ try {
+ 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)) {
+ if (b.getKey().getAnnotation() != null) {
+ handles.add(set.add(b.getKey(), b.getProvider()));
+ }
+ }
+ }
+ } catch (RuntimeException e) {
+ remove(handles);
+ throw e;
+ } catch (Error e) {
+ remove(handles);
+ throw e;
+ }
+ return handles;
+ }
+
+ public static List<RegistrationHandle> attachMaps(
+ Injector src,
+ String groupName,
+ Map<TypeLiteral<?>, DynamicMap<?>> maps) {
+ if (src == null || maps == null || maps.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ List<RegistrationHandle> handles = new ArrayList<RegistrationHandle>(4);
+ try {
+ 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)) {
+ if (b.getKey().getAnnotation() != null) {
+ handles.add(set.put(groupName, b.getKey(), b.getProvider()));
+ }
+ }
+ }
+ } catch (RuntimeException e) {
+ remove(handles);
+ throw e;
+ } catch (Error e) {
+ remove(handles);
+ throw e;
+ }
+ return handles;
+ }
+
+ public static LifecycleListener registerInParentInjectors() {
+ return new LifecycleListener() {
+ private List<RegistrationHandle> handles;
+
+ @Inject
+ private Injector self;
+
+ @Override
+ public void start() {
+ handles = new ArrayList<RegistrationHandle>(4);
+ Injector parent = self.getParent();
+ while (parent != null) {
+ handles.addAll(attachSets(self, dynamicSetsOf(parent)));
+ handles.addAll(attachMaps(self, "gerrit", dynamicMapsOf(parent)));
+ parent = parent.getParent();
+ }
+ if (handles.isEmpty()) {
+ handles = null;
+ }
+ }
+
+ @Override
+ public void stop() {
+ remove(handles);
+ handles = null;
+ }
+ };
+ }
+
+ private static void remove(List<RegistrationHandle> handles) {
+ if (handles != null) {
+ for (RegistrationHandle handle : handles) {
+ handle.remove();
+ }
+ }
+ }
+
+ private static <K,V> Map<K, V> newHashMap() {
+ return new HashMap<K,V>();
+ }
+
+ private static <T> List<Binding<T>> bindings(Injector src, TypeLiteral<T> type) {
+ return src.findBindingsByType(type);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
similarity index 69%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
index 3370b08..2243786 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 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.
@@ -12,8 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.extensions.registration;
-public interface CachePool {
- public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+/** Handle for registered information. */
+public interface RegistrationHandle {
+ /** Delete this registration. */
+ public void remove();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
similarity index 61%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
index 43039e1..7284296 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.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.
@@ -12,11 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.extensions.registration;
+import com.google.inject.Key;
+import com.google.inject.Provider;
-/** Configure a cache declared within a {@link CacheModule} instance. */
-public interface UnnamedCacheBinding<K, V> {
- /** Set the name of the cache. */
- public NamedCacheBinding<K, V> name(String cacheName);
+public interface ReloadableRegistrationHandle<T> extends RegistrationHandle {
+ public Key<T> getKey();
+
+ public RegistrationHandle replace(Key<T> key, Provider<T> item);
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java
new file mode 100644
index 0000000..3d2df21
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.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.systemstatus;
+
+/** Exports current server information to an extension. */
+public interface ServerInformation {
+ /** Current state of the server. */
+ public enum State {
+ /**
+ * The server is starting up, and network connections are not yet being
+ * accepted. Plugins or extensions starting during this time are starting
+ * for the first time in this process.
+ */
+ STARTUP,
+
+ /**
+ * The server is running and handling requests. Plugins starting during this
+ * state may be reloading, or being installed into a running system.
+ */
+ RUNNING,
+
+ /**
+ * The server is attempting a graceful halt of operations and will exit (or
+ * be killed by the operating system) soon.
+ */
+ SHUTDOWN;
+ }
+
+ State getState();
+}
diff --git a/gerrit-gwtdebug/.gitignore b/gerrit-gwtdebug/.gitignore
index 194bedc..4207862 100644
--- a/gerrit-gwtdebug/.gitignore
+++ b/gerrit-gwtdebug/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-gwtdebug.iml
\ No newline at end of file
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
index 734f645..01b93a6 100644
--- a/gerrit-gwtdebug/pom.xml
+++ b/gerrit-gwtdebug/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-gwtdebug</artifactId>
diff --git a/gerrit-gwtui/.gitignore b/gerrit-gwtui/.gitignore
index 194bedc..53d46b3 100644
--- a/gerrit-gwtui/.gitignore
+++ b/gerrit-gwtui/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-gwtui.iml
\ No newline at end of file
diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml
index f14daf6..b3291d1 100644
--- a/gerrit-gwtui/pom.xml
+++ b/gerrit-gwtui/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-gwtui</artifactId>
@@ -168,12 +168,33 @@
</properties>
</profile>
<profile>
+ <id>chrome</id>
+ <properties>
+ <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIsafari</GerritGwtUI.browserType>
+ <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
+ </properties>
+ </profile>
+ <profile>
+ <id>webkit</id>
+ <properties>
+ <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIsafari</GerritGwtUI.browserType>
+ <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
+ </properties>
+ </profile>
+ <profile>
<id>gecko1_8</id>
<properties>
<GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIgecko1_8</GerritGwtUI.browserType>
<GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
</properties>
</profile>
+ <profile>
+ <id>firefox</id>
+ <properties>
+ <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIgecko1_8</GerritGwtUI.browserType>
+ <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
+ </properties>
+ </profile>
</profiles>
<build>
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 3aee0e2..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
@@ -58,6 +58,7 @@
import com.google.gerrit.client.auth.userpass.UserPassSignInDialog;
import com.google.gerrit.client.changes.AccountDashboardScreen;
import com.google.gerrit.client.changes.ChangeScreen;
+import com.google.gerrit.client.changes.CustomDashboardScreen;
import com.google.gerrit.client.changes.PatchTable;
import com.google.gerrit.client.changes.PublishCommentScreen;
import com.google.gerrit.client.changes.QueryScreen;
@@ -234,6 +235,10 @@
}
if (matchExact("mine,drafts", token)) {
+ return PageLinks.toChangeQuery("is:draft");
+ }
+
+ if (matchExact("mine,comments", token)) {
return PageLinks.toChangeQuery("has:draft");
}
@@ -361,8 +366,18 @@
}
private static void dashboard(final String token) {
- Gerrit.display(token, //
- new AccountDashboardScreen(Account.Id.parse(skip(token))));
+ String rest = skip(token);
+ if (rest.matches("[0-9]+")) {
+ Gerrit.display(token, new AccountDashboardScreen(Account.Id.parse(rest)));
+ return;
+ }
+
+ if (rest.startsWith("?")) {
+ Gerrit.display(token, new CustomDashboardScreen(rest.substring(1)));
+ return;
+ }
+
+ Gerrit.display(token, new NotFoundScreen());
}
private static void change(final String token) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
index 74a2678..13bba12 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
@@ -25,6 +25,7 @@
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.user.client.PluginSafePopupPanel;
import com.google.gwtjsonrpc.client.RemoteJsonException;
@@ -94,6 +95,11 @@
body.add(message.toBlockWidget());
}
+ public ErrorDialog(final Widget w) {
+ this();
+ body.add(w);
+ }
+
/** Create a dialog box to nicely format an exception. */
public ErrorDialog(final Throwable what) {
this();
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..701e1fc 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
@@ -266,7 +266,7 @@
}
}
- private static String loginRedirect(String token) {
+ public static String loginRedirect(String token) {
if (token == null) {
token = "";
} else if (token.startsWith("/")) {
@@ -323,6 +323,7 @@
Cookies.removeCookie("GerritAccount");
}
+ @Override
public void onModuleLoad() {
UserAgent.assertNotInIFrame();
@@ -332,6 +333,7 @@
e = URL.encodeQueryString(e);
e = fixPathImpl(e);
e = fixColonImpl(e);
+ e = fixDoubleQuote(e);
return e;
}
@@ -345,6 +347,9 @@
private native String fixColonImpl(String path)
/*-{ return path.replace(/%3A/g, ":"); }-*/;
+
+ private native String fixDoubleQuote(String path)
+ /*-{ return path.replace(/%22/g, '"'); }-*/;
});
initHostname();
@@ -548,9 +553,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/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index a8315d8..574f58e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -104,6 +104,7 @@
String errorDialogTitle();
String errorDialogButtons();
String errorDialogErrorType();
+ String errorDialogText();
String fileColumnHeader();
String fileLine();
String fileLineCONTEXT();
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/RpcStatus.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
index 76ce384..955c8e2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
@@ -56,6 +56,10 @@
@Override
public void onRpcStart(final RpcStartEvent event) {
+ onRpcStart();
+ }
+
+ public void onRpcStart() {
if (++activeCalls == 1) {
if (hideDepth == 0) {
loading.setVisible(true);
@@ -65,6 +69,10 @@
@Override
public void onRpcComplete(final RpcCompleteEvent event) {
+ onRpcComplete();
+ }
+
+ public void onRpcComplete() {
if (--activeCalls == 0) {
loading.setVisible(false);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java
new file mode 100644
index 0000000..0565d3e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java
@@ -0,0 +1,36 @@
+// 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.client.account;
+
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+
+/** Capabilities the caller has from {@code /accounts/self/capabilities}. */
+public class AccountCapabilities extends JavaScriptObject {
+ public static void all(AsyncCallback<AccountCapabilities> cb, String... filter) {
+ RestApi api = new RestApi("/accounts/self/capabilities");
+ for (String name : filter) {
+ api.addParameter("q", name);
+ }
+ api.send(cb);
+ }
+
+ protected AccountCapabilities() {
+ }
+
+ public final native boolean canPerform(String name)
+ /*-{ return this[name] ? true : false; }-*/;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index c886216..d374d35 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -30,8 +30,8 @@
String showSiteHeader();
String useFlashClipboard();
String copySelfOnEmails();
- String displayPatchSetsInReverseOrder();
- String displayPersonNameInReviewCategory();
+ String reversePatchSetOrder();
+ String showUsernameInReviewCategory();
String buttonSaveChanges();
String tabAccountSummary();
@@ -57,6 +57,8 @@
String buttonClearPassword();
String buttonGeneratePassword();
String linkObtainPassword();
+ String linkEditFullName();
+ String linkReloadContact();
String invalidUserName();
String invalidUserEmail();
@@ -113,10 +115,7 @@
String agreementStatus();
String agreementName();
String agreementDescription();
- String agreementAccepted();
String agreementStatus_EXPIRED();
- String agreementStatus_NEW();
- String agreementStatus_REJECTED();
String agreementStatus_VERIFIED();
String newAgreementSelectTypeHeading();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index 499f051..8b32174 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -7,8 +7,8 @@
showSiteHeader = Show Site Header
useFlashClipboard = Use Flash Clipboard Widget
copySelfOnEmails = CC Me On Comments I Write
-displayPatchSetsInReverseOrder = Display Patch Sets In Reverse Order
-displayPersonNameInReviewCategory = Display Person Name In Review Category
+reversePatchSetOrder = Display Patch Sets In Reverse Order
+showUsernameInReviewCategory = Display Person Name In Review Category
defaultContextFieldLabel = Default Context:
maximumPageSizeFieldLabel = Maximum Page Size:
dateFormatLabel = Date/Time Format:
@@ -38,6 +38,8 @@
buttonClearPassword = Clear Password
buttonGeneratePassword = Generate Password
linkObtainPassword = Obtain Password
+linkEditFullName = Edit
+linkReloadContact = Reload
invalidUserName = Username must contain only letters, numbers, _, - or .
invalidUserEmail = Email format is wrong.
sshKeyInvalid = Invalid Key
@@ -103,11 +105,8 @@
agreementStatus = Status
agreementName = Name
agreementStatus_EXPIRED = Expired
-agreementStatus_NEW = Pending
-agreementStatus_REJECTED = Rejected
agreementStatus_VERIFIED = Verified
agreementDescription = Description
-agreementAccepted = Accepted
newAgreementSelectTypeHeading = Select an agreement type:
newAgreementNoneAvailable = No contributor agreements are configured.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index 4e0b3b2..4fbe7a0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -18,17 +18,18 @@
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.OnEditEnabler;
+import com.google.gerrit.common.PageLinks;
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.AuthType;
import com.google.gerrit.reviewdb.client.ContactInformation;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.i18n.client.LocaleInfo;
-import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
@@ -41,6 +42,7 @@
import com.google.gwt.user.client.ui.Widget;
import com.google.gwtexpui.globalkey.client.NpTextBox;
import com.google.gwtexpui.user.client.AutoCenterDialogBox;
+import com.google.gwtjsonrpc.common.AsyncCallback;
import java.util.ArrayList;
import java.util.Collections;
@@ -102,12 +104,37 @@
}
int row = 0;
- if (!Gerrit.getConfig().canEdit(FieldName.USER_NAME)) {
+ if (!Gerrit.getConfig().canEdit(FieldName.USER_NAME)
+ && Gerrit.getConfig().siteHasUsernames()) {
infoPlainText.resizeRows(infoPlainText.getRowCount() + 1);
row(infoPlainText, row++, Util.C.userName(), new UsernameField());
}
- row(infoPlainText, row++, Util.C.contactFieldFullName(), nameTxt);
+ if (!canEditFullName()) {
+ FlowPanel nameLine = new FlowPanel();
+ nameLine.add(nameTxt);
+ if (Gerrit.getConfig().getEditFullNameUrl() != null) {
+ Button edit = new Button(Util.C.linkEditFullName());
+ edit.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ Window.open(Gerrit.getConfig().getEditFullNameUrl(), "_blank", null);
+ }
+ });
+ nameLine.add(edit);
+ }
+ Button reload = new Button(Util.C.linkReloadContact());
+ reload.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ Window.Location.replace(Gerrit.loginRedirect(PageLinks.SETTINGS_CONTACT));
+ }
+ });
+ nameLine.add(reload);
+ row(infoPlainText, row++, Util.C.contactFieldFullName(), nameLine);
+ } else {
+ row(infoPlainText, row++, Util.C.contactFieldFullName(), nameTxt);
+ }
row(infoPlainText, row++, Util.C.contactFieldEmail(), emailLine);
infoPlainText.getCellFormatter().addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
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 af619a8..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,21 +14,15 @@
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;
import com.google.gerrit.client.ui.Hyperlink;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.AgreementInfo;
-import com.google.gerrit.reviewdb.client.AbstractAgreement;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
+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;
@@ -52,16 +46,15 @@
});
}
- private class AgreementTable extends FancyFlexTable<AbstractAgreement> {
+ private class AgreementTable extends FancyFlexTable<ContributorAgreement> {
AgreementTable() {
table.setWidth("");
table.setText(0, 1, Util.C.agreementStatus());
table.setText(0, 2, Util.C.agreementName());
table.setText(0, 3, Util.C.agreementDescription());
- table.setText(0, 4, Util.C.agreementAccepted());
final FlexCellFormatter fmt = table.getFlexCellFormatter();
- for (int c = 1; c <= 4; c++) {
+ for (int c = 1; c < 4; c++) {
fmt.addStyleName(0, c, Gerrit.RESOURCES.css().dataHeader());
}
}
@@ -70,37 +63,22 @@
while (1 < table.getRowCount())
table.removeRow(table.getRowCount() - 1);
- for (final AccountAgreement k : result.userAccepted) {
- addOne(result, k);
- }
- for (final AccountGroupAgreement k : result.groupAccepted) {
+ for (final String k : result.accepted) {
addOne(result, k);
}
}
- void addOne(final AgreementInfo info, final AbstractAgreement k) {
+ void addOne(final AgreementInfo info, final String k) {
final int row = table.getRowCount();
table.insertRow(row);
applyDataRowStyle(row);
- final ContributorAgreement cla = info.agreements.get(k.getAgreementId());
+ final ContributorAgreement cla = info.agreements.get(k);
final String statusName;
- if (cla == null || !cla.isActive()) {
+ if (cla == null) {
statusName = Util.C.agreementStatus_EXPIRED();
} else {
- switch (k.getStatus()) {
- case NEW:
- statusName = Util.C.agreementStatus_NEW();
- break;
- case REJECTED:
- statusName = Util.C.agreementStatus_REJECTED();
- break;
- case VERIFIED:
- statusName = Util.C.agreementStatus_VERIFIED();
- break;
- default:
- statusName = k.getStatus().name();
- }
+ statusName = Util.C.agreementStatus_VERIFIED();
}
table.setText(row, 1, statusName);
@@ -110,28 +88,20 @@
} else {
final String url = cla.getAgreementUrl();
if (url != null && url.length() > 0) {
- final Anchor a = new Anchor(cla.getShortName(), url);
+ final Anchor a = new Anchor(cla.getName(), url);
a.setTarget("_blank");
table.setWidget(row, 2, a);
} else {
- table.setText(row, 2, cla.getShortName());
+ table.setText(row, 2, cla.getName());
}
- table.setText(row, 3, cla.getShortDescription());
+ table.setText(row, 3, cla.getDescription());
}
-
- final SafeHtmlBuilder b = new SafeHtmlBuilder();
- b.append(FormatUtil.mediumFormat(k.getAcceptedOn()));
- b.br();
- b.append(FormatUtil.mediumFormat(k.getReviewedOn()));
- SafeHtml.set(table, row, 4, b);
-
final FlexCellFormatter fmt = table.getFlexCellFormatter();
- for (int c = 1; c <= 4; c++) {
+ for (int c = 1; c < 4; c++) {
fmt.addStyleName(row, c, Gerrit.RESOURCES.css().dataCell());
}
- fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().cLastUpdate());
- setRowItem(row, k);
+ setRowItem(row, cla);
}
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index 84e87aa..de6e666 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -41,8 +41,8 @@
private CheckBox showSiteHeader;
private CheckBox useFlashClipboard;
private CheckBox copySelfOnEmails;
- private CheckBox displayPatchSetsInReverseOrder;
- private CheckBox displayPersonNameInReviewCategory;
+ private CheckBox reversePatchSetOrder;
+ private CheckBox showUsernameInReviewCategory;
private ListBox maximumPageSize;
private ListBox dateFormat;
private ListBox timeFormat;
@@ -74,11 +74,11 @@
copySelfOnEmails = new CheckBox(Util.C.copySelfOnEmails());
copySelfOnEmails.addClickHandler(onClickSave);
- displayPatchSetsInReverseOrder = new CheckBox(Util.C.displayPatchSetsInReverseOrder());
- displayPatchSetsInReverseOrder.addClickHandler(onClickSave);
+ reversePatchSetOrder = new CheckBox(Util.C.reversePatchSetOrder());
+ reversePatchSetOrder.addClickHandler(onClickSave);
- displayPersonNameInReviewCategory = new CheckBox(Util.C.displayPersonNameInReviewCategory());
- displayPersonNameInReviewCategory.addClickHandler(onClickSave);
+ showUsernameInReviewCategory = new CheckBox(Util.C.showUsernameInReviewCategory());
+ showUsernameInReviewCategory.addClickHandler(onClickSave);
maximumPageSize = new ListBox();
for (final short v : PAGESIZE_CHOICES) {
@@ -137,11 +137,11 @@
row++;
formGrid.setText(row, labelIdx, "");
- formGrid.setWidget(row, fieldIdx, displayPatchSetsInReverseOrder);
+ formGrid.setWidget(row, fieldIdx, reversePatchSetOrder);
row++;
formGrid.setText(row, labelIdx, "");
- formGrid.setWidget(row, fieldIdx, displayPersonNameInReviewCategory);
+ formGrid.setWidget(row, fieldIdx, showUsernameInReviewCategory);
row++;
formGrid.setText(row, labelIdx, Util.C.maximumPageSizeFieldLabel());
@@ -179,8 +179,8 @@
showSiteHeader.setEnabled(on);
useFlashClipboard.setEnabled(on);
copySelfOnEmails.setEnabled(on);
- displayPatchSetsInReverseOrder.setEnabled(on);
- displayPersonNameInReviewCategory.setEnabled(on);
+ reversePatchSetOrder.setEnabled(on);
+ showUsernameInReviewCategory.setEnabled(on);
maximumPageSize.setEnabled(on);
dateFormat.setEnabled(on);
timeFormat.setEnabled(on);
@@ -190,8 +190,8 @@
showSiteHeader.setValue(p.isShowSiteHeader());
useFlashClipboard.setValue(p.isUseFlashClipboard());
copySelfOnEmails.setValue(p.isCopySelfOnEmails());
- displayPatchSetsInReverseOrder.setValue(p.isDisplayPatchSetsInReverseOrder());
- displayPersonNameInReviewCategory.setValue(p.isDisplayPersonNameInReviewCategory());
+ reversePatchSetOrder.setValue(p.isReversePatchSetOrder());
+ showUsernameInReviewCategory.setValue(p.isShowUsernameInReviewCategory());
setListBox(maximumPageSize, DEFAULT_PAGESIZE, p.getMaximumPageSize());
setListBox(dateFormat, AccountGeneralPreferences.DateFormat.STD, //
p.getDateFormat());
@@ -251,8 +251,8 @@
p.setShowSiteHeader(showSiteHeader.getValue());
p.setUseFlashClipboard(useFlashClipboard.getValue());
p.setCopySelfOnEmails(copySelfOnEmails.getValue());
- p.setDisplayPatchSetsInReverseOrder(displayPatchSetsInReverseOrder.getValue());
- p.setDisplayPersonNameInReviewCategory(displayPersonNameInReviewCategory.getValue());
+ p.setReversePatchSetOrder(reversePatchSetOrder.getValue());
+ p.setShowUsernameInReviewCategory(showUsernameInReviewCategory.getValue());
p.setMaximumPageSize(getListBox(maximumPageSize, DEFAULT_PAGESIZE));
p.setDateFormat(getListBox(dateFormat,
AccountGeneralPreferences.DateFormat.STD,
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
index 165168d..ae04e0a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
@@ -38,21 +38,24 @@
fieldIdx = 1;
}
- info = new Grid(5, 2);
+ info = new Grid((Gerrit.getConfig().siteHasUsernames() ? 1 : 0) + 4, 2);
info.setStyleName(Gerrit.RESOURCES.css().infoBlock());
info.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
add(info);
- infoRow(0, Util.C.userName());
- infoRow(1, Util.C.fullName());
- infoRow(2, Util.C.preferredEmail());
- infoRow(3, Util.C.registeredOn());
- infoRow(4, Util.C.accountId());
+ int row = 0;
+ if (Gerrit.getConfig().siteHasUsernames()) {
+ infoRow(row++, Util.C.userName());
+ }
+ infoRow(row++, Util.C.fullName());
+ infoRow(row++, Util.C.preferredEmail());
+ infoRow(row++, Util.C.registeredOn());
+ infoRow(row++, Util.C.accountId());
final CellFormatter fmt = info.getCellFormatter();
fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
- fmt.addStyleName(4, 0, Gerrit.RESOURCES.css().bottomheader());
+ fmt.addStyleName(row - 1, 0, Gerrit.RESOURCES.css().bottomheader());
}
@Override
@@ -69,10 +72,13 @@
}
void display(final Account account) {
- info.setWidget(0, fieldIdx, new UsernameField());
- info.setText(1, fieldIdx, account.getFullName());
- info.setText(2, fieldIdx, account.getPreferredEmail());
- info.setText(3, fieldIdx, mediumFormat(account.getRegisteredOn()));
- info.setText(4, fieldIdx, account.getId().toString());
+ int row = 0;
+ if (Gerrit.getConfig().siteHasUsernames()) {
+ info.setWidget(row++, fieldIdx, new UsernameField());
+ }
+ info.setText(row++, fieldIdx, account.getFullName());
+ info.setText(row++, fieldIdx, account.getPreferredEmail());
+ info.setText(row++, fieldIdx, mediumFormat(account.getRegisteredOn()));
+ info.setText(row++, fieldIdx, account.getId().toString());
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
index 5a62eac..1334d87 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
@@ -15,6 +15,7 @@
package com.google.gerrit.client.account;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.projects.ProjectMap;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.HintTextBox;
@@ -22,7 +23,6 @@
import com.google.gerrit.client.ui.ProjectsTable;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.AccountProjectWatchInfo;
-import com.google.gerrit.common.data.ProjectList;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
@@ -226,14 +226,14 @@
// prevent user input from being overwritten by simply poping up
if (! popingUp || "".equals(nameBox.getText()) ) {
- nameBox.setText(getRowItem(row).getName());
+ nameBox.setText(getRowItem(row).name());
}
}
@Override
protected void onOpenRow(final int row) {
super.onOpenRow(row);
- nameBox.setText(getRowItem(row).getName());
+ nameBox.setText(getRowItem(row).name());
doAddNew();
}
};
@@ -361,11 +361,10 @@
}
protected void populateProjects() {
- Util.PROJECT_SVC.visibleProjects(
- new GerritCallback<ProjectList>() {
+ ProjectMap.all(new GerritCallback<ProjectMap>() {
@Override
- public void onSuccess(final ProjectList result) {
- projectsTab.display(result.getProjects());
+ public void onSuccess(final ProjectMap result) {
+ projectsTab.display(result);
if (firstPopupLoad) { // Display was delayed until table was loaded
firstPopupLoad = false;
displayPopup();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
index 154e2ce..ac44dbe 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
@@ -22,10 +22,8 @@
import com.google.gerrit.client.ui.SmallHeading;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.AgreementInfo;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
@@ -53,7 +51,7 @@
public class NewAgreementScreen extends AccountScreen {
private final String nextToken;
- private Set<ContributorAgreement.Id> mySigned;
+ private Set<String> mySigned;
private List<ContributorAgreement> available;
private ContributorAgreement current;
@@ -83,13 +81,7 @@
Util.ACCOUNT_SVC.myAgreements(new GerritCallback<AgreementInfo>() {
public void onSuccess(AgreementInfo result) {
if (isAttached()) {
- mySigned = new HashSet<ContributorAgreement.Id>();
- for (AccountAgreement a : result.userAccepted) {
- mySigned.add(a.getAgreementId());
- }
- for (AccountGroupAgreement a : result.groupAccepted) {
- mySigned.add(a.getAgreementId());
- }
+ mySigned = new HashSet<String>(result.accepted);
postRPC();
}
}
@@ -176,11 +168,11 @@
radios.add(hdr);
for (final ContributorAgreement cla : available) {
- final RadioButton r = new RadioButton("cla_id", cla.getShortName());
+ final RadioButton r = new RadioButton("cla_id", cla.getName());
r.addStyleName(Gerrit.RESOURCES.css().contributorAgreementButton());
radios.add(r);
- if (mySigned.contains(cla.getId())) {
+ if (mySigned.contains(cla.getName())) {
r.setEnabled(false);
final Label l = new Label(Util.C.newAgreementAlreadySubmitted());
l.setStyleName(Gerrit.RESOURCES.css().contributorAgreementAlreadySubmitted());
@@ -194,9 +186,8 @@
});
}
- if (cla.getShortDescription() != null
- && !cla.getShortDescription().equals("")) {
- final Label l = new Label(cla.getShortDescription());
+ if (cla.getDescription() != null && !cla.getDescription().equals("")) {
+ final Label l = new Label(cla.getDescription());
l.setStyleName(Gerrit.RESOURCES.css().contributorAgreementShortDescription());
radios.add(l);
}
@@ -231,7 +222,7 @@
}
private void doEnterAgreement() {
- Util.ACCOUNT_SEC.enterAgreement(current.getId(),
+ Util.ACCOUNT_SEC.enterAgreement(current.getName(),
new GerritCallback<VoidResult>() {
public void onSuccess(final VoidResult result) {
Gerrit.display(nextToken);
@@ -284,8 +275,9 @@
contactGroup.add(contactPanel);
contactPanel.hideSaveButton();
}
- contactGroup.setVisible(cla.isRequireContactInformation());
- finalGroup.setVisible(true);
+ contactGroup.setVisible(
+ cla.isRequireContactInformation() && cla.getAutoVerify() != null);
+ finalGroup.setVisible(cla.getAutoVerify() != null);
yesIAgreeBox.setText("");
submit.setEnabled(false);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
index 72ac1a4..93739c2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
@@ -276,7 +276,8 @@
private class PermissionEditorSource extends EditorSource<PermissionEditor> {
@Override
public PermissionEditor create(int index) {
- PermissionEditor subEditor = new PermissionEditor(readOnly, value);
+ PermissionEditor subEditor =
+ new PermissionEditor(projectAccess.getProjectName(), readOnly, value);
permissionContainer.insert(subEditor, index);
return subEditor;
}
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..ea7c04b 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
@@ -27,17 +27,10 @@
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwtexpui.clippy.client.CopyableLabel;
@@ -45,8 +38,6 @@
import com.google.gwtexpui.globalkey.client.NpTextBox;
import com.google.gwtjsonrpc.common.VoidResult;
-import java.util.List;
-
public class AccountGroupInfoScreen extends AccountGroupScreen {
private CopyableLabel groupUUIDLabel;
@@ -64,12 +55,6 @@
private ListBox typeSelect;
private Button saveType;
- private Panel externalPanel;
- private Label externalName;
- private NpTextBox externalNameFilter;
- private Button externalNameSearch;
- private Grid externalMatches;
-
private CheckBox visibleToAllCheckBox;
private Button saveGroupOptions;
@@ -86,8 +71,6 @@
initDescription();
initGroupOptions();
initGroupType();
-
- initExternal();
}
private void enableForm(final boolean canModify) {
@@ -95,8 +78,6 @@
ownerTxtBox.setEnabled(canModify);
descTxt.setEnabled(canModify);
typeSelect.setEnabled(canModify);
- externalNameFilter.setEnabled(canModify);
- externalNameSearch.setEnabled(canModify);
visibleToAllCheckBox.setEnabled(canModify);
}
@@ -243,7 +224,6 @@
typeSelect = new ListBox();
typeSelect.setStyleName(Gerrit.RESOURCES.css().groupTypeSelectListBox());
typeSelect.addItem(Util.C.groupType_INTERNAL(), AccountGroup.Type.INTERNAL.name());
- typeSelect.addItem(Util.C.groupType_LDAP(), AccountGroup.Type.LDAP.name());
typeSelect.addChangeHandler(new ChangeHandler() {
@Override
public void onChange(ChangeEvent event) {
@@ -279,54 +259,12 @@
add(fp);
}
- private void initExternal() {
- externalName = new Label();
-
- externalNameFilter = new NpTextBox();
- externalNameFilter.setStyleName(Gerrit.RESOURCES.css()
- .groupExternalNameFilterTextBox());
- externalNameFilter.setVisibleLength(30);
- externalNameFilter.addKeyPressHandler(new KeyPressHandler() {
- @Override
- public void onKeyPress(final KeyPressEvent event) {
- if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
- doExternalSearch();
- }
- }
- });
-
- externalNameSearch = new Button(Gerrit.C.searchButton());
- externalNameSearch.addClickHandler(new ClickHandler() {
- @Override
- public void onClick(ClickEvent event) {
- doExternalSearch();
- }
- });
-
- externalMatches = new Grid();
- externalMatches.setStyleName(Gerrit.RESOURCES.css().infoTable());
- externalMatches.setVisible(false);
-
- final FlowPanel searchLine = new FlowPanel();
- searchLine.add(externalNameFilter);
- searchLine.add(externalNameSearch);
-
- externalPanel = new VerticalPanel();
- externalPanel.add(new SmallHeading(Util.C.headingExternalGroup()));
- externalPanel.add(externalName);
- externalPanel.add(searchLine);
- externalPanel.add(externalMatches);
- add(externalPanel);
- }
-
private void setType(final AccountGroup.Type newType) {
final boolean system = newType == AccountGroup.Type.SYSTEM;
typeSystem.setVisible(system);
typeSelect.setVisible(!system);
saveType.setVisible(!system);
- externalPanel.setVisible(newType == AccountGroup.Type.LDAP);
- externalNameFilter.setText(groupNameTxt.getText());
if (!system) {
for (int i = 0; i < typeSelect.getItemCount(); i++) {
@@ -367,75 +305,6 @@
});
}
- private void doExternalSearch() {
- externalNameFilter.setEnabled(false);
- externalNameSearch.setEnabled(false);
- Util.GROUP_SVC.searchExternalGroups(externalNameFilter.getText(),
- new GerritCallback<List<AccountGroup.ExternalNameKey>>() {
- @Override
- public void onSuccess(List<AccountGroup.ExternalNameKey> result) {
- final CellFormatter fmt = externalMatches.getCellFormatter();
-
- 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());
- 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
- public void onFailure(Throwable caught) {
- externalNameFilter.setEnabled(true);
- externalNameSearch.setEnabled(true);
- super.onFailure(caught);
- }
- });
- }
-
- private void setExternalGroup(final AccountGroup.ExternalNameKey key) {
- externalMatches.setVisible(false);
-
- Util.GROUP_SVC.changeExternalGroup(getGroupId(), key,
- new GerritCallback<VoidResult>() {
- @Override
- public void onSuccess(VoidResult result) {
- externalName.setText(key.get());
- }
-
- @Override
- public void onFailure(Throwable caught) {
- externalMatches.setVisible(true);
- super.onFailure(caught);
- }
- });
- }
-
@Override
protected void display(final GroupDetail groupDetail) {
final AccountGroup group = groupDetail.group;
@@ -444,19 +313,12 @@
if (groupDetail.ownerGroup != null) {
ownerTxt.setText(groupDetail.ownerGroup.getName());
} else {
- ownerTxt.setText(Util.M.deletedGroup(group.getOwnerGroupId().get()));
+ ownerTxt.setText(Util.M.deletedReference(group.getOwnerGroupUUID().get()));
}
descTxt.setText(group.getDescription());
visibleToAllCheckBox.setValue(group.isVisibleToAll());
- switch (group.getType()) {
- case LDAP:
- externalName.setText(group.getExternalNameKey() != null ? group
- .getExternalNameKey().get() : Util.C.noGroupSelected());
- break;
- }
-
setType(group.getType());
enableForm(groupDetail.canModify);
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.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index 9250ca3..d049ff6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -89,6 +89,7 @@
String initialRevision();
String buttonAddBranch();
String buttonDeleteBranch();
+ String branchDeletionOpenChanges();
String groupListPrev();
String groupListNext();
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 ef7b73d..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
@@ -68,6 +68,8 @@
initialRevision = Initial Revision
buttonAddBranch = Create Branch
buttonDeleteBranch = Delete
+branchDeletionOpenChanges = The following branches were not deleted \
+because they have open changes:
groupListPrev = Previous group
groupListNext = Next group
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
index 0ce2d77..43558be 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
@@ -17,6 +17,8 @@
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.projects.ProjectInfo;
+import com.google.gerrit.client.projects.ProjectMap;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.HintTextBox;
import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
@@ -38,8 +40,6 @@
import com.google.gwtexpui.globalkey.client.NpTextBox;
import com.google.gwtjsonrpc.common.VoidResult;
-import java.util.List;
-
public class CreateProjectScreen extends Screen {
private NpTextBox project;
private Button create;
@@ -127,31 +127,30 @@
}
@Override
- protected void populate(final int row, final Project k) {
- final Anchor projectLink = new Anchor(k.getName());
+ protected void populate(final int row, final ProjectInfo k) {
+ final Anchor projectLink = new Anchor(k.name());
projectLink.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
- sugestParent.setText(getRowItem(row).getName());
+ sugestParent.setText(getRowItem(row).name());
}
});
table.setWidget(row, 1, projectLink);
- table.setText(row, 2, k.getDescription());
+ table.setText(row, 2, k.description());
setRowItem(row, k);
}
};
suggestedParentsTab.setVisible(false);
- Util.PROJECT_SVC
- .suggestParentCandidates(new GerritCallback<List<Project>>() {
+ ProjectMap.permissions(new GerritCallback<ProjectMap>() {
@Override
- public void onSuccess(List<Project> result) {
- if (result != null && !result.isEmpty()) {
+ public void onSuccess(ProjectMap list) {
+ if (!list.isEmpty()) {
suggestedParentsTab.setVisible(true);
- suggestedParentsTab.display(result);
+ suggestedParentsTab.display(list);
suggestedParentsTab.finishDisplay();
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index 9d62d34..c0a55d0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -14,8 +14,11 @@
package com.google.gerrit.client.admin;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
+
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountCapabilities;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.AccountScreen;
@@ -48,11 +51,17 @@
@Override
protected void onLoad() {
super.onLoad();
+ addPanel.setVisible(false);
+ AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
+ @Override
+ public void onSuccess(AccountCapabilities ac) {
+ addPanel.setVisible(ac.canPerform(CREATE_GROUP));
+ }
+ }, CREATE_GROUP);
Util.GROUP_SVC
.visibleGroups(new ScreenLoadCallback<GroupList>(this) {
@Override
protected void preDisplay(GroupList result) {
- addPanel.setVisible(result.isCanCreateGroup());
groups.display(result.getGroups());
groups.finishDisplay();
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
index 9da9c22..4ddc9a8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
@@ -17,6 +17,7 @@
import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
import com.google.gerrit.client.ui.RPCSuggestOracle;
import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.editor.client.LeafValueEditor;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
@@ -140,4 +141,8 @@
public void setAccessKey(char key) {
suggestBox.setAccessKey(key);
}
+
+ public void setProject(Project.NameKey projectName) {
+ oracle.setProject(projectName);
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
index d10afd1..2c43233 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
@@ -25,6 +25,7 @@
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
@@ -98,20 +99,25 @@
@UiField
DivElement deleted;
+ private final Project.NameKey projectName;
private final boolean readOnly;
private final AccessSection section;
private Permission value;
private PermissionRange.WithDefaults validRange;
private boolean isDeleted;
- public PermissionEditor(boolean readOnly, AccessSection section) {
+ public PermissionEditor(Project.NameKey projectName,
+ boolean readOnly,
+ AccessSection section) {
this.readOnly = readOnly;
this.section = section;
+ this.projectName = projectName;
normalName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
deletedName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
initWidget(uiBinder.createAndBindUi(this));
+ groupToAdd.setProject(projectName);
rules = ListEditor.of(new RuleEditorSource());
exclusiveGroup.setEnabled(!readOnly);
@@ -223,7 +229,8 @@
// If the oracle didn't get to complete a UUID, resolve it now.
//
addRule.setEnabled(false);
- SuggestUtil.SVC.suggestAccountGroup(ref.getName(), 1,
+ SuggestUtil.SVC.suggestAccountGroupForProject(
+ projectName, ref.getName(), 1,
new GerritCallback<List<GroupReference>>() {
@Override
public void onSuccess(List<GroupReference> result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
index 1ed919b..923a63e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
@@ -18,6 +18,7 @@
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.ProjectAccess;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.core.client.GWT;
@@ -33,6 +34,8 @@
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwtexpui.globalkey.client.NpTextArea;
+import java.util.Collections;
+
public class ProjectAccessScreen extends ProjectScreen {
interface Binder extends UiBinder<HTMLPanel, ProjectAccessScreen> {
}
@@ -110,6 +113,8 @@
@UiHandler("edit")
void onEdit(ClickEvent event) {
+ resetEditors();
+
edit.setEnabled(false);
cancel1.setVisible(true);
UIObject.setVisible(commitTools, true);
@@ -117,6 +122,18 @@
driver.edit(access);
}
+ private void resetEditors() {
+ // Push an empty instance through the driver before pushing the real
+ // data. This will force GWT to delete and recreate the editors, which
+ // is required to build initialize them as editable vs. read-only.
+ ProjectAccess mock = new ProjectAccess();
+ mock.setProjectName(access.getProjectName());
+ mock.setRevision(access.getRevision());
+ mock.setLocal(Collections.<AccessSection> emptyList());
+ mock.setOwnerOf(Collections.<String> emptySet());
+ driver.edit(mock);
+ }
+
@UiHandler(value={"cancel1", "cancel2"})
void onCancel(ClickEvent event) {
Gerrit.display(PageLinks.toProjectAcceess(getProjectKey()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index f3ecbb3..56d9417 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -16,16 +16,19 @@
import com.google.gerrit.client.ConfirmationCallback;
import com.google.gerrit.client.ConfirmationDialog;
+import com.google.gerrit.client.ErrorDialog;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.GitwebLink;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.BranchLink;
import com.google.gerrit.client.ui.FancyFlexTable;
import com.google.gerrit.client.ui.HintTextBox;
import com.google.gerrit.common.data.ListBranchesResult;
import com.google.gerrit.common.errors.InvalidNameException;
import com.google.gerrit.common.errors.InvalidRevisionException;
import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
@@ -41,6 +44,7 @@
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import com.google.gwtjsonrpc.client.RemoteJsonException;
@@ -260,24 +264,52 @@
b.toSafeHtml(), new ConfirmationCallback() {
@Override
public void onOk() {
- Util.PROJECT_SVC.deleteBranch(getProjectKey(), ids,
- new GerritCallback<Set<Branch.NameKey>>() {
- public void onSuccess(final Set<Branch.NameKey> deleted) {
- for (int row = 1; row < table.getRowCount();) {
- final Branch k = getRowItem(row);
- if (k != null && deleted.contains(k.getNameKey())) {
- table.removeRow(row);
- } else {
- row++;
- }
- }
- }
- });
+ deleteBranches(ids);
}
});
confirmationDialog.center();
}
+ private void deleteBranches(final Set<Branch.NameKey> branchIds) {
+ Util.PROJECT_SVC.deleteBranch(getProjectKey(), branchIds,
+ new GerritCallback<Set<Branch.NameKey>>() {
+ public void onSuccess(final Set<Branch.NameKey> deleted) {
+ if (!deleted.isEmpty()) {
+ for (int row = 1; row < table.getRowCount();) {
+ final Branch k = getRowItem(row);
+ if (k != null && deleted.contains(k.getNameKey())) {
+ table.removeRow(row);
+ } else {
+ row++;
+ }
+ }
+ }
+
+ branchIds.removeAll(deleted);
+ if (!branchIds.isEmpty()) {
+ final VerticalPanel p = new VerticalPanel();
+ final ErrorDialog errorDialog = new ErrorDialog(p);
+ final Label l = new Label(Util.C.branchDeletionOpenChanges());
+ l.setStyleName(Gerrit.RESOURCES.css().errorDialogText());
+ p.add(l);
+ for (final Branch.NameKey branch : branchIds) {
+ final BranchLink link =
+ new BranchLink(branch.getParentKey(), Change.Status.NEW,
+ branch.get(), null) {
+ @Override
+ public void go() {
+ errorDialog.hide();
+ super.go();
+ };
+ };
+ p.add(link);
+ }
+ errorDialog.center();
+ }
+ }
+ });
+ }
+
void display(final List<Branch> result) {
canDelete = false;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
index 3599c3c..5cb178c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -14,15 +14,19 @@
package com.google.gerrit.client.admin;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
+
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountCapabilities;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.projects.ProjectInfo;
+import com.google.gerrit.client.projects.ProjectMap;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.Hyperlink;
import com.google.gerrit.client.ui.ProjectsTable;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ProjectList;
-import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.ui.VerticalPanel;
@@ -33,11 +37,17 @@
@Override
protected void onLoad() {
super.onLoad();
- Util.PROJECT_SVC.visibleProjects(new ScreenLoadCallback<ProjectList>(this) {
+ createProjectLinkPanel.setVisible(false);
+ AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
@Override
- protected void preDisplay(final ProjectList result) {
- createProjectLinkPanel.setVisible(result.canCreateProject());
- projects.display(result.getProjects());
+ public void onSuccess(AccountCapabilities ac) {
+ createProjectLinkPanel.setVisible(ac.canPerform(CREATE_PROJECT));
+ }
+ }, CREATE_PROJECT);
+ ProjectMap.all(new ScreenLoadCallback<ProjectMap>(this) {
+ @Override
+ protected void preDisplay(final ProjectMap result) {
+ projects.display(result);
projects.finishDisplay();
}
});
@@ -61,14 +71,14 @@
History.newItem(link(getRowItem(row)));
}
- private String link(final Project item) {
- return Dispatcher.toProjectAdmin(item.getNameKey(), ProjectScreen.INFO);
+ private String link(final ProjectInfo item) {
+ return Dispatcher.toProjectAdmin(item.name_key(), ProjectScreen.INFO);
}
@Override
- protected void populate(final int row, final Project k) {
- table.setWidget(row, 1, new Hyperlink(k.getName(), link(k)));
- table.setText(row, 2, k.getDescription());
+ protected void populate(final int row, final ProjectInfo k) {
+ table.setWidget(row, 1, new Hyperlink(k.name(), link(k)));
+ table.setText(row, 2, k.description());
setRowItem(row, k);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index 1d881b6..9efea22 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -14,54 +14,64 @@
package com.google.gerrit.client.changes;
-import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeTable.ApprovalViewType;
+import com.google.gerrit.client.NotFoundScreen;
+import com.google.gerrit.client.rpc.NativeList;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AccountDashboardInfo;
-import com.google.gerrit.common.data.AccountInfo;
import com.google.gerrit.reviewdb.client.Account;
+import java.util.Collections;
+import java.util.Comparator;
public class AccountDashboardScreen extends Screen implements ChangeListScreen {
private final Account.Id ownerId;
- private ChangeTable table;
- private ChangeTable.Section byOwner;
- private ChangeTable.Section forReview;
- private ChangeTable.Section closed;
+ private final boolean mine;
+ private ChangeTable2 table;
+ private ChangeTable2.Section outgoing;
+ private ChangeTable2.Section incoming;
+ private ChangeTable2.Section closed;
public AccountDashboardScreen(final Account.Id id) {
ownerId = id;
+ mine = Gerrit.isSignedIn() && ownerId.equals(Gerrit.getUserAccount().getId());
}
@Override
protected void onInitUI() {
super.onInitUI();
- table = new ChangeTable(true);
+ table = new ChangeTable2();
table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
- byOwner = new ChangeTable.Section("", ApprovalViewType.STRONGEST, null);
- forReview = new ChangeTable.Section("", ApprovalViewType.USER, ownerId);
- closed = new ChangeTable.Section("", ApprovalViewType.STRONGEST, null);
- table.addSection(byOwner);
- table.addSection(forReview);
+ outgoing = new ChangeTable2.Section();
+ incoming = new ChangeTable2.Section();
+ closed = new ChangeTable2.Section();
+
+ outgoing.setTitleText(Util.C.outgoingReviews());
+ incoming.setTitleText(Util.C.incomingReviews());
+ closed.setTitleText(Util.C.recentlyClosed());
+
+ table.addSection(outgoing);
+ table.addSection(incoming);
table.addSection(closed);
add(table);
- table.setSavePointerId(PageLinks.toAccountDashboard(ownerId));
+ table.setSavePointerId("owner:" + ownerId);
}
@Override
protected void onLoad() {
super.onLoad();
- Util.LIST_SVC.forAccount(ownerId,
- new ScreenLoadCallback<AccountDashboardInfo>(this) {
+ String who = mine ? "self" : ownerId.toString();
+ ChangeList.query(
+ new ScreenLoadCallback<NativeList<ChangeList>>(this) {
@Override
- protected void preDisplay(final AccountDashboardInfo r) {
- display(r);
+ protected void preDisplay(NativeList<ChangeList> result) {
+ display(result);
}
- });
+ },
+ "is:open owner:" + who,
+ "is:open reviewer:" + who + " -owner:" + who,
+ "is:closed owner:" + who + " -age:1w limit:10");
}
@Override
@@ -70,20 +80,72 @@
table.setRegisterKeys(true);
}
- private void display(final AccountDashboardInfo r) {
- table.setAccountInfoCache(r.getAccounts());
+ private void display(NativeList<ChangeList> result) {
+ if (!mine && !hasChanges(result)) {
+ // When no results are returned and the data is not for the
+ // current user, the target user is presumed to not exist.
+ Gerrit.display(getToken(), new NotFoundScreen());
+ return;
+ }
- final AccountInfo o = r.getAccounts().get(r.getOwner());
- final String name = FormatUtil.name(o);
- setWindowTitle(name);
- setPageTitle(Util.M.accountDashboardTitle(name));
- byOwner.setTitleText(Util.M.changesStartedBy(name));
- forReview.setTitleText(Util.M.changesReviewableBy(name));
- closed.setTitleText(Util.C.changesRecentlyClosed());
+ ChangeList out = result.get(0);
+ ChangeList in = result.get(1);
+ ChangeList done = result.get(2);
- byOwner.display(r.getByOwner());
- forReview.display(r.getForReview());
- closed.display(r.getClosed());
+ if (mine) {
+ setWindowTitle(Util.C.myDashboardTitle());
+ setPageTitle(Util.C.myDashboardTitle());
+ } else {
+ // The server doesn't tell us who the dashboard is for. Try to guess
+ // by looking at a change started by the owner and extract the name.
+ String name = guessName(out);
+ if (name == null) {
+ name = guessName(done);
+ }
+ if (name != null) {
+ setWindowTitle(name);
+ setPageTitle(Util.M.accountDashboardTitle(name));
+ } else {
+ setWindowTitle(Util.C.unknownDashboardTitle());
+ setWindowTitle(Util.C.unknownDashboardTitle());
+ }
+ }
+
+ Collections.sort(out.asList(), outComparator());
+
+ table.updateColumnsForLabels(out, in, done);
+ outgoing.display(out);
+ incoming.display(in);
+ closed.display(done);
table.finishDisplay();
}
+
+ private Comparator<ChangeInfo> outComparator() {
+ return new Comparator<ChangeInfo>() {
+ @Override
+ public int compare(ChangeInfo a, ChangeInfo b) {
+ int cmp = a.created().compareTo(b.created());
+ if (cmp != 0) return cmp;
+ return a._number() - b._number();
+ }
+ };
+ }
+
+ private boolean hasChanges(NativeList<ChangeList> result) {
+ for (ChangeList list : result.asList()) {
+ if (!list.isEmpty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String guessName(ChangeList list) {
+ for (ChangeInfo change : list.asList()) {
+ if (change.owner() != null && change.owner().name() != null) {
+ return change.owner().name();
+ }
+ }
+ return null;
+ }
}
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..20df1df 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) {
@@ -182,6 +182,9 @@
break;
}
+ case MAY:
+ break;
+
case NEED:
case IMPOSSIBLE:
if (reportedMissing.add(lbl.label)) {
@@ -204,10 +207,12 @@
continue;
}
String labelName = legacyType.getCategory().getLabelName();
- if (psa.getValue() == legacyType.getMax().getValue()) {
- ad.approved(labelName);
- } else if (psa.getValue() == legacyType.getMin().getValue()) {
- ad.rejected(labelName);
+ if (psa.getValue() != 0 ) {
+ if (psa.getValue() == legacyType.getMax().getValue()) {
+ ad.approved(labelName);
+ } else if (psa.getValue() == legacyType.getMin().getValue()) {
+ ad.rejected(labelName);
+ }
}
if (!columns.contains(labelName)) {
columns.add(labelName);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
index 27f76f6..5a9da1a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
@@ -38,7 +38,6 @@
private Change.Id changeId;
private ChangeDetailCache detail;
private ListenableValue<ChangeInfo> info;
- private StarCache starred;
protected ChangeCache(Change.Id chg) {
changeId = chg;
@@ -61,11 +60,4 @@
}
return info;
}
-
- public StarCache getStarCache() {
- if (starred == null) {
- starred = new StarCache(changeId);
- }
- return starred;
- }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 3372096..d42992f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -23,7 +23,11 @@
String statusLongAbandoned();
String statusLongDraft();
- String changesRecentlyClosed();
+ String myDashboardTitle();
+ String unknownDashboardTitle();
+ String incomingReviews();
+ String outgoingReviews();
+ String recentlyClosed();
String starredHeading();
String watchedHeading();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index ad70674..8ceb74c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -7,7 +7,11 @@
starredHeading = Starred Changes
watchedHeading = Open Changes of Watched Projects
draftsHeading = Changes with unpublished drafts
-changesRecentlyClosed = Recently closed
+myDashboardTitle = My Reviews
+unknownDashboardTitle = Code Review Dashboard
+incomingReviews = Incoming reviews
+outgoingReviews = Outgoing reviews
+recentlyClosed = Recently closed
allOpenChanges = All open changes
allAbandonedChanges = All abandoned changes
allMergedChanges = All merged changes
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
index bb28e11..9c19d50 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
@@ -65,6 +65,7 @@
public static void setChangeDetail(ChangeDetail detail) {
Change.Id chgId = detail.getChange().getId();
ChangeCache.get(chgId).getChangeDetailCache().set(detail);
+ StarredChanges.fireChangeStarEvent(chgId, detail.isStarred());
}
private final Change.Id changeId;
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
new file mode 100644
index 0000000..52e3bd2
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
@@ -0,0 +1,122 @@
+// 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.client.changes;
+
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
+
+import java.sql.Timestamp;
+import java.util.Set;
+
+public class ChangeInfo extends JavaScriptObject {
+ public final Project.NameKey project_name_key() {
+ return new Project.NameKey(project());
+ }
+
+ public final Change.Id legacy_id() {
+ return new Change.Id(_number());
+ }
+
+ public final Timestamp created() {
+ Timestamp ts = _get_cts();
+ if (ts == null) {
+ ts = JavaSqlTimestamp_JsonSerializer.parseTimestamp(createdRaw());
+ _set_cts(ts);
+ }
+ return ts;
+ }
+
+ private final native Timestamp _get_cts() /*-{ return this._cts; }-*/;
+ private final native void _set_cts(Timestamp ts) /*-{ this._cts = ts; }-*/;
+
+ public final Timestamp updated() {
+ return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
+ }
+
+ public final String id_abbreviated() {
+ return new Change.Key(id()).abbreviate();
+ }
+
+ public final Change.Status status() {
+ return Change.Status.valueOf(statusRaw());
+ }
+
+ public final Set<String> labels() {
+ return Natives.keys(labels0());
+ }
+
+ public final native String project() /*-{ return this.project; }-*/;
+ public final native String branch() /*-{ return this.branch; }-*/;
+ public final native String topic() /*-{ return this.topic; }-*/;
+ public final native String id() /*-{ return this.id; }-*/;
+ private final native String statusRaw() /*-{ return this.status; }-*/;
+ public final native String subject() /*-{ return this.subject; }-*/;
+ public final native AccountInfo owner() /*-{ return this.owner; }-*/;
+ private final native String createdRaw() /*-{ return this.created; }-*/;
+ private final native String updatedRaw() /*-{ return this.updated; }-*/;
+ public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
+ public final native String _sortkey() /*-{ return this._sortkey; }-*/;
+ private final native JavaScriptObject labels0() /*-{ return this.labels; }-*/;
+ public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
+ final native int _number() /*-{ return this._number; }-*/;
+ final native boolean _more_changes()
+ /*-{ return this._more_changes ? true : false; }-*/;
+
+ protected ChangeInfo() {
+ }
+
+ public static class AccountInfo extends JavaScriptObject {
+ public final native String name() /*-{ return this.name; }-*/;
+
+ protected AccountInfo() {
+ }
+ }
+
+ public static class LabelInfo extends JavaScriptObject {
+ public final SubmitRecord.Label.Status status() {
+ if (approved() != null) {
+ return SubmitRecord.Label.Status.OK;
+ } else if (rejected() != null) {
+ return SubmitRecord.Label.Status.REJECT;
+ } else if (optional()) {
+ return SubmitRecord.Label.Status.MAY;
+ } else {
+ return SubmitRecord.Label.Status.NEED;
+ }
+ }
+
+ public final native String name() /*-{ return this._name; }-*/;
+ public final native AccountInfo approved() /*-{ return this.approved; }-*/;
+ public final native AccountInfo rejected() /*-{ return this.rejected; }-*/;
+
+ public final native AccountInfo recommended() /*-{ return this.recommended; }-*/;
+ public final native AccountInfo disliked() /*-{ return this.disliked; }-*/;
+ public final native boolean optional() /*-{ return this.optional ? true : false; }-*/;
+ final native short _value()
+ /*-{
+ if (this.value) return this.value;
+ if (this.disliked) return -1;
+ if (this.recommended) return 1;
+ return 0;
+ }-*/;
+
+ protected LabelInfo() {
+ }
+ }
+}
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/ChangeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
new file mode 100644
index 0000000..debe145
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.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.client.changes;
+
+import com.google.gerrit.client.rpc.NativeList;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtorm.client.KeyUtil;
+
+/** List of changes available from {@code /changes/}. */
+public class ChangeList extends NativeList<ChangeInfo> {
+ private static final String URI = "/changes/";
+
+ /** Run 2 or more queries in a single remote invocation. */
+ public static void query(
+ AsyncCallback<NativeList<ChangeList>> callback, String... queries) {
+ assert queries.length >= 2; // At least 2 is required for correct result.
+ RestApi call = new RestApi(URI);
+ for (String q : queries) {
+ call.addParameterRaw("q", KeyUtil.encode(q));
+ }
+ call.send(callback);
+ }
+
+ public static void prev(String query,
+ int limit, String sortkey,
+ AsyncCallback<ChangeList> callback) {
+ RestApi call = newQuery(query);
+ if (limit > 0) {
+ call.addParameter("n", limit);
+ }
+ if (!PagedSingleListScreen.MIN_SORTKEY.equals(sortkey)) {
+ call.addParameter("P", sortkey);
+ }
+ call.send(callback);
+ }
+
+ public static void next(String query,
+ int limit, String sortkey,
+ AsyncCallback<ChangeList> callback) {
+ RestApi call = newQuery(query);
+ if (limit > 0) {
+ call.addParameter("n", limit);
+ }
+ if (!PagedSingleListScreen.MAX_SORTKEY.equals(sortkey)) {
+ call.addParameter("N", sortkey);
+ }
+ call.send(callback);
+ }
+
+ private static RestApi newQuery(String query) {
+ RestApi call = new RestApi(URI);
+ // The server default is ?q=status:open so don't repeat it.
+ if (!"status:open".equals(query) && !"is:open".equals(query)) {
+ call.addParameterRaw("q", KeyUtil.encode(query));
+ }
+ return call;
+ }
+
+ protected ChangeList() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index 41dc6c1..4bd0828 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -18,8 +18,6 @@
public interface ChangeMessages extends Messages {
String accountDashboardTitle(String fullName);
- String changesStartedBy(String fullName);
- String changesReviewableBy(String fullName);
String changesOpenInProject(String string);
String changesMergedInProject(String string);
String changesAbandonedInProject(String string);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index 40088a1..2449613 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -1,6 +1,4 @@
accountDashboardTitle = Code Review Dashboard for {0}
-changesStartedBy = Started by {0}
-changesReviewableBy = Review Requests for {0}
changesOpenInProject = Open Changes In {0}
changesMergedInProject = Merged Changes In {0}
changesAbandonedInProject = Abandoned Changes In {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
index 8d5e105..15c1150 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
@@ -28,9 +28,9 @@
import com.google.gerrit.common.data.ChangeInfo;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.KeyPressEvent;
@@ -42,7 +42,6 @@
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
@@ -60,9 +59,7 @@
private final Change.Id changeId;
private final PatchSet.Id openPatchSetId;
private ChangeDetailCache detailCache;
- private StarCache starred;
- private Image starChange;
private ChangeDescriptionBlock descriptionBlock;
private ApprovalTable approvals;
@@ -118,14 +115,6 @@
}
@Override
- public void onSignOut() {
- super.onSignOut();
- if (starChange != null) {
- starChange.setVisible(false);
- }
- }
-
- @Override
protected void onLoad() {
super.onLoad();
detailCache.refresh();
@@ -163,8 +152,6 @@
detailCache = cache.getChangeDetailCache();
detailCache.addValueChangeHandler(this);
- starred = cache.getStarCache();
-
addStyleName(Gerrit.RESOURCES.css().changeScreen());
keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
@@ -173,13 +160,13 @@
keysNavigation.add(new ExpandCollapseDependencySectionKeyCommand(0, 'd', Util.C.expandCollapseDependencies()));
if (Gerrit.isSignedIn()) {
- keysAction.add(starred.new KeyCommand(0, 's', Util.C.changeTableStar()));
+ StarredChanges.Icon star = StarredChanges.createIcon(changeId, false);
+ star.setStyleName(Gerrit.RESOURCES.css().changeScreenStarIcon());
+ setTitleWest(star);
+
+ keysAction.add(StarredChanges.newKeyCommand(star));
keysAction.add(new PublishCommentsKeyCommand(0, 'r', Util.C
.keyPublishComments()));
-
- starChange = starred.createStar();
- starChange.setStyleName(Gerrit.RESOURCES.css().changeScreenStarIcon());
- setTitleWest(starChange);
}
descriptionBlock = new ChangeDescriptionBlock();
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 5a568f1..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;
@@ -145,7 +145,7 @@
protected void onStarClick(final int row) {
final ChangeInfo c = getRowItem(row);
if (c != null && Gerrit.isSignedIn()) {
- ChangeCache.get(c.getId()).getStarCache().toggleStar();
+ ((StarredChanges.Icon) table.getWidget(row, C_STAR)).toggleStar();
}
}
@@ -198,7 +198,7 @@
final String idstr = c.getKey().abbreviate();
table.setWidget(row, C_ARROW, null);
if (Gerrit.isSignedIn()) {
- table.setWidget(row, C_STAR, cache.getStarCache().createStar());
+ table.setWidget(row, C_STAR, StarredChanges.createIcon(c.getId(), c.isStarred()));
}
table.setWidget(row, C_ID, new TableChangeLink(idstr, c));
@@ -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) {
@@ -286,13 +286,13 @@
int col = BASE_COLUMNS;
boolean haveReview = false;
- boolean displayPersonNameInReviewCategory = false;
+ boolean showUsernameInReviewCategory = false;
if (Gerrit.isSignedIn()) {
AccountGeneralPreferences prefs = Gerrit.getUserAccount().getGeneralPreferences();
- if (prefs.isDisplayPersonNameInReviewCategory()) {
- displayPersonNameInReviewCategory = true;
+ if (prefs.isShowUsernameInReviewCategory()) {
+ showUsernameInReviewCategory = true;
}
}
@@ -314,7 +314,7 @@
if (type.isMaxNegative(ca)) {
- if (displayPersonNameInReviewCategory) {
+ if (showUsernameInReviewCategory) {
FlowPanel fp = new FlowPanel();
fp.add(new Image(Gerrit.RESOURCES.redNot()));
fp.add(new InlineLabel(FormatUtil.name(ai)));
@@ -325,7 +325,7 @@
} else if (type.isMaxPositive(ca)) {
- if (displayPersonNameInReviewCategory) {
+ if (showUsernameInReviewCategory) {
FlowPanel fp = new FlowPanel();
fp.add(new Image(Gerrit.RESOURCES.greenCheck()));
fp.add(new InlineLabel(FormatUtil.name(ai)));
@@ -337,7 +337,7 @@
} else {
String vstr = String.valueOf(ca.getValue());
- if (displayPersonNameInReviewCategory) {
+ if (showUsernameInReviewCategory) {
vstr = vstr + " " + FormatUtil.name(ai);
}
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
new file mode 100644
index 0000000..1b9db39
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
@@ -0,0 +1,406 @@
+// Copyright (C) 2008 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.client.changes;
+
+import static com.google.gerrit.client.FormatUtil.shortFormat;
+
+import com.google.gerrit.client.Gerrit;
+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;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLTable.Cell;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.InlineLabel;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ChangeTable2 extends NavigationTable<ChangeInfo> {
+ private static final int C_STAR = 1;
+ private static final int C_ID = 2;
+ private static final int C_SUBJECT = 3;
+ private static final int C_OWNER = 4;
+ private static final int C_PROJECT = 5;
+ private static final int C_BRANCH = 6;
+ private static final int C_LAST_UPDATE = 7;
+ private static final int BASE_COLUMNS = 8;
+
+ private final List<Section> sections;
+ private int columns;
+ private List<String> labelNames;
+
+ public ChangeTable2() {
+ columns = BASE_COLUMNS;
+ labelNames = Collections.emptyList();
+
+ keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.changeTablePrev()));
+ keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.changeTableNext()));
+ keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.changeTableOpen()));
+ keysNavigation.add(
+ new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.C.changeTableOpen()));
+
+ if (Gerrit.isSignedIn()) {
+ keysAction.add(new StarKeyCommand(0, 's', Util.C.changeTableStar()));
+ }
+
+ sections = new ArrayList<Section>();
+ table.setText(0, C_STAR, "");
+ table.setText(0, C_ID, Util.C.changeTableColumnID());
+ table.setText(0, C_SUBJECT, Util.C.changeTableColumnSubject());
+ table.setText(0, C_OWNER, Util.C.changeTableColumnOwner());
+ table.setText(0, C_PROJECT, Util.C.changeTableColumnProject());
+ table.setText(0, C_BRANCH, Util.C.changeTableColumnBranch());
+ table.setText(0, C_LAST_UPDATE, Util.C.changeTableColumnLastUpdate());
+
+ final FlexCellFormatter fmt = table.getFlexCellFormatter();
+ fmt.addStyleName(0, C_STAR, Gerrit.RESOURCES.css().iconHeader());
+ fmt.addStyleName(0, C_ID, Gerrit.RESOURCES.css().cID());
+ for (int i = C_ID; i < columns; i++) {
+ fmt.addStyleName(0, i, Gerrit.RESOURCES.css().dataHeader());
+ }
+
+ table.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(final ClickEvent event) {
+ final Cell cell = table.getCellForEvent(event);
+ if (cell == null) {
+ return;
+ }
+ if (cell.getCellIndex() == C_STAR) {
+ // Don't do anything (handled by star itself).
+ } else if (cell.getCellIndex() == C_OWNER) {
+ // Don't do anything.
+ } else if (getRowItem(cell.getRowIndex()) != null) {
+ movePointerTo(cell.getRowIndex());
+ }
+ }
+ });
+ }
+
+ @Override
+ protected Object getRowItemKey(final ChangeInfo item) {
+ return item.legacy_id();
+ }
+
+ @Override
+ protected void onOpenRow(final int row) {
+ final ChangeInfo c = getRowItem(row);
+ final Change.Id id = c.legacy_id();
+ Gerrit.display(PageLinks.toChange(id), new ChangeScreen(id));
+ }
+
+ private void insertNoneRow(final int row) {
+ insertRow(row);
+ table.setText(row, 0, Util.C.changeTableNone());
+ final FlexCellFormatter fmt = table.getFlexCellFormatter();
+ fmt.setColSpan(row, 0, columns);
+ fmt.setStyleName(row, 0, Gerrit.RESOURCES.css().emptySection());
+ }
+
+ private void insertChangeRow(final int row) {
+ insertRow(row);
+ applyDataRowStyle(row);
+ }
+
+ @Override
+ protected void applyDataRowStyle(final int row) {
+ super.applyDataRowStyle(row);
+ final CellFormatter fmt = table.getCellFormatter();
+ fmt.addStyleName(row, C_STAR, Gerrit.RESOURCES.css().iconCell());
+ for (int i = C_ID; i < columns; i++) {
+ fmt.addStyleName(row, i, Gerrit.RESOURCES.css().dataCell());
+ }
+ fmt.addStyleName(row, C_ID, Gerrit.RESOURCES.css().cID());
+ fmt.addStyleName(row, C_SUBJECT, Gerrit.RESOURCES.css().cSUBJECT());
+ fmt.addStyleName(row, C_PROJECT, Gerrit.RESOURCES.css().cPROJECT());
+ fmt.addStyleName(row, C_BRANCH, Gerrit.RESOURCES.css().cPROJECT());
+ fmt.addStyleName(row, C_LAST_UPDATE, Gerrit.RESOURCES.css().cLastUpdate());
+ for (int i = BASE_COLUMNS; i < columns; i++) {
+ fmt.addStyleName(row, i, Gerrit.RESOURCES.css().cAPPROVAL());
+ }
+ }
+
+ public void updateColumnsForLabels(ChangeList... lists) {
+ labelNames = new ArrayList<String>();
+ for (ChangeList list : lists) {
+ for (int i = 0; i < list.size(); i++) {
+ for (String name : list.get(i).labels()) {
+ if (!labelNames.contains(name)) {
+ labelNames.add(name);
+ }
+ }
+ }
+ }
+ Collections.sort(labelNames);
+
+ if (BASE_COLUMNS + labelNames.size() < columns) {
+ int n = columns - (BASE_COLUMNS + labelNames.size());
+ for (int row = 0; row < table.getRowCount(); row++) {
+ table.removeCells(row, columns, n);
+ }
+ }
+ columns = BASE_COLUMNS + labelNames.size();
+
+ FlexCellFormatter fmt = table.getFlexCellFormatter();
+ for (int i = 0; i < labelNames.size(); i++) {
+ String name = labelNames.get(i);
+ int col = BASE_COLUMNS + i;
+
+ StringBuilder abbrev = new StringBuilder();
+ for (String t : name.split("-")) {
+ abbrev.append(t.substring(0, 1).toUpperCase());
+ }
+ table.setText(0, col, abbrev.toString());
+ table.getCellFormatter().getElement(0, col).setTitle(name);
+ fmt.addStyleName(0, col, Gerrit.RESOURCES.css().dataHeader());
+ }
+
+ for (Section s : sections) {
+ if (s.titleRow >= 0) {
+ fmt.setColSpan(s.titleRow, 0, columns);
+ }
+ }
+ }
+
+ private void populateChangeRow(final int row, final ChangeInfo c) {
+ if (Gerrit.isSignedIn()) {
+ table.setWidget(row, C_STAR, StarredChanges.createIcon(
+ c.legacy_id(),
+ c.starred()));
+ }
+ table.setWidget(row, C_ID, new TableChangeLink(c.id_abbreviated(), c));
+
+ String subject = c.subject();
+ if (subject.length() > 80) {
+ subject = subject.substring(0, 80);
+ }
+ Change.Status status = c.status();
+ if (status != Change.Status.NEW) {
+ subject += " (" + Util.toLongString(status) + ")";
+ }
+ table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
+
+ String owner = "";
+ if (c.owner() != null && c.owner().name() != null) {
+ owner = c.owner().name();
+ }
+
+ table.setWidget(row, C_OWNER, new InlineHyperlink(owner,
+ PageLinks.toAccountQuery(owner)));
+
+ table.setWidget(
+ row, C_PROJECT, new ProjectLink(c.project_name_key(), c.status()));
+ table.setWidget(row, C_BRANCH, new BranchLink(c.project_name_key(), c
+ .status(), c.branch(), c.topic()));
+ table.setText(row, C_LAST_UPDATE, shortFormat(c.updated()));
+
+ boolean displayName = Gerrit.isSignedIn() && Gerrit.getUserAccount()
+ .getGeneralPreferences().isShowUsernameInReviewCategory();
+
+ CellFormatter fmt = table.getCellFormatter();
+ for (int idx = 0; idx < labelNames.size(); idx++) {
+ String name = labelNames.get(idx);
+ int col = BASE_COLUMNS + idx;
+
+ LabelInfo label = c.label(name);
+ if (label == null) {
+ table.clearCell(row, col);
+ continue;
+ }
+
+ String user;
+ if (label.rejected() != null) {
+ user = label.rejected().name();
+ if (displayName && user != null) {
+ FlowPanel panel = new FlowPanel();
+ panel.add(new Image(Gerrit.RESOURCES.redNot()));
+ panel.add(new InlineLabel(user));
+ table.setWidget(row, col, panel);
+ } else {
+ table.setWidget(row, col, new Image(Gerrit.RESOURCES.redNot()));
+ }
+ } else if (label.approved() != null) {
+ user = label.approved().name();
+ if (displayName && user != null) {
+ FlowPanel panel = new FlowPanel();
+ panel.add(new Image(Gerrit.RESOURCES.greenCheck()));
+ panel.add(new InlineLabel(user));
+ table.setWidget(row, col, panel);
+ } else {
+ table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
+ }
+ } else if (label.disliked() != null) {
+ user = label.disliked().name();
+ String vstr = String.valueOf(label._value());
+ if (displayName && user != null) {
+ vstr = vstr + " " + user;
+ }
+ fmt.addStyleName(row, col, Gerrit.RESOURCES.css().negscore());
+ table.setText(row, col, vstr);
+ } else if (label.recommended() != null) {
+ user = label.recommended().name();
+ String vstr = "+" + label._value();
+ if (displayName && user != null) {
+ vstr = vstr + " " + user;
+ }
+ fmt.addStyleName(row, col, Gerrit.RESOURCES.css().posscore());
+ table.setText(row, col, vstr);
+ } else {
+ table.clearCell(row, col);
+ continue;
+ }
+ fmt.addStyleName(row, col, Gerrit.RESOURCES.css().singleLine());
+
+ if (!displayName && user != null) {
+ // Some web browsers ignore the embedded newline; some like it;
+ // so we include a space before the newline to accommodate both.
+ fmt.getElement(row, col).setTitle(name + " \nby " + user);
+ }
+ }
+
+ // TODO(sop): Highlight changes I haven't reviewed on my dashboard.
+ // final Element tr = DOM.getParent(fmt.getElement(row, 0));
+ // UIObject.setStyleName(tr, Gerrit.RESOURCES.css().needsReview(),
+ // !haveReview && highlightUnreviewed);
+
+ setRowItem(row, c);
+ }
+
+ public void addSection(final Section s) {
+ assert s.parent == null;
+
+ if (s.titleText != null) {
+ s.titleRow = table.getRowCount();
+ table.setText(s.titleRow, 0, s.titleText);
+ final FlexCellFormatter fmt = table.getFlexCellFormatter();
+ fmt.setColSpan(s.titleRow, 0, columns);
+ fmt.addStyleName(s.titleRow, 0, Gerrit.RESOURCES.css().sectionHeader());
+ } else {
+ s.titleRow = -1;
+ }
+
+ s.parent = this;
+ s.dataBegin = table.getRowCount();
+ insertNoneRow(s.dataBegin);
+ sections.add(s);
+ }
+
+ private int insertRow(final int beforeRow) {
+ for (final Section s : sections) {
+ if (beforeRow <= s.titleRow) {
+ s.titleRow++;
+ }
+ if (beforeRow < s.dataBegin) {
+ s.dataBegin++;
+ }
+ }
+ return table.insertRow(beforeRow);
+ }
+
+ private void removeRow(final int row) {
+ for (final Section s : sections) {
+ if (row < s.titleRow) {
+ s.titleRow--;
+ }
+ if (row < s.dataBegin) {
+ s.dataBegin--;
+ }
+ }
+ table.removeRow(row);
+ }
+
+ public class StarKeyCommand extends NeedsSignInKeyCommand {
+ public StarKeyCommand(int mask, char key, String help) {
+ super(mask, key, help);
+ }
+
+ @Override
+ public void onKeyPress(final KeyPressEvent event) {
+ int row = getCurrentRow();
+ ChangeInfo c = getRowItem(row);
+ if (c != null && Gerrit.isSignedIn()) {
+ ((StarredChanges.Icon) table.getWidget(row, C_STAR)).toggleStar();
+ }
+ }
+ }
+
+ private final class TableChangeLink extends ChangeLink {
+ private TableChangeLink(final String text, final ChangeInfo c) {
+ super(text, c.legacy_id());
+ }
+
+ @Override
+ public void go() {
+ movePointerTo(cid);
+ super.go();
+ }
+ }
+
+ public static class Section {
+ ChangeTable2 parent;
+ String titleText;
+ int titleRow = -1;
+ int dataBegin;
+ int rows;
+
+ public void setTitleText(final String text) {
+ titleText = text;
+ if (titleRow >= 0) {
+ parent.table.setText(titleRow, 0, titleText);
+ }
+ }
+
+ public void display(ChangeList changeList) {
+ final int sz = changeList != null ? changeList.size() : 0;
+ final boolean hadData = rows > 0;
+
+ if (hadData) {
+ while (sz < rows) {
+ parent.removeRow(dataBegin);
+ rows--;
+ }
+ } else {
+ parent.removeRow(dataBegin);
+ }
+
+ if (sz == 0) {
+ parent.insertNoneRow(dataBegin);
+ return;
+ }
+
+ while (rows < sz) {
+ parent.insertChangeRow(dataBegin + rows);
+ rows++;
+ }
+ for (int i = 0; i < sz; i++) {
+ parent.populateChangeRow(dataBegin + i, changeList.get(i));
+ }
+ }
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
new file mode 100644
index 0000000..c9f1dc6
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
@@ -0,0 +1,112 @@
+// 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.client.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.NativeList;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.Screen;
+import com.google.gwt.http.client.URL;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CustomDashboardScreen extends Screen implements ChangeListScreen {
+ private String title;
+ private List<String> titles;
+ private List<String> queries;
+ private ChangeTable2 table;
+ private List<ChangeTable2.Section> sections;
+
+ public CustomDashboardScreen(String params) {
+ titles = new ArrayList<String>();
+ queries = new ArrayList<String>();
+ for (String kvPair : params.split("[,;&]")) {
+ String[] kv = kvPair.split("=", 2);
+ if (kv.length != 2 || kv[0].isEmpty()) {
+ continue;
+ }
+
+ if ("title".equals(kv[0])) {
+ title = URL.decodeQueryString(kv[1]);
+ } else {
+ titles.add(URL.decodeQueryString(kv[0]));
+ queries.add(URL.decodeQueryString(kv[1]));
+ }
+ }
+ }
+
+ @Override
+ protected void onInitUI() {
+ super.onInitUI();
+
+ if (title != null) {
+ setWindowTitle(title);
+ setPageTitle(title);
+ }
+
+ table = new ChangeTable2();
+ table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
+
+ sections = new ArrayList<ChangeTable2.Section>();
+ for (String title : titles) {
+ ChangeTable2.Section s = new ChangeTable2.Section();
+ s.setTitleText(title);
+ table.addSection(s);
+ sections.add(s);
+ }
+ add(table);
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+
+ if (queries.isEmpty()) {
+ display();
+ } else if (queries.size() == 1) {
+ ChangeList.next(queries.get(0),
+ 0, PagedSingleListScreen.MAX_SORTKEY,
+ new ScreenLoadCallback<ChangeList>(this) {
+ @Override
+ protected void preDisplay(ChangeList result) {
+ table.updateColumnsForLabels(result);
+ sections.get(0).display(result);
+ table.finishDisplay();
+ }
+ });
+ } else {
+ ChangeList.query(
+ new ScreenLoadCallback<NativeList<ChangeList>>(this) {
+ @Override
+ protected void preDisplay(NativeList<ChangeList> result) {
+ table.updateColumnsForLabels(
+ result.asList().toArray(new ChangeList[result.size()]));
+ for (int i = 0; i < result.size(); i++) {
+ sections.get(i).display(result.get(i));
+ }
+ table.finishDisplay();
+ }
+ },
+ queries.toArray(new String[queries.size()]));
+ }
+ }
+
+ @Override
+ public void registerKeys() {
+ super.registerKeys();
+ table.setRegisterKeys(true);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
index 72300b2..23ce178 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
@@ -15,12 +15,9 @@
package com.google.gerrit.client.changes;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeTable.ApprovalViewType;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.Hyperlink;
import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.SingleListChangeInfo;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.user.client.History;
@@ -28,19 +25,16 @@
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwtexpui.globalkey.client.KeyCommand;
-import java.util.List;
-
-
public abstract class PagedSingleListScreen extends Screen {
protected static final String MIN_SORTKEY = "";
protected static final String MAX_SORTKEY = "z";
protected final int pageSize;
- private ChangeTable table;
- private ChangeTable.Section section;
+ private ChangeTable2 table;
+ private ChangeTable2.Section section;
protected Hyperlink prev;
protected Hyperlink next;
- protected List<ChangeInfo> changes;
+ protected ChangeList changes;
protected final String anchorPrefix;
protected boolean useLoadPrev;
@@ -71,7 +65,7 @@
next = new Hyperlink(Util.C.pagedChangeListNext(), true, "");
next.setVisible(false);
- table = new ChangeTable(true) {
+ table = new ChangeTable2() {
{
keysNavigation.add(new DoLinkCommand(0, 'p', Util.C
.changeTablePagePrev(), prev));
@@ -79,8 +73,7 @@
.changeTablePageNext(), next));
}
};
- section = new ChangeTable.Section(null, ApprovalViewType.STRONGEST, null);
-
+ section = new ChangeTable2.Section();
table.addSection(section);
table.setSavePointerId(anchorPrefix);
add(table);
@@ -112,36 +105,34 @@
protected abstract void loadNext();
- protected AsyncCallback<SingleListChangeInfo> loadCallback() {
- return new ScreenLoadCallback<SingleListChangeInfo>(this) {
+ protected AsyncCallback<ChangeList> loadCallback() {
+ return new ScreenLoadCallback<ChangeList>(this) {
@Override
- protected void preDisplay(final SingleListChangeInfo result) {
+ protected void preDisplay(ChangeList result) {
display(result);
}
};
}
- protected void display(final SingleListChangeInfo result) {
- changes = result.getChanges();
-
+ protected void display(final ChangeList result) {
+ changes = result;
if (!changes.isEmpty()) {
final ChangeInfo f = changes.get(0);
final ChangeInfo l = changes.get(changes.size() - 1);
- prev.setTargetHistoryToken(anchorPrefix + ",p," + f.getSortKey());
- next.setTargetHistoryToken(anchorPrefix + ",n," + l.getSortKey());
+ prev.setTargetHistoryToken(anchorPrefix + ",p," + f._sortkey());
+ next.setTargetHistoryToken(anchorPrefix + ",n," + l._sortkey());
if (useLoadPrev) {
- prev.setVisible(!result.isAtEnd());
+ prev.setVisible(f._more_changes());
next.setVisible(!MIN_SORTKEY.equals(pos));
} else {
prev.setVisible(!MAX_SORTKEY.equals(pos));
- next.setVisible(!result.isAtEnd());
+ next.setVisible(l._more_changes());
}
}
-
- table.setAccountInfoCache(result.getAccounts());
- section.display(result.getChanges());
+ table.updateColumnsForLabels(result);
+ section.display(result);
table.finishDisplay();
}
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..00baf28 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,16 +20,16 @@
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;
import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Patch;
@@ -232,9 +232,8 @@
.anonymousDownload("HTTP"), r.toString()));
}
- if (Gerrit.getConfig().getSshdAddress() != null && Gerrit.isSignedIn()
- && Gerrit.getUserAccount().getUserName() != null
- && Gerrit.getUserAccount().getUserName().length() > 0
+ if (Gerrit.getConfig().getSshdAddress() != null
+ && hasUserName()
&& (allowedSchemes.contains(DownloadScheme.SSH) ||
allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
String sshAddr = Gerrit.getConfig().getSshdAddress();
@@ -256,13 +255,12 @@
urls.add(new DownloadUrlLink(DownloadScheme.SSH, "SSH", r.toString()));
}
- if (Gerrit.isSignedIn() && Gerrit.getUserAccount().getUserName() != null
- && Gerrit.getUserAccount().getUserName().length() > 0
- && (allowedSchemes.contains(DownloadScheme.HTTP) ||
- allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
+ if ((hasUserName() || siteReliesOnHttp())
+ && (allowedSchemes.contains(DownloadScheme.HTTP)
+ || allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
final StringBuilder r = new StringBuilder();
if (Gerrit.getConfig().getGitHttpUrl() != null
- && changeDetail.isAllowsAnonymous()) {
+ && (changeDetail.isAllowsAnonymous() || siteReliesOnHttp())) {
r.append(Gerrit.getConfig().getGitHttpUrl());
} else {
String base = hostPageUrl;
@@ -372,6 +370,18 @@
infoTable.setWidget(R_DOWNLOAD, 1, fp);
}
+ private static boolean siteReliesOnHttp() {
+ return Gerrit.getConfig().getGitHttpUrl() != null
+ && Gerrit.getConfig().getAuthType() == AuthType.CUSTOM_EXTENSION
+ && !Gerrit.getConfig().siteHasUsernames();
+ }
+
+ private static boolean hasUserName() {
+ return Gerrit.isSignedIn()
+ && Gerrit.getUserAccount().getUserName() != null
+ && Gerrit.getUserAccount().getUserName().length() > 0;
+ }
+
private void displayUserIdentity(final int row, final UserIdentity who) {
if (who == null) {
infoTable.clearCell(row, 1);
@@ -381,9 +391,9 @@
final FlowPanel fp = new FlowPanel();
fp.setStyleName(Gerrit.RESOURCES.css().patchSetUserIdentity());
if (who.getName() != null) {
- final Account.Id aId = who.getAccount();
- if (aId != null) {
- fp.add(new AccountDashboardLink(who.getName(), aId));
+ if (who.getAccount() != null) {
+ 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 +447,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 +615,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 +628,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 +636,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/changes/PatchSetsBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
index 7e659a1..f206d6e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
@@ -79,7 +79,7 @@
if (Gerrit.isSignedIn()) {
final AccountGeneralPreferences p =
Gerrit.getUserAccount().getGeneralPreferences();
- if (p.isDisplayPatchSetsInReverseOrder()) {
+ if (p.isReversePatchSetOrder()) {
Collections.reverse(patchSets);
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
index cf9c526..b94fcae 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
@@ -17,13 +17,11 @@
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.SingleListChangeInfo;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtorm.client.KeyUtil;
-
public class QueryScreen extends PagedSingleListScreen implements
ChangeListScreen {
public static QueryScreen forQuery(String query) {
@@ -49,13 +47,15 @@
}
@Override
- protected AsyncCallback<SingleListChangeInfo> loadCallback() {
- return new GerritCallback<SingleListChangeInfo>() {
- public final void onSuccess(final SingleListChangeInfo result) {
+ protected AsyncCallback<ChangeList> loadCallback() {
+ return new GerritCallback<ChangeList>() {
+ @Override
+ public final void onSuccess(ChangeList result) {
if (isAttached()) {
- if (result.getChanges().size() == 1 && isSingleQuery(query)) {
- final ChangeInfo c = result.getChanges().get(0);
- Gerrit.display(PageLinks.toChange(c), new ChangeScreen(c));
+ if (result.size() == 1 && isSingleQuery(query)) {
+ ChangeInfo c = result.get(0);
+ Change.Id id = c.legacy_id();
+ Gerrit.display(PageLinks.toChange(id), new ChangeScreen(id));
} else {
Gerrit.setQueryString(query);
display(result);
@@ -68,12 +68,12 @@
@Override
protected void loadPrev() {
- Util.LIST_SVC.allQueryPrev(query, pos, pageSize, loadCallback());
+ ChangeList.prev(query, pageSize, pos, loadCallback());
}
@Override
protected void loadNext() {
- Util.LIST_SVC.allQueryNext(query, pos, pageSize, loadCallback());
+ ChangeList.next(query, pageSize, pos, loadCallback());
}
private static boolean isSingleQuery(String query) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java
deleted file mode 100644
index d7624a6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
-import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.ToggleStarRequest;
-import com.google.gerrit.reviewdb.client.Change;
-
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.GwtEvent;
-import com.google.gwt.event.shared.HandlerManager;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.resources.client.ImageResource;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwtjsonrpc.common.VoidResult;
-
-public class StarCache implements HasValueChangeHandlers<Boolean> {
- public class KeyCommand extends NeedsSignInKeyCommand {
- public KeyCommand(int mask, char key, String help) {
- super(mask, key, help);
- }
-
- @Override
- public void onKeyPress(final KeyPressEvent event) {
- StarCache.this.toggleStar();
- }
- }
-
- ChangeCache cache;
-
- private HandlerManager manager = new HandlerManager(this);
-
- public StarCache(final Change.Id chg) {
- cache = ChangeCache.get(chg);
- }
-
- public boolean get() {
- ChangeDetail detail = cache.getChangeDetailCache().get();
- if (detail != null) {
- return detail.isStarred();
- }
- ChangeInfo info = cache.getChangeInfoCache().get();
- if (info != null) {
- return info.isStarred();
- }
- return false;
- }
-
- public void set(final boolean s) {
- if (Gerrit.isSignedIn() && s != get()) {
- final ToggleStarRequest req = new ToggleStarRequest();
- req.toggle(cache.getChangeId(), s);
-
- Util.LIST_SVC.toggleStars(req, new GerritCallback<VoidResult>() {
- public void onSuccess(final VoidResult result) {
- setStarred(s);
- fireEvent(new ValueChangeEvent<Boolean>(s){});
- }
- });
- }
- }
-
- private void setStarred(final boolean s) {
- ChangeDetail detail = cache.getChangeDetailCache().get();
- if (detail != null) {
- detail.setStarred(s);
- }
- ChangeInfo info = cache.getChangeInfoCache().get();
- if (info != null) {
- info.setStarred(s);
- }
- }
-
- public void toggleStar() {
- set(!get());
- }
-
- @SuppressWarnings("unchecked")
- public Image createStar() {
- final Image star = new Image(getResource());
- star.setVisible(Gerrit.isSignedIn());
-
- star.addClickHandler(new ClickHandler() {
- @Override
- public void onClick(final ClickEvent event) {
- StarCache.this.toggleStar();
- }
- });
-
- @SuppressWarnings("rawtypes")
- ValueChangeHandler starUpdater = new ValueChangeHandler() {
- @Override
- public void onValueChange(ValueChangeEvent event) {
- star.setResource(StarCache.this.getResource());
- }
- };
-
- cache.getChangeDetailCache().addValueChangeHandler(starUpdater);
- cache.getChangeInfoCache().addValueChangeHandler(starUpdater);
-
- this.addValueChangeHandler(starUpdater);
-
- return star;
- }
-
- private ImageResource getResource() {
- return get() ? Gerrit.RESOURCES.starFilled() : Gerrit.RESOURCES.starOpen();
- }
-
- public void fireEvent(GwtEvent<?> event) {
- manager.fireEvent(event);
- }
-
- public HandlerRegistration addValueChangeHandler(
- ValueChangeHandler<Boolean> handler) {
- return manager.addHandler(ValueChangeEvent.getType(), handler);
- }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
new file mode 100644
index 0000000..8b5aa1c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
@@ -0,0 +1,216 @@
+// 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.client.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.data.ToggleStarRequest;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.SimpleEventBus;
+import com.google.gwt.resources.client.ImageResource;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.web.bindery.event.shared.Event;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+
+/** Supports the star icon displayed on changes and tracking the status. */
+public class StarredChanges {
+ private static final EventBus eventBus = new SimpleEventBus();
+ private static final Event.Type<ChangeStarHandler> TYPE =
+ new Event.Type<ChangeStarHandler>();
+
+ /** Handler that can receive notifications of a change's starred status. */
+ public static interface ChangeStarHandler {
+ public void onChangeStar(ChangeStarEvent event);
+ }
+
+ /** Event fired when a star changes status. The new status is reported. */
+ public static class ChangeStarEvent extends Event<ChangeStarHandler> {
+ private boolean starred;
+
+ public ChangeStarEvent(Change.Id source, boolean starred) {
+ setSource(source);
+ this.starred = starred;
+ }
+
+ public boolean isStarred() {
+ return starred;
+ }
+
+ @Override
+ public Type<ChangeStarHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(ChangeStarHandler handler) {
+ handler.onChangeStar(this);
+ }
+ }
+
+ /**
+ * Create a star icon for the given change, and current status. Returns null
+ * if the user is not signed in and cannot support starred changes.
+ */
+ public static Icon createIcon(Change.Id source, boolean starred) {
+ return Gerrit.isSignedIn() ? new Icon(source, starred) : null;
+ }
+
+ /** Make a key command that toggles the star for a change. */
+ public static KeyCommand newKeyCommand(final Icon icon) {
+ return new KeyCommand(0, 's', Util.C.changeTableStar()) {
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ icon.toggleStar();
+ }
+ };
+ }
+
+ /** Add a handler to listen for starred status to change. */
+ public static HandlerRegistration addHandler(
+ Change.Id source,
+ ChangeStarHandler handler) {
+ return eventBus.addHandlerToSource(TYPE, source, handler);
+ }
+
+ /**
+ * Broadcast the current starred value of a change to UI widgets. This does
+ * not RPC to the server and does not alter the starred status of a change.
+ */
+ public static void fireChangeStarEvent(Change.Id id, boolean starred) {
+ eventBus.fireEventFromSource(
+ new ChangeStarEvent(id, starred),
+ id);
+ }
+
+ /**
+ * Set the starred status of a change. This method broadcasts to all
+ * interested UI widgets and sends an RPC to the server to record the
+ * updated status.
+ */
+ public static void toggleStar(
+ final Change.Id changeId,
+ final boolean newValue) {
+ if (next == null) {
+ next = new ToggleStarRequest();
+ }
+ next.toggle(changeId, newValue);
+ fireChangeStarEvent(changeId, newValue);
+ if (!busy) {
+ start();
+ }
+ }
+
+ private static ToggleStarRequest next;
+ private static boolean busy;
+
+ private static void start() {
+ final ToggleStarRequest req = next;
+ next = null;
+ busy = true;
+
+ Util.LIST_SVC.toggleStars(req, new GerritCallback<VoidResult>() {
+ @Override
+ public void onSuccess(VoidResult result) {
+ if (next != null) {
+ start();
+ } else {
+ busy = false;
+ }
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ rollback(req);
+ if (next != null) {
+ rollback(next);
+ next = null;
+ }
+ busy = false;
+ super.onFailure(caught);
+ }
+ });
+ }
+
+ private static void rollback(ToggleStarRequest req) {
+ if (req.getAddSet() != null) {
+ for (Change.Id id : req.getAddSet()) {
+ fireChangeStarEvent(id, false);
+ }
+ }
+ if (req.getRemoveSet() != null) {
+ for (Change.Id id : req.getRemoveSet()) {
+ fireChangeStarEvent(id, true);
+ }
+ }
+ }
+
+ public static class Icon extends Image
+ implements ChangeStarHandler, ClickHandler {
+ private final Change.Id changeId;
+ private boolean starred;
+ private HandlerRegistration handler;
+
+ Icon(Change.Id changeId, boolean starred) {
+ super(resource(starred));
+ this.changeId = changeId;
+ this.starred = starred;
+ addClickHandler(this);
+ }
+
+ /**
+ * Toggles the state of the star, as if the user clicked on the image. This
+ * will broadcast the new star status to all interested UI widgets, and RPC
+ * to the server to store the changed value.
+ */
+ public void toggleStar() {
+ StarredChanges.toggleStar(changeId, !starred);
+ }
+
+ @Override
+ protected void onLoad() {
+ handler = StarredChanges.addHandler(changeId, this);
+ }
+
+ @Override
+ protected void onUnload() {
+ handler.removeHandler();
+ handler = null;
+ }
+
+ @Override
+ public void onChangeStar(ChangeStarEvent event) {
+ setResource(resource(event.isStarred()));
+ starred = event.isStarred();
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ toggleStar();
+ }
+
+ private static ImageResource resource(boolean starred) {
+ return starred ? Gerrit.RESOURCES.starFilled() : Gerrit.RESOURCES.starOpen();
+ }
+ }
+
+ private StarredChanges() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index 1081e47..f588044 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -375,6 +375,18 @@
width: 100%;
margin-top: 15px;
}
+.errorDialogText {
+ font-size: 15px;
+ font-family: verdana;
+}
+.errorDialog a,
+.errorDialog a:visited,
+.errorDialog a:hover {
+ color: white;
+ font-weight: bold;
+ font-size: 15px;
+ font-family: verdana;
+}
/** Screen **/
@@ -662,11 +674,17 @@
white-space: pre;
width: 3.5em;
text-align: right;
- border-right: thin solid #b0bdcc;
padding-right: 0.2em;
background: white;
border-bottom: 1px solid white;
}
+.lineNumber.rightmost {
+ border-left: thin solid #b0bdcc;
+}
+.lineNumber a {
+ color: #888;
+ text-decoration: none;
+}
.patchContentTable td.fileColumnHeader {
background: trimColor;
font-family: norm-font;
@@ -689,6 +707,7 @@
padding-left: 0;
padding-right: 0;
white-space: pre;
+ border-left: thin solid #b0bdcc;
}
.fileLineNone {
background: #eeeeee;
@@ -723,11 +742,21 @@
font-family: norm-font;
text-align: center;
font-style: italic;
- background: lightblue;
+ background: #def;
+ color: grey;
}
.patchContentTable .skipLine div {
display: inline;
}
+.patchContentTable a.skipLine {
+ color: grey;
+ text-decoration: none;
+}
+.patchContentTable a:hover.skipLine {
+ background: white;
+ color: #00A;
+ text-decoration: underline;
+}
.patchContentTable .activeRow .iconCell,
.patchContentTable .activeRow .lineNumber {
@@ -1082,6 +1111,9 @@
.accountInfoBlock {
margin-bottom: 10px;
}
+.accountInfoBlock .gwt-Button {
+ margin-left: 10px;
+}
.accountContactPrivacyDetails {
margin-left: 10px;
margin-top: 5px;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index 8282caa..24a2ae5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
@@ -516,22 +516,6 @@
Gerrit.RESOURCES.css().iconCell());
}
- protected void addStyle(final int row, final int col, final String style) {
- table.getCellFormatter().addStyleName(row, col, style);
- }
-
- protected void removeRow(final int row) {
- table.removeRow(row);
- }
-
- protected void setHtml(final int row, final int col, final String html) {
- table.setHTML(row, col, html);
- }
-
- protected void setWidget(final int row, final int col, final Widget widget) {
- table.setWidget(row, col, widget);
- }
-
@Override
protected void onOpenRow(final int row) {
final Object item = getRowItem(row);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
index 6a1dbc1..23090a2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
@@ -48,5 +48,5 @@
fileTypeSymlink = Type: Symbolic Link
fileTypeGitlink = Type: Git Commit in Subproject
-patchSkipRegionStart = (... skipping
-patchSkipRegionEnd = common lines ...)
+patchSkipRegionStart = ... skipped
+patchSkipRegionEnd = common lines ...
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties
index 52dcba2..2d01e24 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties
@@ -1,4 +1,3 @@
-expandBefore = Expand {0} before
-expandAfter = Expand {0} after
-
+expandBefore = +{0}⇧
+expandAfter = +{0}⇩
draftSaved = Draft saved at {0,time,short}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties
index 58c4f6c..2f3c20a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties
@@ -1,2 +1,2 @@
-expandBefore = Expand {0} before
-expandAfter = Expand {0} after
+expandBefore = +{0}⇧
+expandAfter = +{0}⇩
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
index 6379e23..964ba4b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
@@ -26,16 +26,16 @@
import com.google.gerrit.prettify.common.EditList;
import com.google.gerrit.prettify.common.SparseHtmlFile;
import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLTable.Cell;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.HasVerticalAlignment;
+import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
import com.google.gwtorm.client.KeyUtil;
@@ -47,9 +47,8 @@
import java.util.List;
public class SideBySideTable extends AbstractPatchContentTable {
- private static final int COL_A = 2;
- private static final int COL_B = 4;
-
+ private static final int A = 2;
+ private static final int B = 3;
private static final int NUM_ROWS_TO_EXPAND = 10;
private SparseHtmlFile a;
@@ -59,24 +58,17 @@
protected void onCellDoubleClick(final int row, int column) {
if (column > 0 && getRowItem(row) instanceof PatchLine) {
final PatchLine line = (PatchLine) getRowItem(row);
- final short file = (short) ((column - 1) / 2);
- if (column < (1 + file * 2 + 1)) {
- column++;
- }
- switch (file) {
- case 0:
- createCommentEditor(row + 1, column, line.getLineA(), file);
- break;
- case 1:
- createCommentEditor(row + 1, column, line.getLineB(), file);
- break;
+ if (column == 1 || column == A) {
+ createCommentEditor(row + 1, A, line.getLineA(), (short) 0);
+ } else if (column == B || column == 4) {
+ createCommentEditor(row + 1, B, line.getLineB(), (short) 1);
}
}
}
@Override
protected void onCellSingleClick(int row, int column) {
- if (column == 1 || column == 3) {
+ if (column == 1 || column == 4) {
onCellDoubleClick(row, column);
}
}
@@ -84,7 +76,7 @@
@Override
protected void onInsertComment(final PatchLine line) {
final int row = getCurrentRow();
- createCommentEditor(row + 1, 4, line.getLineB(), (short) 1);
+ createCommentEditor(row + 1, B, line.getLineB(), (short) 1);
}
@Override
@@ -100,7 +92,8 @@
appendHeader(script, nc);
lines.add(null);
- if(script.getFileModeA()!=FileMode.FILE||script.getFileModeB()!=FileMode.FILE){
+ if (script.getFileModeA() != FileMode.FILE
+ || script.getFileModeB() != FileMode.FILE) {
openLine(nc);
appendModeLine(nc, script.getFileModeA());
appendModeLine(nc, script.getFileModeB());
@@ -121,13 +114,14 @@
if (hunk.isContextLine()) {
openLine(nc);
final SafeHtml ctx = a.getSafeHtmlLine(hunk.getCurA());
- appendLineText(nc, hunk.getCurA(), CONTEXT, ctx, false, false);
+ appendLineNumber(nc, hunk.getCurA(), false);
+ appendLineText(nc, CONTEXT, ctx, false, false);
if (ignoreWS && b.contains(hunk.getCurB())) {
- appendLineText(nc, hunk.getCurB(), CONTEXT, b, hunk.getCurB(),
- false);
+ appendLineText(nc, CONTEXT, b, hunk.getCurB(), false);
} else {
- appendLineText(nc, hunk.getCurB(), CONTEXT, ctx, false, false);
+ appendLineText(nc, CONTEXT, ctx, false, false);
}
+ appendLineNumber(nc, hunk.getCurB(), true);
closeLine(nc);
hunk.incBoth();
lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
@@ -140,21 +134,27 @@
openLine(nc);
if (del) {
- appendLineText(nc, hunk.getCurA(), DELETE, a, hunk.getCurA(), full);
+ appendLineNumber(nc, hunk.getCurA(), false);
+ appendLineText(nc, DELETE, a, hunk.getCurA(), full);
hunk.incA();
} else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
+ appendLineNumber(nc, false);
appendLineNone(nc, DELETE);
} else {
+ appendLineNumber(nc, false);
appendLineNone(nc, CONTEXT);
}
if (ins) {
- appendLineText(nc, hunk.getCurB(), INSERT, b, hunk.getCurB(), full);
+ appendLineText(nc, INSERT, b, hunk.getCurB(), full);
+ appendLineNumber(nc, hunk.getCurB(), true);
hunk.incB();
} else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
appendLineNone(nc, INSERT);
+ appendLineNumber(nc, true);
} else {
appendLineNone(nc, CONTEXT);
+ appendLineNumber(nc, true);
}
closeLine(nc);
@@ -229,13 +229,13 @@
final PatchLineComment ac = ai.next();
final PatchLineComment bc = bi.next();
insertRow(row);
- bindComment(row, COL_A, ac, !ai.hasNext(), expandComments);
- bindComment(row, COL_B, bc, !bi.hasNext(), expandComments);
+ bindComment(row, A, ac, !ai.hasNext(), expandComments);
+ bindComment(row, B, bc, !bi.hasNext(), expandComments);
row++;
}
- row = finish(ai, row, COL_A, expandComments);
- row = finish(bi, row, COL_B, expandComments);
+ row = finish(ai, row, A, expandComments);
+ row = finish(bi, row, B, expandComments);
} else {
row++;
}
@@ -246,10 +246,10 @@
protected void insertRow(final int row) {
super.insertRow(row);
final CellFormatter fmt = table.getCellFormatter();
- fmt.addStyleName(row, COL_A - 1, Gerrit.RESOURCES.css().lineNumber());
- fmt.addStyleName(row, COL_A, Gerrit.RESOURCES.css().diffText());
- fmt.addStyleName(row, COL_B - 1, Gerrit.RESOURCES.css().lineNumber());
- fmt.addStyleName(row, COL_B, Gerrit.RESOURCES.css().diffText());
+ fmt.addStyleName(row, A - 1, Gerrit.RESOURCES.css().lineNumber());
+ fmt.addStyleName(row, A, Gerrit.RESOURCES.css().diffText());
+ fmt.addStyleName(row, B, Gerrit.RESOURCES.css().diffText());
+ fmt.addStyleName(row, B + 1, Gerrit.RESOURCES.css().lineNumber());
}
private int finish(final Iterator<PatchLineComment> i, int row, final int col, boolean expandComment) {
@@ -263,6 +263,8 @@
}
private void appendHeader(PatchScript script, final SafeHtmlBuilder m) {
+ boolean isCommitMessage = Patch.COMMIT_MSG.equals(script.getNewName());
+
m.openTr();
m.openTd();
@@ -284,12 +286,26 @@
} else {
m.append(PatchUtil.C.patchHeaderOld());
}
- m.br();
- if (0 < script.getA().size()) {
- if (idSideA == null) {
- downloadLink(m, patchKey, "1");
- } else {
- downloadLink(m, new Patch.Key(idSideA, patchKey.get()), "0");
+ if (!isCommitMessage) {
+ m.br();
+ if (0 < script.getA().size()) {
+ if (idSideA == null) {
+ downloadLink(m, patchKey, "1");
+ } else {
+ downloadLink(m, new Patch.Key(idSideA, patchKey.get()), "0");
+ }
+ }
+ }
+ m.closeTd();
+
+ m.openTd();
+ m.setStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
+ m.setAttribute("width", "50%");
+ m.append(PatchUtil.C.patchHeaderNew());
+ if (!isCommitMessage) {
+ m.br();
+ if (0 < script.getB().size()) {
+ downloadLink(m, new Patch.Key(idSideB, patchKey.get()), "0");
}
}
m.closeTd();
@@ -299,16 +315,6 @@
m.addStyleName(Gerrit.RESOURCES.css().lineNumber());
m.closeTd();
- m.openTd();
- m.setStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
- m.setAttribute("width", "50%");
- m.append(PatchUtil.C.patchHeaderNew());
- m.br();
- if (0 < script.getB().size()) {
- downloadLink(m, new Patch.Key(idSideB, patchKey.get()), "0");
- }
- m.closeTd();
-
m.closeTr();
}
@@ -336,21 +342,21 @@
m.closeTr();
}
- ClickHandler expandAllListener = new ClickHandler() {
+ private ClickHandler expandAllListener = new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
expand(event, 0);
}
};
- ClickHandler expandBeforeListener = new ClickHandler() {
+ private ClickHandler expandBeforeListener = new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
expand(event, NUM_ROWS_TO_EXPAND);
}
};
- ClickHandler expandAfterListener = new ClickHandler() {
+ private ClickHandler expandAfterListener = new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
expand(event, -NUM_ROWS_TO_EXPAND);
@@ -358,11 +364,11 @@
};
private void expand(ClickEvent event, final int numRows) {
- Cell cell = table.getCellForEvent(event);
- int row = cell.getRowIndex();
+ int row = table.getCellForEvent(event).getRowIndex();
if (!(getRowItem(row) instanceof SkippedLine)) {
return;
}
+
SkippedLine line = (SkippedLine) getRowItem(row);
int loopTo = numRows;
if (numRows == 0) {
@@ -374,29 +380,34 @@
if (numRows < 0) {
offset = 1;
}
+
+ CellFormatter fmt = table.getCellFormatter();
for (int i = 0 + offset; i < loopTo + offset; i++) {
- insertRow(row + i);
+ // The overridden version of insertRow adds some css classes we don't
+ // want.
+ super.insertRow(row + i);
+ table.getRowFormatter().setVerticalAlign(row + i,
+ HasVerticalAlignment.ALIGN_TOP);
int lineA = line.getStartA() + i;
int lineB = line.getStartB() + i;
if (numRows < 0) {
lineA = line.getStartA() + line.getSize() + numRows + i - offset;
lineB = line.getStartB() + line.getSize() + numRows + i - offset;
}
- setHtml(row + i, 1, "<a href=\"javascript:void(0)\">" + (lineA + 1)
- + "</a>");
- addStyle(row + i, 1, Gerrit.RESOURCES.css().lineNumber());
- setHtml(row + i, 2, a.getSafeHtmlLine(lineA).asString());
- addStyle(row + i, 2, Gerrit.RESOURCES.css().fileLine());
- addStyle(row + i, 2, Gerrit.RESOURCES.css().fileLineCONTEXT());
+ table.setHTML(row + i, A - 1, "<a href=\"javascript:;\">" + (lineA + 1) + "</a>");
+ fmt.addStyleName(row + i, A - 1, Gerrit.RESOURCES.css().lineNumber());
- setHtml(row + i, 3, "<a href=\"javascript:void(0)\">" + (lineB + 1)
- + "</a>");
- addStyle(row + i, 3, Gerrit.RESOURCES.css().lineNumber());
+ table.setHTML(row + i, A, a.getSafeHtmlLine(lineA).asString());
+ fmt.addStyleName(row + i, A, Gerrit.RESOURCES.css().fileLine());
+ fmt.addStyleName(row + i, A, Gerrit.RESOURCES.css().fileLineCONTEXT());
- setHtml(row + i, 4, b.getSafeHtmlLine(lineB).asString());
- addStyle(row + i, 4, Gerrit.RESOURCES.css().fileLine());
- addStyle(row + i, 4, Gerrit.RESOURCES.css().fileLineCONTEXT());
+ table.setHTML(row + i, B, b.getSafeHtmlLine(lineB).asString());
+ fmt.addStyleName(row + i, B, Gerrit.RESOURCES.css().fileLine());
+ fmt.addStyleName(row + i, B, Gerrit.RESOURCES.css().fileLineCONTEXT());
+
+ table.setHTML(row + i, B + 1, "<a href=\"javascript:;\">" + (lineB + 1) + "</a>");
+ fmt.addStyleName(row + i, B + 1, Gerrit.RESOURCES.css().lineNumber());
setRowItem(row + i, new PatchLine(CONTEXT, lineA, lineB));
}
@@ -408,34 +419,41 @@
line.reduceSize(-numRows);
createSkipLine(row, line);
} else {
- removeRow(row + loopTo);
+ table.removeRow(row + loopTo);
}
}
private void createSkipLine(int row, SkippedLine line) {
FlowPanel p = new FlowPanel();
- Label l1 = new Label(" " + PatchUtil.C.patchSkipRegionStart() + " ");
+ InlineLabel l1 = new InlineLabel(" " + PatchUtil.C.patchSkipRegionStart() + " ");
+ InlineLabel l2 = new InlineLabel(" " + PatchUtil.C.patchSkipRegionEnd() + " ");
+
Anchor all = new Anchor(String.valueOf(line.getSize()));
- Label l2 = new Label(" " + PatchUtil.C.patchSkipRegionEnd() + " ");
all.addClickHandler(expandAllListener);
+ all.setStyleName(Gerrit.RESOURCES.css().skipLine());
+
if (line.getSize() > 30) {
- // We only show the expand before & after links if we skip more than
- // 30 lines.
- Anchor before = new Anchor(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND));
- before.addClickHandler(expandBeforeListener);
- Anchor after = new Anchor(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND));
- after.addClickHandler(expandAfterListener);
- p.add(before);
+ // Only show the expand before/after if skipped more than 30 lines.
+ Anchor b = new Anchor(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND), true);
+ Anchor a = new Anchor(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND), true);
+
+ b.addClickHandler(expandBeforeListener);
+ a.addClickHandler(expandAfterListener);
+
+ b.setStyleName(Gerrit.RESOURCES.css().skipLine());
+ a.setStyleName(Gerrit.RESOURCES.css().skipLine());
+
+ p.add(b);
p.add(l1);
p.add(all);
p.add(l2);
- p.add(after);
+ p.add(a);
} else {
p.add(l1);
p.add(all);
p.add(l2);
}
- setWidget(row, 1, p);
+ table.setWidget(row, 1, p);
}
private void openLine(final SafeHtmlBuilder m) {
@@ -447,22 +465,34 @@
m.closeTd();
}
- private void appendLineText(final SafeHtmlBuilder m,
- final int lineNumberMinusOne, final PatchLine.Type type,
- final SparseHtmlFile src, final int i, final boolean fullBlock) {
- appendLineText(m, lineNumberMinusOne, type, //
- src.getSafeHtmlLine(i), src.hasTrailingEdit(i), fullBlock);
+ private void appendLineNumber(SafeHtmlBuilder m, boolean right) {
+ m.openTd();
+ m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
+ if (right) {
+ m.addStyleName(Gerrit.RESOURCES.css().rightmost());
+ }
+ m.closeTd();
+ }
+
+ private void appendLineNumber(SafeHtmlBuilder m, int lineNumberMinusOne, boolean right) {
+ m.openTd();
+ m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
+ if (right) {
+ m.addStyleName(Gerrit.RESOURCES.css().rightmost());
+ }
+ m.append(SafeHtml.asis("<a href=\"javascript:;\">"+ (lineNumberMinusOne + 1) + "</a>"));
+ m.closeTd();
}
private void appendLineText(final SafeHtmlBuilder m,
- final int lineNumberMinusOne, final PatchLine.Type type,
- final SafeHtml lineHtml, final boolean trailingEdit,
+ final PatchLine.Type type, final SparseHtmlFile src, final int i,
final boolean fullBlock) {
- m.openTd();
- m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
- m.append(SafeHtml.asis("<a href=\"javascript:void(0)\">"+ (lineNumberMinusOne + 1) + "</a>"));
- m.closeTd();
+ appendLineText(m, type, src.getSafeHtmlLine(i), src.hasTrailingEdit(i), fullBlock);
+ }
+ private void appendLineText(final SafeHtmlBuilder m,
+ final PatchLine.Type type, final SafeHtml lineHtml,
+ final boolean trailingEdit, final boolean fullBlock) {
m.openTd();
m.addStyleName(Gerrit.RESOURCES.css().fileLine());
switch (type) {
@@ -488,10 +518,6 @@
private void appendLineNone(final SafeHtmlBuilder m, final PatchLine.Type type) {
m.openTd();
- m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
- m.closeTd();
-
- m.openTd();
m.addStyleName(Gerrit.RESOURCES.css().fileLine());
switch (type != null ? type : PatchLine.Type.CONTEXT) {
case DELETE:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
new file mode 100644
index 0000000..80c1feb
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.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.client.projects;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.ui.SuggestOracle;
+
+public class ProjectInfo
+ extends JavaScriptObject
+ implements SuggestOracle.Suggestion {
+ public final Project.NameKey name_key() {
+ return new Project.NameKey(name());
+ }
+
+ public final native String name() /*-{ return this.name; }-*/;
+ public final native String description() /*-{ return this.description; }-*/;
+
+ @Override
+ public final String getDisplayString() {
+ if (description() != null) {
+ return name() + " (" + description() + ")";
+ }
+ return name();
+ }
+
+ @Override
+ public final String getReplacementString() {
+ return name();
+ }
+
+ protected ProjectInfo() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
new file mode 100644
index 0000000..55bb902
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
@@ -0,0 +1,50 @@
+// 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.client.projects;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.http.client.URL;
+
+/** Projects available from {@code /projects/}. */
+public class ProjectMap extends NativeMap<ProjectInfo> {
+ public static void all(AsyncCallback<ProjectMap> callback) {
+ new RestApi("/projects/")
+ .addParameterRaw("type", "ALL")
+ .addParameterTrue("all")
+ .addParameterTrue("d") // description
+ .send(NativeMap.copyKeysIntoChildren(callback));
+ }
+
+ public static void permissions(AsyncCallback<ProjectMap> callback) {
+ new RestApi("/projects/")
+ .addParameterRaw("type", "PERMISSIONS")
+ .addParameterTrue("all")
+ .addParameterTrue("d") // description
+ .send(NativeMap.copyKeysIntoChildren(callback));
+ }
+
+ public static void suggest(String prefix, int limit, AsyncCallback<ProjectMap> cb) {
+ new RestApi("/projects/" + URL.encode(prefix).replaceAll("[?]", "%3F"))
+ .addParameterRaw("type", "ALL")
+ .addParameter("n", limit)
+ .addParameterTrue("d") // description
+ .send(NativeMap.copyKeysIntoChildren(cb));
+ }
+
+ protected ProjectMap() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java
new file mode 100644
index 0000000..e820fe0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java
@@ -0,0 +1,55 @@
+// 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.client.rpc;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+import java.util.AbstractList;
+import java.util.List;
+
+/** A read-only list of native JavaScript objects stored in a JSON array. */
+public class NativeList<T extends JavaScriptObject> extends JavaScriptObject {
+ protected NativeList() {
+ }
+
+ public final List<T> asList() {
+ return new AbstractList<T>() {
+ @Override
+ public T set(int index, T element) {
+ T old = NativeList.this.get(index);
+ NativeList.this.set0(index, element);
+ return old;
+ }
+
+ @Override
+ public T get(int index) {
+ return NativeList.this.get(index);
+ }
+
+ @Override
+ public int size() {
+ return NativeList.this.size();
+ }
+ };
+ }
+
+ public final boolean isEmpty() {
+ return size() == 0;
+ }
+
+ public final native int size() /*-{ return this.length; }-*/;
+ public final native T get(int i) /*-{ return this[i]; }-*/;
+ private final native void set0(int i, T v) /*-{ this[i] = v; }-*/;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
new file mode 100644
index 0000000..cde9041
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
@@ -0,0 +1,93 @@
+// 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.client.rpc;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+
+import java.util.Set;
+
+/** A map of native JSON objects, keyed by a string. */
+public class NativeMap<T extends JavaScriptObject> extends JavaScriptObject {
+ /**
+ * Loop through the result map's entries and copy the key strings into the
+ * "name" property of the corresponding child object. This only runs on the
+ * top level map of the result, and requires the children to be JSON objects
+ * and not a JSON primitive (e.g. boolean or string).
+ */
+ public static <T extends JavaScriptObject,
+ M extends NativeMap<T>> AsyncCallback<M> copyKeysIntoChildren(
+ AsyncCallback<M> callback) {
+ return copyKeysIntoChildren("name", callback);
+ }
+
+ /** Loop through the result map and set asProperty on the children. */
+ public static <T extends JavaScriptObject,
+ M extends NativeMap<T>> AsyncCallback<M> copyKeysIntoChildren(
+ final String asProperty, AsyncCallback<M> callback) {
+ return new TransformCallback<M, M>(callback) {
+ @Override
+ protected M transform(M result) {
+ result.copyKeysIntoChildren(asProperty);
+ return result;
+ }
+ };
+ }
+
+ protected NativeMap() {
+ }
+
+ public final Set<String> keySet() {
+ return Natives.keys(this);
+ }
+
+ public final native NativeList<T> values()
+ /*-{
+ var s = this;
+ var v = [];
+ var i = 0;
+ for (var k in s) {
+ if (s.hasOwnProperty(k)) {
+ v[i++] = s[k];
+ }
+ }
+ return v;
+ }-*/;
+
+ public final int size() {
+ return keySet().size();
+ }
+
+ public final boolean isEmpty() {
+ return size() == 0;
+ }
+
+ public final boolean containsKey(String n) {
+ return get(n) != null;
+ }
+
+ public final native T get(String n) /*-{ return this[n]; }-*/;
+
+ public final native void copyKeysIntoChildren(String p)
+ /*-{
+ var s = this;
+ for (var k in s) {
+ if (s.hasOwnProperty(k)) {
+ var c = s[k];
+ c[p] = k;
+ }
+ }
+ }-*/;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java
new file mode 100644
index 0000000..a6c609c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.rpc;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.json.client.JSONObject;
+
+import java.util.Collections;
+import java.util.Set;
+
+public class Natives {
+ /**
+ * Get the names of defined properties on the object. The returned set
+ * iterates in the native iteration order, which may match the source order.
+ */
+ public static Set<String> keys(JavaScriptObject obj) {
+ if (obj != null) {
+ return new JSONObject(obj).keySet();
+ }
+ return Collections.emptySet();
+ }
+
+ public static <T extends JavaScriptObject> T parseJSON(String json) {
+ if (parser == null) {
+ parser = bestJsonParser();
+ }
+ // javac generics bug
+ return Natives.<T>parse0(parser, json);
+ }
+
+ private static native <T extends JavaScriptObject>
+ T parse0(JavaScriptObject p, String s)
+ /*-{ return p(s); }-*/;
+
+ private static JavaScriptObject parser;
+ private static native JavaScriptObject bestJsonParser()
+ /*-{
+ if ($wnd.JSON && typeof $wnd.JSON.parse === 'function')
+ return $wnd.JSON.parse;
+ return function(s) { return eval('(' + s + ')'); };
+ }-*/;
+
+ private Natives() {
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
new file mode 100644
index 0000000..e1fb883
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
@@ -0,0 +1,175 @@
+// 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.client.rpc;
+
+import com.google.gerrit.client.RpcStatus;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.RequestException;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.http.client.URL;
+import com.google.gwt.user.client.rpc.StatusCodeException;
+import com.google.gwtjsonrpc.client.RemoteJsonException;
+import com.google.gwtjsonrpc.client.ServerUnavailableException;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.JsonConstants;
+
+/** Makes a REST API call to the server. */
+public class RestApi {
+ /**
+ * Expected JSON content body prefix that prevents XSSI.
+ * <p>
+ * The server always includes this line as the first line of the response
+ * content body when the response body is formatted as JSON. It gets inserted
+ * by the server to prevent the resource from being imported into another
+ * domain's page using a <script> tag. This line must be removed before
+ * the JSON can be parsed.
+ */
+ private static final String JSON_MAGIC = ")]}'\n";
+
+ private StringBuilder url;
+ private boolean hasQueryParams;
+
+ /**
+ * Initialize a new API call.
+ * <p>
+ * By default the JSON format will be selected by including an HTTP Accept
+ * header in the request.
+ *
+ * @param name URL of the REST resource to access, e.g. {@code "/projects/"}
+ * to list accessible projects from the server.
+ */
+ public RestApi(String name) {
+ if (name.startsWith("/")) {
+ name = name.substring(1);
+ }
+
+ url = new StringBuilder();
+ url.append(GWT.getHostPageBaseURL());
+ url.append(name);
+ }
+
+ public RestApi addParameter(String name, String value) {
+ return addParameterRaw(name, URL.encodeQueryString(value));
+ }
+
+ public RestApi addParameterTrue(String name) {
+ return addParameterRaw(name, null);
+ }
+
+ public RestApi addParameter(String name, boolean value) {
+ return addParameterRaw(name, value ? "t" : "f");
+ }
+
+ public RestApi addParameter(String name, int value) {
+ return addParameterRaw(name, String.valueOf(value));
+ }
+
+ public RestApi addParameterRaw(String name, String value) {
+ if (hasQueryParams) {
+ url.append("&");
+ } else {
+ url.append("?");
+ hasQueryParams = true;
+ }
+ url.append(name);
+ if (value != null) {
+ url.append("=").append(value);
+ }
+ return this;
+ }
+
+ public <T extends JavaScriptObject> void send(final AsyncCallback<T> cb) {
+ RequestBuilder req = new RequestBuilder(RequestBuilder.GET, url.toString());
+ req.setHeader("Accept", JsonConstants.JSON_TYPE);
+ req.setCallback(new RequestCallback() {
+ @Override
+ public void onResponseReceived(Request req, Response res) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ int status = res.getStatusCode();
+ if (status != 200) {
+ if ((400 <= status && status < 500) && isTextBody(res)) {
+ cb.onFailure(new RemoteJsonException(res.getText(), status, null));
+ } else {
+ cb.onFailure(new StatusCodeException(status, res.getStatusText()));
+ }
+ return;
+ }
+
+ if (!isJsonBody(res)) {
+ cb.onFailure(new RemoteJsonException("Invalid JSON"));
+ return;
+ }
+
+ String json = res.getText();
+ if (!json.startsWith(JSON_MAGIC)) {
+ cb.onFailure(new RemoteJsonException("Invalid JSON"));
+ return;
+ }
+
+ T data;
+ try {
+ // javac generics bug
+ data = Natives.<T>parseJSON(json.substring(JSON_MAGIC.length()));
+ } catch (RuntimeException e) {
+ cb.onFailure(new RemoteJsonException("Invalid JSON"));
+ return;
+ }
+
+ cb.onSuccess(data);
+ }
+
+ @Override
+ public void onError(Request req, Throwable err) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ if (err.getMessage().contains("XmlHttpRequest.status")) {
+ cb.onFailure(new ServerUnavailableException());
+ } else {
+ cb.onFailure(err);
+ }
+ }
+ });
+ try {
+ RpcStatus.INSTANCE.onRpcStart();
+ req.send();
+ } catch (RequestException e) {
+ RpcStatus.INSTANCE.onRpcComplete();
+ cb.onFailure(e);
+ }
+ }
+
+ private static boolean isJsonBody(Response res) {
+ return isContentType(res, JsonConstants.JSON_TYPE);
+ }
+
+ private static boolean isTextBody(Response res) {
+ return isContentType(res, "text/plain");
+ }
+
+ private static boolean isContentType(Response res, String want) {
+ String type = res.getHeader("Content-Type");
+ if (type == null) {
+ return false;
+ }
+ int semi = type.indexOf(';');
+ if (semi >= 0) {
+ type = type.substring(0, semi).trim();
+ }
+ return want.equals(type);
+ }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
new file mode 100644
index 0000000..2cd22cb
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
@@ -0,0 +1,38 @@
+// 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.client.rpc;
+
+import com.google.gwtjsonrpc.common.AsyncCallback;
+
+/** Transforms a value and passes it on to another callback. */
+public abstract class TransformCallback<I, O> implements AsyncCallback<I>{
+ private final AsyncCallback<O> callback;
+
+ protected TransformCallback(AsyncCallback<O> callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void onSuccess(I result) {
+ callback.onSuccess(transform(result));
+ }
+
+ @Override
+ public void onFailure(Throwable caught) {
+ callback.onFailure(caught);
+ }
+
+ protected abstract O transform(I result);
+}
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/AccountDashboardLink.java
deleted file mode 100644
index 5233a6b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (C) 2008 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.client.ui;
-
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.AccountDashboardScreen;
-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 {
- /** Create a link after locating account details from an active cache. */
- public static AccountDashboardLink link(final AccountInfoCache cache,
- final Account.Id id) {
- final AccountInfo ai = cache.get(id);
- return ai != null ? new AccountDashboardLink(ai) : null;
- }
-
- private Account.Id accountId;
-
- public AccountDashboardLink(final AccountInfo ai) {
- this(FormatUtil.name(ai), ai);
- }
-
- public AccountDashboardLink(final String text, final AccountInfo ai) {
- this(text, ai.getId());
- setTitle(FormatUtil.nameEmail(ai));
- }
-
- public AccountDashboardLink(final String text, final Account.Id ai) {
- super(text, PageLinks.toAccountDashboard(ai));
- addStyleName(Gerrit.RESOURCES.css().accountName());
- accountId = ai;
- }
-
- @Override
- public void go() {
- Gerrit.display(getTargetHistoryToken(), //
- new AccountDashboardScreen(accountId));
- }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
index 885f53b..5da00cd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
@@ -18,6 +18,7 @@
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.user.client.ui.SuggestOracle;
import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
@@ -31,11 +32,14 @@
private Map<String, AccountGroup.UUID> priorResults =
new HashMap<String, AccountGroup.UUID>();
+ private Project.NameKey projectName;
+
@Override
public void onRequestSuggestions(final Request req, final Callback callback) {
RpcStatus.hide(new Runnable() {
public void run() {
- SuggestUtil.SVC.suggestAccountGroup(req.getQuery(), req.getLimit(),
+ SuggestUtil.SVC.suggestAccountGroupForProject(
+ projectName, req.getQuery(), req.getLimit(),
new GerritCallback<List<GroupReference>>() {
public void onSuccess(final List<GroupReference> result) {
priorResults.clear();
@@ -52,6 +56,10 @@
});
}
+ public void setProject(Project.NameKey projectName) {
+ this.projectName = projectName;
+ }
+
private static class AccountGroupSuggestion implements
SuggestOracle.Suggestion {
private final GroupReference info;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
new file mode 100644
index 0000000..790102c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.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.client.ui;
+
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.Gerrit;
+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 AccountLink extends InlineHyperlink {
+ /** Create a link after locating account details from an active cache. */
+ public static AccountLink link(final AccountInfoCache cache,
+ final Account.Id id) {
+ final AccountInfo ai = cache.get(id);
+ return ai != null ? new AccountLink(ai) : null;
+ }
+
+ public AccountLink(final AccountInfo ai) {
+ super(FormatUtil.name(ai), PageLinks.toAccountQuery(owner(ai)));
+ setTitle(FormatUtil.nameEmail(ai));
+ }
+
+ private static String owner(AccountInfo ai) {
+ if (ai.getPreferredEmail() != null) {
+ return ai.getPreferredEmail();
+ } else if (ai.getFullName() != null) {
+ return ai.getFullName();
+ }
+ return "" + ai.getId().get();
+ }
+
+ @Override
+ public void go() {
+ Gerrit.display(getTargetHistoryToken());
+ }
+}
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-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
index be82eff..25ed258 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
@@ -15,49 +15,25 @@
package com.google.gerrit.client.ui;
import com.google.gerrit.client.RpcStatus;
+import com.google.gerrit.client.projects.ProjectMap;
import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.user.client.ui.SuggestOracle;
import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
-import java.util.ArrayList;
-import java.util.List;
-
/** Suggestion Oracle for Project.NameKey entities. */
public class ProjectNameSuggestOracle extends HighlightSuggestOracle {
@Override
public void onRequestSuggestions(final Request req, final Callback callback) {
RpcStatus.hide(new Runnable() {
+ @Override
public void run() {
- SuggestUtil.SVC.suggestProjectNameKey(req.getQuery(), req.getLimit(),
- new GerritCallback<List<Project.NameKey>>() {
- public void onSuccess(final List<Project.NameKey> result) {
- final ArrayList<ProjectNameSuggestion> r =
- new ArrayList<ProjectNameSuggestion>(result.size());
- for (final Project.NameKey p : result) {
- r.add(new ProjectNameSuggestion(p));
- }
- callback.onSuggestionsReady(req, new Response(r));
+ ProjectMap.suggest(req.getQuery(), req.getLimit(),
+ new GerritCallback<ProjectMap>() {
+ @Override
+ public void onSuccess(ProjectMap map) {
+ callback.onSuggestionsReady(req, new Response(map.values().asList()));
}
});
}
});
}
-
- private static class ProjectNameSuggestion implements
- SuggestOracle.Suggestion {
- private final Project.NameKey key;
-
- ProjectNameSuggestion(final Project.NameKey k) {
- key = k;
- }
-
- public String getDisplayString() {
- return key.get();
- }
-
- public String getReplacementString() {
- return key.get();
- }
- }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
index b768643..50dab9f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
@@ -15,16 +15,19 @@
package com.google.gerrit.client.ui;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.client.projects.ProjectInfo;
+import com.google.gerrit.client.projects.ProjectMap;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
-public class ProjectsTable extends NavigationTable<Project> {
+public class ProjectsTable extends NavigationTable<ProjectInfo> {
public ProjectsTable() {
keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.projectListPrev()));
@@ -41,6 +44,7 @@
fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
}
+ @Override
protected MyFlexTable createFlexTable() {
MyFlexTable table = new MyFlexTable() {
@Override
@@ -78,8 +82,8 @@
}
@Override
- protected Object getRowItemKey(final Project item) {
- return item.getNameKey();
+ protected Object getRowItemKey(final ProjectInfo item) {
+ return item.name();
}
@Override
@@ -89,17 +93,24 @@
}
}
- public void display(final List<Project> projects) {
+ public void display(ProjectMap projects) {
while (1 < table.getRowCount())
table.removeRow(table.getRowCount() - 1);
- for (final Project k : projects)
- insert(table.getRowCount(), k);
+ List<ProjectInfo> list = projects.values().asList();
+ Collections.sort(list, new Comparator<ProjectInfo>() {
+ @Override
+ public int compare(ProjectInfo a, ProjectInfo b) {
+ return a.name().compareTo(b.name());
+ }
+ });
+ for(ProjectInfo p : list)
+ insert(table.getRowCount(), p);
finishDisplay();
}
- protected void insert(final int row, final Project k) {
+ protected void insert(final int row, final ProjectInfo k) {
table.insertRow(row);
applyDataRowStyle(row);
@@ -112,9 +123,9 @@
populate(row, k);
}
- protected void populate(final int row, final Project k) {
- table.setText(row, 1, k.getName());
- table.setText(row, 2, k.getDescription());
+ protected void populate(final int row, final ProjectInfo k) {
+ table.setText(row, 1, k.name());
+ table.setText(row, 2, k.description());
setRowItem(row, k);
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
index 5cb4727..7f5eead 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
@@ -15,8 +15,6 @@
package com.google.gerrit.client.ui;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.common.PageLinks;
-import com.google.gwt.user.client.History;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
@@ -148,13 +146,6 @@
return requiresSignIn;
}
- /** Invoked if this screen is the current screen and the user signs out. */
- public void onSignOut() {
- if (isRequiresSignIn()) {
- History.newItem(PageLinks.toChangeQuery("status:open"));
- }
- }
-
public void onShowView() {
if (windowTitle != null) {
Gerrit.setWindowTitle(this, windowTitle);
diff --git a/gerrit-httpd/.gitignore b/gerrit-httpd/.gitignore
index 194bedc..5bbeafd 100644
--- a/gerrit-httpd/.gitignore
+++ b/gerrit-httpd/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-httpd.iml
\ No newline at end of file
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml
index a6374da..ceacb66 100644
--- a/gerrit-httpd/pom.xml
+++ b/gerrit-httpd/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-httpd</artifactId>
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index fb30a4d..151a6d9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -26,14 +26,11 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EvictionPolicy;
import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Provider;
-import com.google.inject.TypeLiteral;
import com.google.inject.servlet.RequestScoped;
import javax.servlet.http.Cookie;
@@ -49,13 +46,9 @@
return new CacheModule() {
@Override
protected void configure() {
- final String cacheName = WebSessionManager.CACHE_NAME;
- final TypeLiteral<Cache<Key, Val>> type =
- new TypeLiteral<Cache<Key, Val>>() {};
- disk(type, cacheName) //
- .memoryLimit(1024) // reasonable default for many sites
- .maxAge(MAX_AGE_MINUTES, MINUTES) // expire sessions if they are inactive
- .evictionPolicy(EvictionPolicy.LRU) // keep most recently used
+ persist(WebSessionManager.CACHE_NAME, String.class, Val.class)
+ .maximumWeight(1024) // reasonable default for many sites
+ .expireAfterWrite(MAX_AGE_MINUTES, MINUTES) // expire sessions if they are inactive
;
bind(WebSessionManager.class);
bind(WebSession.class)
@@ -170,7 +163,7 @@
/** Set the user account for this current request only. */
public void setUserAccountId(Account.Id id) {
key = new Key("id:" + id);
- val = new Val(id, 0, false, null, "");
+ val = new Val(id, 0, false, null, "", 0);
}
public void logout() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
index c0e3f42..29b5d95 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
@@ -20,12 +20,10 @@
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtjsonrpc.server.XsrfException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import org.eclipse.jgit.http.server.GitSmartHttpTools;
import org.eclipse.jgit.lib.Config;
import java.io.IOException;
@@ -39,7 +37,6 @@
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpServletResponseWrapper;
/**
* Trust the authentication which is done by the container.
@@ -62,7 +59,7 @@
@Inject
ContainerAuthFilter(Provider<WebSession> session, AccountCache accountCache,
- @GerritServerConfig Config config) throws XsrfException {
+ @GerritServerConfig Config config) {
this.session = session;
this.accountCache = accountCache;
this.config = config;
@@ -80,20 +77,14 @@
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
- if (!GitSmartHttpTools.isGitClient(req)) {
- chain.doFilter(request, response);
- return;
- }
-
- HttpServletResponseWrapper rsp =
- new HttpServletResponseWrapper((HttpServletResponse) response);
+ HttpServletResponse rsp = (HttpServletResponse) response;
if (verify(req, rsp)) {
chain.doFilter(req, response);
}
}
- private boolean verify(HttpServletRequest req, HttpServletResponseWrapper rsp)
+ private boolean verify(HttpServletRequest req, HttpServletResponse rsp)
throws IOException {
String username = req.getRemoteUser();
if (username == null) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
index 1953480..f92f13d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
@@ -93,10 +93,12 @@
case LDAP:
case LDAP_BIND:
config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
+ config.setEditFullNameUrl(cfg.getString("auth", null, "editFullNameUrl"));
break;
case CUSTOM_EXTENSION:
config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
+ config.setEditFullNameUrl(cfg.getString("auth", null, "editFullNameUrl"));
config.setHttpPasswordUrl(cfg.getString("auth", null, "httpPasswordUrl"));
break;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
index 6fd94c9..aa004e3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
@@ -41,5 +41,7 @@
String git = GitOverHttpServlet.URL_REGEX;
filterRegex(git).through(authFilter);
serveRegex(git).with(GitOverHttpServlet.class);
+
+ filter("/a/*").through(authFilter);
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index c36df04a..1c9c521 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -14,13 +14,13 @@
package com.google.gerrit.httpd;
+import com.google.common.cache.Cache;
import com.google.gerrit.common.data.Capable;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.git.AsyncReceiveCommits;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -99,11 +99,11 @@
install(new CacheModule() {
@Override
protected void configure() {
- TypeLiteral<Cache<AdvertisedObjectsCacheKey, Set<ObjectId>>> cache =
- new TypeLiteral<Cache<AdvertisedObjectsCacheKey, Set<ObjectId>>>() {};
- core(cache, ID_CACHE)
- .memoryLimit(4096)
- .maxAge(10, TimeUnit.MINUTES);
+ cache(ID_CACHE,
+ AdvertisedObjectsCacheKey.class,
+ new TypeLiteral<Set<ObjectId>>() {})
+ .maximumWeight(4096)
+ .expireAfterWrite(10, TimeUnit.MINUTES);
}
});
}
@@ -167,7 +167,12 @@
}
req.setAttribute(ATT_CONTROL, pc);
- return manager.openRepository(pc.getProject().getNameKey());
+ try {
+ return manager.openRepository(pc.getProject().getNameKey());
+ } catch (IOException e) {
+ throw new RepositoryNotFoundException(
+ pc.getProject().getNameKey().get(), e);
+ }
}
}
@@ -315,12 +320,12 @@
if (isGet) {
rc.advertiseHistory();
- cache.remove(cacheKey);
+ cache.invalidate(cacheKey);
} else {
- Set<ObjectId> ids = cache.get(cacheKey);
+ Set<ObjectId> ids = cache.getIfPresent(cacheKey);
if (ids != null) {
rp.getAdvertisedObjects().addAll(ids);
- cache.remove(cacheKey);
+ cache.invalidate(cacheKey);
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java
deleted file mode 100644
index 6c420a5..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2009 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;
-
-import com.google.gerrit.common.errors.NotSignedInException;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-
-class HttpIdentifiedUserProvider implements Provider<IdentifiedUser> {
- private final Provider<CurrentUser> currUserProvider;
-
- @Inject
- HttpIdentifiedUserProvider(Provider<CurrentUser> currUserProvider) {
- this.currUserProvider = currUserProvider;
- }
-
- @Override
- public IdentifiedUser get() {
- CurrentUser user = currUserProvider.get();
- if (user instanceof IdentifiedUser) {
- return (IdentifiedUser) user;
- }
- throw new ProvisionException(NotSignedInException.MESSAGE,
- new NotSignedInException());
- }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
similarity index 81%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
index c87a143..8ef826b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
@@ -15,19 +15,19 @@
package com.google.gerrit.httpd;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.util.RequestContext;
import com.google.inject.Inject;
-import com.google.inject.Provider;
-class HttpCurrentUserProvider implements Provider<CurrentUser> {
+class HttpRequestContext implements RequestContext {
private final WebSession session;
@Inject
- HttpCurrentUserProvider(final WebSession session) {
+ HttpRequestContext(final WebSession session) {
this.session = session;
}
@Override
- public CurrentUser get() {
+ public CurrentUser getCurrentUser() {
return session.getCurrentUser();
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
index c5b0e90..9e12e8c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
@@ -30,7 +30,6 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import org.eclipse.jgit.http.server.GitSmartHttpTools;
import org.eclipse.jgit.lib.Config;
import java.io.IOException;
@@ -100,12 +99,7 @@
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
- if (!GitSmartHttpTools.isGitClient(req)) {
- chain.doFilter(request, response);
- return;
- }
-
- Response rsp = new Response((HttpServletResponse) response);
+ Response rsp = new Response(req, (HttpServletResponse) response);
if (verify(req, rsp)) {
chain.doFilter(req, rsp);
@@ -281,10 +275,6 @@
return p;
}
- private String getDomain() {
- return urlProvider.get() + "p/";
- }
-
private String newNonce() {
try {
return tokens.newToken("");
@@ -295,11 +285,12 @@
class Response extends HttpServletResponseWrapper {
private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
-
+ private final HttpServletRequest req;
Boolean stale;
- Response(HttpServletResponse rsp) {
+ Response(HttpServletRequest req, HttpServletResponse rsp) {
super(rsp);
+ this.req = req;
}
private void status(int sc) {
@@ -307,7 +298,18 @@
StringBuilder v = new StringBuilder();
v.append("Digest");
v.append(" realm=\"" + REALM_NAME + "\"");
- v.append(", domain=\"" + getDomain() + "\"");
+
+ String url = urlProvider.get();
+ if (url == null) {
+ url = req.getContextPath();
+ if (url != null && !url.isEmpty() && !url.endsWith("/")) {
+ url += "/";
+ }
+ }
+ if (url != null && !url.isEmpty()) {
+ v.append(", domain=\"" + url + "\"");
+ }
+
v.append(", qop=\"auth\"");
if (stale != null) {
v.append(", stale=" + stale);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java
similarity index 61%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java
copy to gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java
index 0e6a567..b46505f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java
@@ -15,9 +15,13 @@
package com.google.gerrit.httpd;
import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.inject.Inject;
+import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import com.google.inject.servlet.ServletModule;
import java.io.IOException;
@@ -30,12 +34,27 @@
/** Executes any pending {@link RequestCleanup} at the end of a request. */
@Singleton
-class RequestCleanupFilter implements Filter {
+public class RequestContextFilter implements Filter {
+ public static Module module() {
+ return new ServletModule() {
+ @Override
+ protected void configureServlets() {
+ filter("/*").through(RequestContextFilter.class);
+ }
+ };
+ }
+
private final Provider<RequestCleanup> cleanup;
+ private final Provider<HttpRequestContext> requestContext;
+ private final ThreadLocalRequestContext local;
@Inject
- RequestCleanupFilter(final Provider<RequestCleanup> r) {
+ RequestContextFilter(final Provider<RequestCleanup> r,
+ final Provider<HttpRequestContext> c,
+ final ThreadLocalRequestContext l) {
cleanup = r;
+ requestContext = c;
+ local = l;
}
@Override
@@ -50,10 +69,15 @@
public void doFilter(final ServletRequest request,
final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
+ RequestContext old = local.setContext(requestContext.get());
try {
- chain.doFilter(request, response);
+ try {
+ chain.doFilter(request, response);
+ } finally {
+ cleanup.get().run();
+ }
} finally {
- cleanup.get().run();
+ local.setContext(old);
}
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java
similarity index 62%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java
index 0e6a567..499c2a5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.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.
@@ -14,7 +14,8 @@
package com.google.gerrit.httpd;
-import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -27,15 +28,16 @@
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
-/** Executes any pending {@link RequestCleanup} at the end of a request. */
+/** Requires the user to be authenticated over HTTP. */
@Singleton
-class RequestCleanupFilter implements Filter {
- private final Provider<RequestCleanup> cleanup;
+class RequireIdentifiedUserFilter implements Filter {
+ private final Provider<CurrentUser> user;
@Inject
- RequestCleanupFilter(final Provider<RequestCleanup> r) {
- cleanup = r;
+ RequireIdentifiedUserFilter(Provider<CurrentUser> user) {
+ this.user = user;
}
@Override
@@ -47,13 +49,14 @@
}
@Override
- public void doFilter(final ServletRequest request,
- final ServletResponse response, final FilterChain chain)
+ public void doFilter(ServletRequest request,
+ ServletResponse response, FilterChain chain)
throws IOException, ServletException {
- try {
+ if (user.get() instanceof IdentifiedUser) {
chain.doFilter(request, response);
- } finally {
- cleanup.get().run();
+ } else {
+ HttpServletResponse res = (HttpServletResponse) response;
+ res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java
new file mode 100644
index 0000000..8105e25
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.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;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.util.cli.CmdLineParser;
+import com.google.gwtjsonrpc.server.RPCServletUtils;
+import com.google.gwtjsonrpc.common.JsonConstants;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.CmdLineException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public abstract class RestApiServlet extends HttpServlet {
+ private static final long serialVersionUID = 1L;
+ private static final Logger log =
+ LoggerFactory.getLogger(RestApiServlet.class);
+
+ /** MIME type used for a JSON response body. */
+ protected static final String JSON_TYPE = JsonConstants.JSON_TYPE;
+
+ /**
+ * Garbage prefix inserted before JSON output to prevent XSSI.
+ * <p>
+ * This prefix is ")]}'\n" and is designed to prevent a web browser from
+ * executing the response body if the resource URI were to be referenced using
+ * a <script src="...> HTML tag from another web site. Clients using the
+ * HTTP interface will need to always strip the first line of response data to
+ * remove this magic header.
+ */
+ protected static final byte[] JSON_MAGIC;
+
+ static {
+ try {
+ JSON_MAGIC = ")]}'\n".getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 not supported", e);
+ }
+ }
+
+ @Override
+ protected void service(HttpServletRequest req, HttpServletResponse res)
+ throws ServletException, IOException {
+ noCache(res);
+ try {
+ super.service(req, res);
+ } catch (Error err) {
+ handleError(err, req, res);
+ } catch (RuntimeException err) {
+ handleError(err, req, res);
+ }
+ }
+
+ 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 void handleError(
+ Throwable err, HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ String uri = req.getRequestURI();
+ if (!Strings.isNullOrEmpty(req.getQueryString())) {
+ uri += "?" + req.getQueryString();
+ }
+ log.error(String.format("Error in %s %s", req.getMethod(), uri), err);
+
+ if (!res.isCommitted()) {
+ res.reset();
+ res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ noCache(res);
+ sendText(req, res, "Internal Server Error");
+ }
+ }
+
+ protected static boolean acceptsJson(HttpServletRequest req) {
+ String accept = req.getHeader("Accept");
+ if (accept == null) {
+ return false;
+ } else if (JSON_TYPE.equals(accept)) {
+ return true;
+ } else if (accept.startsWith(JSON_TYPE + ",")) {
+ return true;
+ }
+ for (String p : accept.split("[ ,;][ ,;]*")) {
+ if (JSON_TYPE.equals(p)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ protected static void sendText(HttpServletRequest req,
+ HttpServletResponse res, String data) throws IOException {
+ res.setContentType("text/plain");
+ res.setCharacterEncoding("UTF-8");
+ send(req, res, data.getBytes("UTF-8"));
+ }
+
+ protected static void send(HttpServletRequest req, HttpServletResponse res,
+ byte[] data) throws IOException {
+ if (data.length > 256 && RPCServletUtils.acceptsGzipEncoding(req)) {
+ res.setHeader("Content-Encoding", "gzip");
+ data = HtmlDomUtil.compress(data);
+ }
+ res.setContentLength(data.length);
+ OutputStream out = res.getOutputStream();
+ try {
+ out.write(data);
+ } finally {
+ out.close();
+ }
+ }
+
+ public static class ParameterParser {
+ private final CmdLineParser.Factory parserFactory;
+
+ @Inject
+ ParameterParser(CmdLineParser.Factory pf) {
+ this.parserFactory = pf;
+ }
+
+ public <T> boolean parse(T param, HttpServletRequest req,
+ HttpServletResponse res) throws IOException {
+ CmdLineParser clp = parserFactory.create(param);
+ try {
+ @SuppressWarnings("unchecked")
+ Map<String, String[]> parameterMap = req.getParameterMap();
+ clp.parseOptionMap(parameterMap);
+ } catch (CmdLineException e) {
+ if (!clp.wasHelpRequestedByOption()) {
+ res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ sendText(req, res, e.getMessage());
+ return false;
+ }
+ }
+
+ if (clp.wasHelpRequestedByOption()) {
+ StringWriter msg = new StringWriter();
+ clp.printQueryStringUsage(req.getRequestURI(), msg);
+ msg.write('\n');
+ msg.write('\n');
+ clp.printUsage(msg, null);
+ msg.write('\n');
+ sendText(req, res, msg.toString());
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index f90c20d..9e69946 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -23,14 +23,22 @@
import com.google.gerrit.httpd.raw.SshInfoServlet;
import com.google.gerrit.httpd.raw.StaticServlet;
import com.google.gerrit.httpd.raw.ToolServlet;
+import com.google.gerrit.httpd.rpc.account.AccountCapabilitiesServlet;
+import com.google.gerrit.httpd.rpc.change.DeprecatedChangeQueryServlet;
+import com.google.gerrit.httpd.rpc.change.ListChangesServlet;
+import com.google.gerrit.httpd.rpc.project.ListProjectsServlet;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gwtexpui.server.CacheControlFilter;
+import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.Provider;
import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.servlet.ServletModule;
+import org.eclipse.jgit.lib.Config;
+
import java.io.IOException;
import javax.servlet.http.HttpServlet;
@@ -38,6 +46,21 @@
import javax.servlet.http.HttpServletResponse;
class UrlModule extends ServletModule {
+ static class UrlConfig {
+ private final boolean deprecatedQuery;
+
+ @Inject
+ UrlConfig(@GerritServerConfig Config cfg) {
+ deprecatedQuery = cfg.getBoolean("site", "enableDeprecatedQuery", true);
+ }
+ }
+
+ private final UrlConfig cfg;
+
+ UrlModule(UrlConfig cfg) {
+ this.cfg = cfg;
+ }
+
@Override
protected void configureServlets() {
filter("/*").through(Key.get(CacheControlFilter.class));
@@ -48,7 +71,6 @@
serve("/Gerrit/*").with(legacyGerritScreen());
serve("/cat/*").with(CatServlet.class);
serve("/logout").with(HttpLogoutServlet.class);
- serve("/query").with(ChangeQueryServlet.class);
serve("/signout").with(HttpLogoutServlet.class);
serve("/ssh_info").with(SshInfoServlet.class);
serve("/static/*").with(StaticServlet.class);
@@ -69,6 +91,15 @@
serveRegex("^/([1-9][0-9]*)/?$").with(directChangeById());
serveRegex("^/p/(.*)$").with(queryProjectNew());
serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
+
+ filter("/a/*").through(RequireIdentifiedUserFilter.class);
+ serveRegex("^/(?:a/)?accounts/self/capabilities$").with(AccountCapabilitiesServlet.class);
+ serveRegex("^/(?:a/)?changes/$").with(ListChangesServlet.class);
+ serveRegex("^/(?:a/)?projects/(.*)?$").with(ListProjectsServlet.class);
+
+ if (cfg.deprecatedQuery) {
+ serve("/query").with(DeprecatedChangeQueryServlet.class);
+ }
}
private Key<HttpServlet> notFound() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
index 8ee2c41..852caae 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -15,6 +15,7 @@
package com.google.gerrit.httpd;
import static com.google.inject.Scopes.SINGLETON;
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
import com.google.gerrit.common.data.GerritConfig;
import com.google.gerrit.httpd.auth.become.BecomeAnyAccountLoginServlet;
@@ -23,8 +24,7 @@
import com.google.gerrit.httpd.auth.ldap.LdapAuthModule;
import com.google.gerrit.httpd.gitweb.GitWebModule;
import com.google.gerrit.httpd.rpc.UiRpcModule;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.ChangeUserName;
@@ -38,6 +38,7 @@
import com.google.gerrit.server.contact.ContactStoreProvider;
import com.google.gerrit.server.util.GuiceRequestScopePropagator;
import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.gerrit.util.cli.CmdLineParser;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Injector;
@@ -51,14 +52,17 @@
public class WebModule extends FactoryModule {
private final AuthConfig authConfig;
+ private final UrlModule.UrlConfig urlConfig;
private final boolean wantSSL;
private final GitWebConfig gitWebConfig;
@Inject
WebModule(final AuthConfig authConfig,
+ final UrlModule.UrlConfig urlConfig,
@CanonicalWebUrl @Nullable final String canonicalUrl,
final Injector creatingInjector) {
this.authConfig = authConfig;
+ this.urlConfig = urlConfig;
this.wantSSL = canonicalUrl != null && canonicalUrl.startsWith("https:");
this.gitWebConfig =
@@ -72,13 +76,8 @@
@Override
protected void configure() {
- install(new ServletModule() {
- @Override
- protected void configureServlets() {
- filter("/*").through(RequestCleanupFilter.class);
- }
- });
bind(RequestScopePropagator.class).to(GuiceRequestScopePropagator.class);
+ bind(HttpRequestContext.class);
if (wantSSL) {
install(new RequireSslFilter.Module());
@@ -116,7 +115,7 @@
throw new ProvisionException("Unsupported loginType: " + authConfig.getAuthType());
}
- install(new UrlModule());
+ install(new UrlModule(urlConfig));
install(new UiRpcModule());
install(new GerritRequestModule());
install(new GitOverHttpServlet.Module());
@@ -135,12 +134,17 @@
bind(ChangeUserName.CurrentUser.class);
factory(ChangeUserName.Factory.class);
factory(ClearPassword.Factory.class);
+ factory(CmdLineParser.Factory.class);
factory(GeneratePassword.Factory.class);
bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
HttpRemotePeerProvider.class).in(RequestScoped.class);
- bind(CurrentUser.class).toProvider(HttpCurrentUserProvider.class);
- bind(IdentifiedUser.class).toProvider(HttpIdentifiedUserProvider.class);
+ install(new LifecycleModule() {
+ @Override
+ protected void configure() {
+ listener().toInstance(registerInParentInjectors());
+ }
+ });
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
index 55d0ca5..4b4edf4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -26,9 +26,9 @@
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
+import com.google.common.cache.Cache;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
@@ -43,6 +43,7 @@
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.security.SecureRandom;
+import java.util.concurrent.TimeUnit;
@Singleton
class WebSessionManager {
@@ -54,11 +55,11 @@
private final long sessionMaxAgeMillis;
private final SecureRandom prng;
- private final Cache<Key, Val> self;
+ private final Cache<String, Val> self;
@Inject
WebSessionManager(@GerritServerConfig Config cfg,
- @Named(CACHE_NAME) final Cache<Key, Val> cache) {
+ @Named(CACHE_NAME) final Cache<String, Val> cache) {
prng = new SecureRandom();
self = cache;
@@ -75,7 +76,7 @@
prng.nextBytes(rnd);
buf = new ByteArrayOutputStream(3 + nonceLen);
- writeVarInt32(buf, (int) Key.serialVersionUID);
+ writeVarInt32(buf, (int) Val.serialVersionUID);
writeVarInt32(buf, who.get());
writeBytes(buf, rnd);
@@ -104,7 +105,9 @@
final long halfAgeRefresh = sessionMaxAgeMillis >>> 1;
final long minRefresh = MILLISECONDS.convert(1, HOURS);
final long refresh = Math.min(halfAgeRefresh, minRefresh);
- final long refreshCookieAt = now() + refresh;
+ final long now = now();
+ final long refreshCookieAt = now + refresh;
+ final long expiresAt = now + sessionMaxAgeMillis;
if (xsrfToken == null) {
// If we don't yet have a token for this session, establish one.
@@ -115,8 +118,9 @@
xsrfToken = CookieBase64.encode(rnd);
}
- Val val = new Val(who, refreshCookieAt, remember, lastLogin, xsrfToken);
- self.put(key, val);
+ Val val = new Val(who, refreshCookieAt, remember,
+ lastLogin, xsrfToken, expiresAt);
+ self.put(key.token, val);
return val;
}
@@ -137,16 +141,19 @@
}
Val get(final Key key) {
- return self.get(key);
+ Val val = self.getIfPresent(key.token);
+ if (val != null && val.expiresAt <= now()) {
+ self.invalidate(key.token);
+ return null;
+ }
+ return val;
}
void destroy(final Key key) {
- self.remove(key);
+ self.invalidate(key.token);
}
- static final class Key implements Serializable {
- static final long serialVersionUID = 2L;
-
+ static final class Key {
private transient String token;
Key(final String t) {
@@ -166,33 +173,28 @@
public boolean equals(Object obj) {
return obj instanceof Key && token.equals(((Key) obj).token);
}
-
- private void writeObject(final ObjectOutputStream out) throws IOException {
- writeString(out, token);
- }
-
- private void readObject(final ObjectInputStream in) throws IOException {
- token = readString(in);
- }
}
static final class Val implements Serializable {
- static final long serialVersionUID = Key.serialVersionUID;
+ static final long serialVersionUID = 2L;
private transient Account.Id accountId;
private transient long refreshCookieAt;
private transient boolean persistentCookie;
private transient AccountExternalId.Key externalId;
private transient String xsrfToken;
+ private transient long expiresAt;
Val(final Account.Id accountId, final long refreshCookieAt,
final boolean persistentCookie, final AccountExternalId.Key externalId,
- final String xsrfToken) {
+ final String xsrfToken,
+ final long expiresAt) {
this.accountId = accountId;
this.refreshCookieAt = refreshCookieAt;
this.persistentCookie = persistentCookie;
this.externalId = externalId;
this.xsrfToken = xsrfToken;
+ this.expiresAt = expiresAt;
}
Account.Id getAccountId() {
@@ -233,6 +235,9 @@
writeVarInt32(out, 5);
writeString(out, xsrfToken);
+ writeVarInt32(out, 6);
+ writeFixInt64(out, expiresAt);
+
writeVarInt32(out, 0);
}
@@ -257,10 +262,16 @@
case 5:
xsrfToken = readString(in);
continue;
+ case 6:
+ expiresAt = readFixInt64(in);
+ continue;
default:
throw new IOException("Unknown tag found in object: " + tag);
}
}
+ if (expiresAt == 0) {
+ expiresAt = refreshCookieAt + TimeUnit.HOURS.toMillis(2);
+ }
}
}
}
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..bb47b8b
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.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.gerrit.server.cache.CacheModule;
+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 {
+ static final String PLUGIN_RESOURCES = "plugin_resources";
+
+ @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);
+
+ install(new CacheModule() {
+ @Override
+ protected void configure() {
+ cache(PLUGIN_RESOURCES, ResourceKey.class, Resource.class)
+ .maximumWeight(2 << 20)
+ .weigher(ResourceWeigher.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..79f9011
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -0,0 +1,585 @@
+// 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.cache.Cache;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.MimeUtilFileTypeRegistry;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.documentation.MarkdownFormatter;
+import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
+import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.inject.servlet.GuiceFilter;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+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 int SMALL_RESOURCE = 128 * 1024;
+ private static final long serialVersionUID = 1L;
+ private static final Logger log
+ = LoggerFactory.getLogger(HttpPluginServlet.class);
+
+ private final MimeUtilFileTypeRegistry mimeUtil;
+ private final Provider<String> webUrl;
+ private final Cache<ResourceKey, Resource> resourceCache;
+ private final String sshHost;
+ private final int sshPort;
+
+ private List<Plugin> pending = Lists.newArrayList();
+ private String base;
+ private final ConcurrentMap<String, PluginHolder> plugins
+ = Maps.newConcurrentMap();
+
+ @Inject
+ HttpPluginServlet(MimeUtilFileTypeRegistry mimeUtil,
+ @CanonicalWebUrl Provider<String> webUrl,
+ @Named(HttpPluginModule.PLUGIN_RESOURCES) Cache<ResourceKey, Resource> cache,
+ @GerritServerConfig Config cfg,
+ SshInfo sshInfo) {
+ this.mimeUtil = mimeUtil;
+ this.webUrl = webUrl;
+ this.resourceCache = cache;
+
+ String sshHost = "review.example.com";
+ int sshPort = 29418;
+ if (!sshInfo.getHostKeys().isEmpty()) {
+ String host = sshInfo.getHostKeys().get(0).getHost();
+ int c = host.lastIndexOf(':');
+ if (0 <= c) {
+ sshHost = host.substring(0, c);
+ sshPort = Integer.parseInt(host.substring(c+1));
+ } else {
+ sshHost = host;
+ sshPort = 22;
+ }
+ }
+ this.sshHost = sshHost;
+ this.sshPort = sshPort;
+ }
+
+ @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 {
+ if (!"GET".equals(req.getMethod()) && !"HEAD".equals(req.getMethod())) {
+ noCache(res);
+ res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+ return;
+ }
+
+ String uri = req.getRequestURI();
+ String ctx = req.getContextPath();
+ String file = uri.substring(ctx.length() + 1);
+
+ ResourceKey key = new ResourceKey(holder.plugin, file);
+ Resource rsc = resourceCache.getIfPresent(key);
+ if (rsc != null) {
+ rsc.send(req, res);
+ return;
+ }
+
+ if ("".equals(file)) {
+ res.sendRedirect(uri + "Documentation/index.html");
+ } else if (file.startsWith("static/")) {
+ JarFile jar = holder.plugin.getJarFile();
+ JarEntry entry = jar.getJarEntry(file);
+ if (exists(entry)) {
+ sendResource(jar, entry, key, res);
+ } else {
+ resourceCache.put(key, Resource.NOT_FOUND);
+ Resource.NOT_FOUND.send(req, res);
+ }
+ } else if (file.equals("Documentation")) {
+ res.sendRedirect(uri + "/index.html");
+ } else if (file.startsWith("Documentation/") && file.endsWith("/")) {
+ res.sendRedirect(uri + "index.html");
+ } else if (file.startsWith("Documentation/")) {
+ JarFile jar = holder.plugin.getJarFile();
+ JarEntry entry = jar.getJarEntry(file);
+ if (!exists(entry)) {
+ entry = findSource(jar, file);
+ }
+ if (!exists(entry) && file.endsWith("/index.html")) {
+ String pfx = file.substring(0, file.length() - "index.html".length());
+ sendAutoIndex(jar, pfx, holder.plugin.getName(), key, res);
+ } else if (exists(entry) && entry.getName().endsWith(".md")) {
+ sendMarkdownAsHtml(jar, entry, holder.plugin.getName(), key, res);
+ } else if (exists(entry)) {
+ sendResource(jar, entry, key, res);
+ } else {
+ resourceCache.put(key, Resource.NOT_FOUND);
+ Resource.NOT_FOUND.send(req, res);
+ }
+ } else {
+ resourceCache.put(key, Resource.NOT_FOUND);
+ Resource.NOT_FOUND.send(req, res);
+ }
+ }
+
+ private void sendAutoIndex(JarFile jar,
+ String prefix, String pluginName,
+ ResourceKey cacheKey, HttpServletResponse res) throws IOException {
+ List<JarEntry> cmds = Lists.newArrayList();
+ List<JarEntry> docs = Lists.newArrayList();
+ Enumeration<JarEntry> entries = jar.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry entry = entries.nextElement();
+ String name = entry.getName();
+ long size = entry.getSize();
+ if (name.startsWith(prefix)
+ && (name.endsWith(".md")
+ || name.endsWith(".html"))
+ && 0 < size && size <= SMALL_RESOURCE) {
+ if (name.substring(prefix.length()).startsWith("cmd-")) {
+ cmds.add(entry);
+ } else {
+ docs.add(entry);
+ }
+ }
+ }
+ Collections.sort(cmds, new Comparator<JarEntry>() {
+ @Override
+ public int compare(JarEntry a, JarEntry b) {
+ return a.getName().compareTo(b.getName());
+ }
+ });
+ Collections.sort(docs, new Comparator<JarEntry>() {
+ @Override
+ public int compare(JarEntry a, JarEntry b) {
+ return a.getName().compareTo(b.getName());
+ }
+ });
+
+ StringBuilder md = new StringBuilder();
+ md.append(String.format("# Plugin %s #\n", pluginName));
+ md.append("\n");
+ appendPluginInfoTable(md, jar.getManifest().getMainAttributes());
+
+ if (!docs.isEmpty()) {
+ md.append("## Documentation ##\n");
+ for(JarEntry entry : docs) {
+ String rsrc = entry.getName().substring(prefix.length());
+ String title;
+ if (rsrc.endsWith(".html")) {
+ title = rsrc.substring(0, rsrc.length() - 5).replace('-', ' ');
+ } else if (rsrc.endsWith(".md")) {
+ title = extractTitleFromMarkdown(jar, entry);
+ if (Strings.isNullOrEmpty(title)) {
+ title = rsrc.substring(0, rsrc.length() - 3).replace('-', ' ');
+ }
+ rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
+ } else {
+ title = rsrc.replace('-', ' ');
+ }
+ md.append(String.format("* [%s](%s)\n", title, rsrc));
+ }
+ md.append("\n");
+ }
+
+ if (!cmds.isEmpty()) {
+ md.append("## Commands ##\n");
+ for(JarEntry entry : cmds) {
+ String rsrc = entry.getName().substring(prefix.length());
+ String title;
+ if (rsrc.endsWith(".html")) {
+ title = rsrc.substring(4, rsrc.length() - 5).replace('-', ' ');
+ } else if (rsrc.endsWith(".md")) {
+ title = extractTitleFromMarkdown(jar, entry);
+ if (Strings.isNullOrEmpty(title)) {
+ title = rsrc.substring(4, rsrc.length() - 3).replace('-', ' ');
+ }
+ rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
+ } else {
+ title = rsrc.substring(4).replace('-', ' ');
+ }
+ md.append(String.format("* [%s](%s)\n", title, rsrc));
+ }
+ md.append("\n");
+ }
+
+ sendMarkdownAsHtml(md.toString(), pluginName, cacheKey, res);
+ }
+
+ private void sendMarkdownAsHtml(String md, String pluginName,
+ ResourceKey cacheKey, HttpServletResponse res)
+ throws UnsupportedEncodingException, IOException {
+ Map<String, String> macros = Maps.newHashMap();
+ macros.put("PLUGIN", pluginName);
+ macros.put("SSH_HOST", sshHost);
+ macros.put("SSH_PORT", "" + sshPort);
+ String url = webUrl.get();
+ if (Strings.isNullOrEmpty(url)) {
+ url = "http://review.example.com/";
+ }
+ macros.put("URL", url);
+
+ Matcher m = Pattern.compile("(\\\\)?@([A-Z_]+)@").matcher(md);
+ StringBuffer sb = new StringBuffer();
+ while (m.find()) {
+ String key = m.group(2);
+ String val = macros.get(key);
+ if (m.group(1) != null) {
+ m.appendReplacement(sb, "@" + key + "@");
+ } else if (val != null) {
+ m.appendReplacement(sb, val);
+ } else {
+ m.appendReplacement(sb, "@" + key + "@");
+ }
+ }
+ m.appendTail(sb);
+
+ byte[] html = new MarkdownFormatter()
+ .markdownToDocHtml(sb.toString(), "UTF-8");
+ resourceCache.put(cacheKey, new SmallResource(html)
+ .setContentType("text/html")
+ .setCharacterEncoding("UTF-8"));
+ res.setContentType("text/html");
+ res.setCharacterEncoding("UTF-8");
+ res.setContentLength(html.length);
+ res.getOutputStream().write(html);
+ }
+
+ private static void appendPluginInfoTable(StringBuilder html, Attributes main) {
+ if (main != null) {
+ String t = main.getValue(Attributes.Name.IMPLEMENTATION_TITLE);
+ String n = main.getValue(Attributes.Name.IMPLEMENTATION_VENDOR);
+ String v = main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+ String u = main.getValue(Attributes.Name.IMPLEMENTATION_URL);
+ String a = main.getValue("Gerrit-ApiVersion");
+
+ html.append("<table class=\"plugin_info\">");
+ if (!Strings.isNullOrEmpty(t)) {
+ html.append("<tr><th>Name</th><td>")
+ .append(t)
+ .append("</td></tr>\n");
+ }
+ if (!Strings.isNullOrEmpty(n)) {
+ html.append("<tr><th>Vendor</th><td>")
+ .append(n)
+ .append("</td></tr>\n");
+ }
+ if (!Strings.isNullOrEmpty(v)) {
+ html.append("<tr><th>Version</th><td>")
+ .append(v)
+ .append("</td></tr>\n");
+ }
+ if (!Strings.isNullOrEmpty(u)) {
+ html.append("<tr><th>URL</th><td>")
+ .append(String.format("<a href=\"%s\">%s</a>", u, u))
+ .append("</td></tr>\n");
+ }
+ if (!Strings.isNullOrEmpty(a)) {
+ html.append("<tr><th>API Version</th><td>")
+ .append(a)
+ .append("</td></tr>\n");
+ }
+ html.append("</table>\n");
+ }
+ }
+
+ private static String extractTitleFromMarkdown(JarFile jar, JarEntry entry)
+ throws IOException {
+ String charEnc = null;
+ Attributes atts = entry.getAttributes();
+ if (atts != null) {
+ charEnc = Strings.emptyToNull(atts.getValue("Character-Encoding"));
+ }
+ if (charEnc == null) {
+ charEnc = "UTF-8";
+ }
+ return new MarkdownFormatter().extractTitleFromMarkdown(
+ readWholeEntry(jar, entry),
+ charEnc);
+ }
+
+ private static JarEntry findSource(JarFile jar, String file) {
+ if (file.endsWith(".html")) {
+ int d = file.lastIndexOf('.');
+ return jar.getJarEntry(file.substring(0, d) + ".md");
+ }
+ return null;
+ }
+
+ private static boolean exists(JarEntry entry) {
+ return entry != null && entry.getSize() > 0;
+ }
+
+ private void sendMarkdownAsHtml(JarFile jar, JarEntry entry,
+ String pluginName, ResourceKey key, HttpServletResponse res)
+ throws IOException {
+ byte[] rawmd = readWholeEntry(jar, entry);
+ String encoding = null;
+ Attributes atts = entry.getAttributes();
+ if (atts != null) {
+ encoding = Strings.emptyToNull(atts.getValue("Character-Encoding"));
+ }
+
+ String txtmd = RawParseUtils.decode(
+ Charset.forName(encoding != null ? encoding : "UTF-8"),
+ rawmd);
+ long time = entry.getTime();
+ if (0 < time) {
+ res.setDateHeader("Last-Modified", time);
+ }
+ sendMarkdownAsHtml(txtmd, pluginName, key, res);
+ }
+
+ private void sendResource(JarFile jar, JarEntry entry,
+ ResourceKey key, HttpServletResponse res)
+ throws IOException {
+ byte[] data = null;
+ if (entry.getSize() <= SMALL_RESOURCE) {
+ data = readWholeEntry(jar, entry);
+ }
+
+ 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) {
+ contentType = mimeUtil.getMimeType(entry.getName(), data).toString();
+ }
+
+ long time = entry.getTime();
+ if (0 < time) {
+ res.setDateHeader("Last-Modified", time);
+ }
+ res.setHeader("Content-Length", Long.toString(entry.getSize()));
+ res.setContentType(contentType);
+ if (charEnc != null) {
+ res.setCharacterEncoding(charEnc);
+ }
+ if (data != null) {
+ resourceCache.put(key, new SmallResource(data)
+ .setContentType(contentType)
+ .setCharacterEncoding(charEnc)
+ .setLastModified(time));
+ 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 byte[] readWholeEntry(JarFile jar, JarEntry entry)
+ throws IOException {
+ byte[] data = new byte[(int) entry.getSize()];
+ InputStream in = jar.getInputStream(entry);
+ try {
+ IO.readFully(in, data, 0, data.length);
+ } finally {
+ in.close();
+ }
+ return data;
+ }
+
+ 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);
+ }
+
+ 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/Resource.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java
new file mode 100644
index 0000000..05970af
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.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 java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+abstract class Resource {
+ static final Resource NOT_FOUND = new Resource() {
+ @Override
+ int weigh() {
+ return 0;
+ }
+
+ @Override
+ void send(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ HttpPluginServlet.noCache(res);
+ res.sendError(HttpServletResponse.SC_NOT_FOUND);
+ }
+ };
+
+ abstract int weigh();
+ abstract void send(HttpServletRequest req, HttpServletResponse res)
+ throws IOException;
+}
\ No newline at end of file
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.java
new file mode 100644
index 0000000..068d6b4
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceKey.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.httpd.plugins;
+
+import com.google.gerrit.server.plugins.Plugin;
+
+final class ResourceKey {
+ private final Plugin.CacheKey plugin;
+ private final String resource;
+
+ ResourceKey(Plugin p, String r) {
+ this.plugin = p.getCacheKey();
+ this.resource = r;
+ }
+
+ int weigh() {
+ return resource.length() * 2;
+ }
+
+ @Override
+ public int hashCode() {
+ return plugin.hashCode() * 31 + resource.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ResourceKey) {
+ ResourceKey rk = (ResourceKey) other;
+ return plugin == rk.plugin && resource.equals(rk.resource);
+ }
+ return false;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java
similarity index 63%
rename from gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java
index 43039e1..2514272 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.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.
@@ -12,11 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.httpd.plugins;
+import com.google.common.cache.Weigher;
-/** Configure a cache declared within a {@link CacheModule} instance. */
-public interface UnnamedCacheBinding<K, V> {
- /** Set the name of the cache. */
- public NamedCacheBinding<K, V> name(String cacheName);
+class ResourceWeigher implements Weigher<ResourceKey, Resource> {
+ @Override
+ public int weigh(ResourceKey key, Resource value) {
+ return key.weigh() + value.weigh();
+ }
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java
new file mode 100644
index 0000000..e408f72
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/SmallResource.java
@@ -0,0 +1,66 @@
+// 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 java.io.IOException;
+
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+final class SmallResource extends Resource {
+ private final byte[] data;
+ private String contentType;
+ private String characterEncoding;
+ private long lastModified;
+
+ SmallResource(byte[] data) {
+ this.data = data;
+ }
+
+ SmallResource setLastModified(long when) {
+ this.lastModified = when;
+ return this;
+ }
+
+ SmallResource setContentType(String contentType) {
+ this.contentType = contentType;
+ return this;
+ }
+
+ SmallResource setCharacterEncoding(@Nullable String enc) {
+ this.characterEncoding = enc;
+ return this;
+ }
+
+ @Override
+ int weigh() {
+ return contentType.length() * 2 + data.length;
+ }
+
+ @Override
+ void send(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ if (0 < lastModified) {
+ res.setDateHeader("Last-Modified", lastModified);
+ }
+ res.setContentType(contentType);
+ if (characterEncoding != null) {
+ res.setCharacterEncoding(characterEncoding);
+ }
+ res.setContentLength(data.length);
+ res.getOutputStream().write(data);
+ }
+}
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/BaseServiceImplementation.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
index 26db6f9..62506f0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
@@ -26,6 +26,7 @@
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
import com.google.inject.Provider;
/** Support for services which require a {@link ReviewDb} instance. */
@@ -70,20 +71,14 @@
callback.onFailure(new NoSuchEntityException());
} catch (NoSuchGroupException e) {
callback.onFailure(new NoSuchEntityException());
-
- } catch (OrmException e) {
- if (e.getCause() instanceof Failure) {
- callback.onFailure(e.getCause().getCause());
-
- } else if (e.getCause() instanceof CorruptEntityException) {
- callback.onFailure(e.getCause());
-
- } else if (e.getCause() instanceof NoSuchEntityException) {
- callback.onFailure(e.getCause());
-
- } else {
- callback.onFailure(e);
+ } catch (OrmRuntimeException e) {
+ Exception ex = e;
+ if (e.getCause() instanceof OrmException) {
+ ex = (OrmException) e.getCause();
}
+ handleOrmException(callback, ex);
+ } catch (OrmException e) {
+ handleOrmException(callback, e);
} catch (Failure e) {
if (e.getCause() instanceof NoSuchProjectException
|| e.getCause() instanceof NoSuchChangeException) {
@@ -95,6 +90,19 @@
}
}
+ private static <T> void handleOrmException(
+ final AsyncCallback<T> callback, Exception e) {
+ if (e.getCause() instanceof Failure) {
+ callback.onFailure(e.getCause().getCause());
+ } else if (e.getCause() instanceof CorruptEntityException) {
+ callback.onFailure(e.getCause());
+ } else if (e.getCause() instanceof NoSuchEntityException) {
+ callback.onFailure(e.getCause());
+ } else {
+ callback.onFailure(e);
+ }
+ }
+
/** Exception whose cause is passed into onFailure. */
public static class Failure extends Exception {
private static final long serialVersionUID = 1L;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
index 9a101d3..66b2dc0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/ChangeListServiceImpl.java
@@ -29,7 +29,6 @@
import com.google.gerrit.reviewdb.server.ChangeAccess;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountInfoCacheFactory;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
index f3e8e65e..1baa49b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
@@ -14,6 +14,8 @@
package com.google.gerrit.httpd.rpc;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
import com.google.gerrit.common.data.AccountInfo;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.ReviewerInfo;
@@ -21,8 +23,6 @@
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -31,15 +31,13 @@
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountVisibility;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupMembers;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.patch.AddReviewer;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtorm.server.OrmException;
@@ -55,46 +53,43 @@
import java.util.Map;
import java.util.Set;
+import javax.annotation.Nullable;
+
class SuggestServiceImpl extends BaseServiceImplementation implements
SuggestService {
private static final String MAX_SUFFIX = "\u9fa5";
private final Provider<ReviewDb> reviewDbProvider;
- private final ProjectControl.Factory projectControlFactory;
- private final ProjectCache projectCache;
private final AccountCache accountCache;
- private final GroupControl.Factory groupControlFactory;
private final GroupMembers.Factory groupMembersFactory;
private final IdentifiedUser.GenericFactory identifiedUserFactory;
private final AccountControl.Factory accountControlFactory;
private final ChangeControl.Factory changeControlFactory;
+ private final ProjectControl.Factory projectControlFactory;
private final Config cfg;
- private final GroupCache groupCache;
+ private final GroupBackend groupBackend;
private final boolean suggestAccounts;
@Inject
SuggestServiceImpl(final Provider<ReviewDb> schema,
- final ProjectControl.Factory projectControlFactory,
- final ProjectCache projectCache, final AccountCache accountCache,
- final GroupControl.Factory groupControlFactory,
+ final AccountCache accountCache,
final GroupMembers.Factory groupMembersFactory,
final Provider<CurrentUser> currentUser,
final IdentifiedUser.GenericFactory identifiedUserFactory,
final AccountControl.Factory accountControlFactory,
final ChangeControl.Factory changeControlFactory,
- @GerritServerConfig final Config cfg, final GroupCache groupCache) {
+ final ProjectControl.Factory projectControlFactory,
+ @GerritServerConfig final Config cfg, final GroupBackend groupBackend) {
super(schema, currentUser);
this.reviewDbProvider = schema;
- this.projectControlFactory = projectControlFactory;
- this.projectCache = projectCache;
this.accountCache = accountCache;
- this.groupControlFactory = groupControlFactory;
this.groupMembersFactory = groupMembersFactory;
this.identifiedUserFactory = identifiedUserFactory;
this.accountControlFactory = accountControlFactory;
this.changeControlFactory = changeControlFactory;
+ this.projectControlFactory = projectControlFactory;
this.cfg = cfg;
- this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
if ("OFF".equals(cfg.getString("suggest", null, "accounts"))) {
this.suggestAccounts = false;
@@ -111,28 +106,6 @@
}
}
- public void suggestProjectNameKey(final String query, final int limit,
- final AsyncCallback<List<Project.NameKey>> callback) {
- final int max = 10;
- final int n = limit <= 0 ? max : Math.min(limit, max);
-
- final List<Project.NameKey> r = new ArrayList<Project.NameKey>(n);
- for (final Project.NameKey nameKey : projectCache.byName(query)) {
- final ProjectControl ctl;
- try {
- ctl = projectControlFactory.validateFor(nameKey);
- } catch (NoSuchProjectException e) {
- continue;
- }
-
- r.add(ctl.getProject().getNameKey());
- if (r.size() == n) {
- break;
- }
- }
- callback.onSuccess(r);
- }
-
private interface VisibilityControl {
boolean isVisible(Account account) throws OrmException;
}
@@ -205,33 +178,31 @@
public void suggestAccountGroup(final String query, final int limit,
final AsyncCallback<List<GroupReference>> callback) {
+ suggestAccountGroupForProject(null, query, limit, callback);
+ }
+
+ public void suggestAccountGroupForProject(final Project.NameKey project,
+ final String query, final int limit,
+ final AsyncCallback<List<GroupReference>> callback) {
run(callback, new Action<List<GroupReference>>() {
- public List<GroupReference> run(final ReviewDb db) throws OrmException {
- return suggestAccountGroup(db, query, limit);
+ public List<GroupReference> run(final ReviewDb db) {
+ ProjectControl projectControl = null;
+ if (project != null) {
+ try {
+ projectControl = projectControlFactory.controlFor(project);
+ } catch (NoSuchProjectException e) {
+ return Collections.emptyList();
+ }
+ }
+ return suggestAccountGroup(projectControl, query, limit);
}
});
}
- private List<GroupReference> suggestAccountGroup(final ReviewDb db,
- final String query, final int limit) throws OrmException {
- final String a = query;
- final String b = a + MAX_SUFFIX;
- final int max = 10;
- final int n = limit <= 0 ? max : Math.min(limit, max);
- List<GroupReference> r = new ArrayList<GroupReference>(n);
- for (AccountGroupName group : db.accountGroupNames().suggestByName(a, b, n)) {
- try {
- if (groupControlFactory.controlFor(group.getId()).isVisible()) {
- AccountGroup g = groupCache.get(group.getId());
- if (g != null && g.getGroupUUID() != null) {
- r.add(GroupReference.forGroup(g));
- }
- }
- } catch (NoSuchGroupException e) {
- continue;
- }
- }
- return r;
+ private List<GroupReference> suggestAccountGroup(
+ @Nullable final ProjectControl projectControl, final String query, final int limit) {
+ final int n = limit <= 0 ? 10 : Math.min(limit, 10);
+ return Lists.newArrayList(Iterables.limit(groupBackend.suggest(query), n));
}
@Override
@@ -258,7 +229,9 @@
public boolean isVisible(Account account) throws OrmException {
IdentifiedUser who =
identifiedUserFactory.create(reviewDbProvider, account.getId());
- return changeControl.forUser(who).isVisible(reviewDbProvider.get());
+ // we can't use changeControl directly as it won't suggest reviewers
+ // to drafts
+ return changeControl.forUser(who).isRefVisible();
}
};
@@ -270,7 +243,7 @@
reviewer.add(new ReviewerInfo(a));
}
final List<GroupReference> suggestedAccountGroups =
- suggestAccountGroup(db, query, limit);
+ suggestAccountGroup(changeControl.getProjectControl(), query, limit);
for (final GroupReference g : suggestedAccountGroups) {
if (suggestGroupAsReviewer(changeControl.getProject().getNameKey(), g)) {
reviewer.add(new ReviewerInfo(g));
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
index 9de7d88..931605c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
@@ -14,16 +14,15 @@
package com.google.gerrit.httpd.rpc;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.data.GerritConfig;
import com.google.gerrit.common.data.SshHostKey;
import com.google.gerrit.common.data.SystemInfoService;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -34,6 +33,7 @@
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
@@ -44,32 +44,31 @@
private static final JSch JSCH = new JSch();
- private final SchemaFactory<ReviewDb> schema;
private final List<HostKey> hostKeys;
private final Provider<HttpServletRequest> httpRequest;
private final Provider<GerritConfig> config;
+ private final ProjectCache projectCache;
@Inject
- SystemInfoServiceImpl(final SchemaFactory<ReviewDb> sf, final SshInfo daemon,
- final Provider<HttpServletRequest> hsr, Provider<GerritConfig> cfg) {
- schema = sf;
+ SystemInfoServiceImpl(final SshInfo daemon,
+ final Provider<HttpServletRequest> hsr, final Provider<GerritConfig> cfg,
+ final ProjectCache pc) {
hostKeys = daemon.getHostKeys();
httpRequest = hsr;
config = cfg;
+ projectCache = pc;
}
public void contributorAgreements(
final AsyncCallback<List<ContributorAgreement>> callback) {
- try {
- final ReviewDb db = schema.open();
- try {
- callback.onSuccess(db.contributorAgreements().active().toList());
- } finally {
- db.close();
- }
- } catch (OrmException e) {
- callback.onFailure(e);
+ Collection<ContributorAgreement> agreements =
+ projectCache.getAllProjects().getConfig().getContributorAgreements();
+ List<ContributorAgreement> cas =
+ Lists.newArrayListWithCapacity(agreements.size());
+ for (ContributorAgreement ca : agreements) {
+ cas.add(ca.forUi());
}
+ callback.onSuccess(cas);
}
public void daemonHostKeys(final AsyncCallback<List<SshHostKey>> callback) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java
new file mode 100644
index 0000000..0d0ffe7
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java
@@ -0,0 +1,188 @@
+// 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.rpc.account;
+
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_ACCOUNT;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
+import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
+import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
+import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
+import static com.google.gerrit.common.data.GlobalCapability.START_REPLICATION;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.git.QueueProvider;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.kohsuke.args4j.Option;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class AccountCapabilitiesServlet extends RestApiServlet {
+ private static final long serialVersionUID = 1L;
+ private final ParameterParser paramParser;
+ private final Provider<Impl> factory;
+
+ @Inject
+ AccountCapabilitiesServlet(
+ ParameterParser paramParser, Provider<Impl> factory) {
+ this.paramParser = paramParser;
+ this.factory = factory;
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ Impl impl = factory.get();
+ if (acceptsJson(req)) {
+ impl.format = OutputFormat.JSON_COMPACT;
+ }
+ if (paramParser.parse(impl, req, res)) {
+ impl.compute();
+
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ OutputStreamWriter out = new OutputStreamWriter(buf, "UTF-8");
+ if (impl.format.isJson()) {
+ res.setContentType(JSON_TYPE);
+ buf.write(JSON_MAGIC);
+ impl.format.newGson().toJson(
+ impl.have,
+ new TypeToken<Map<String, Object>>() {}.getType(),
+ out);
+ out.flush();
+ buf.write('\n');
+ } else {
+ res.setContentType("text/plain");
+ for (Map.Entry<String, Object> e : impl.have.entrySet()) {
+ out.write(e.getKey());
+ if (!(e.getValue() instanceof Boolean)) {
+ out.write(": ");
+ out.write(e.getValue().toString());
+ }
+ out.write('\n');
+ }
+ out.flush();
+ }
+ res.setCharacterEncoding("UTF-8");
+ send(req, res, buf.toByteArray());
+ }
+ }
+
+ static class Impl {
+ private final CapabilityControl cc;
+ private final Map<String, Object> have;
+
+ @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+ private OutputFormat format = OutputFormat.TEXT;
+
+ @Option(name = "-q", metaVar = "CAP", multiValued = true, usage = "Capability to inspect")
+ void addQuery(String name) {
+ if (query == null) {
+ query = Sets.newHashSet();
+ }
+ query.add(name.toLowerCase());
+ }
+ private Set<String> query;
+
+ @Inject
+ Impl(CurrentUser user) {
+ cc = user.getCapabilities();
+ have = Maps.newLinkedHashMap();
+ }
+
+ void compute() {
+ for (String name : GlobalCapability.getAllNames()) {
+ if (!name.equals(PRIORITY) && want(name) && cc.canPerform(name)) {
+ if (GlobalCapability.hasRange(name)) {
+ have.put(name, new Range(cc.getRange(name)));
+ } else {
+ have.put(name, true);
+ }
+ }
+ }
+
+ have.put(CREATE_ACCOUNT, cc.canCreateAccount());
+ have.put(CREATE_GROUP, cc.canCreateGroup());
+ have.put(CREATE_PROJECT, cc.canCreateProject());
+ have.put(KILL_TASK, cc.canKillTask());
+ have.put(VIEW_CACHES, cc.canViewCaches());
+ have.put(FLUSH_CACHES, cc.canFlushCaches());
+ have.put(VIEW_CONNECTIONS, cc.canViewConnections());
+ have.put(VIEW_QUEUE, cc.canViewQueue());
+ have.put(START_REPLICATION, cc.canStartReplication());
+
+ QueueProvider.QueueType queue = cc.getQueueType();
+ if (queue != QueueProvider.QueueType.INTERACTIVE
+ || (query != null && query.contains(PRIORITY))) {
+ have.put(PRIORITY, queue);
+ }
+
+ Iterator<Map.Entry<String, Object>> itr = have.entrySet().iterator();
+ while (itr.hasNext()) {
+ Map.Entry<String, Object> e = itr.next();
+ if (!want(e.getKey())) {
+ itr.remove();
+ } else if (e.getValue() instanceof Boolean && !((Boolean) e.getValue())) {
+ itr.remove();
+ }
+ }
+ }
+
+ private boolean want(String name) {
+ return query == null || query.contains(name.toLowerCase());
+ }
+ }
+
+ private static class Range {
+ private transient PermissionRange range;
+ @SuppressWarnings("unused")
+ private int min;
+ @SuppressWarnings("unused")
+ private int max;
+
+ Range(PermissionRange r) {
+ range = r;
+ min = r.getMin();
+ max = r.getMax();
+ }
+
+ @Override
+ public String toString() {
+ return range.toString();
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index aa94759f..8cdccac 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -16,6 +16,7 @@
import com.google.gerrit.common.ChangeHooks;
import com.google.gerrit.common.data.AccountSecurity;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.data.GroupDetail;
import com.google.gerrit.common.errors.ContactInformationStoreException;
import com.google.gerrit.common.errors.InvalidSshKeyException;
@@ -25,12 +26,13 @@
import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.reviewdb.client.ContactInformation;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
@@ -42,12 +44,14 @@
import com.google.gerrit.server.account.ChangeUserName;
import com.google.gerrit.server.account.ClearPassword;
import com.google.gerrit.server.account.GeneratePassword;
+import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.contact.ContactStore;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.mail.EmailTokenVerifier;
import com.google.gerrit.server.mail.RegisterNewEmailSender;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.VoidResult;
@@ -68,6 +72,7 @@
private final ContactStore contactStore;
private final AuthConfig authConfig;
private final Realm realm;
+ private final ProjectCache projectCache;
private final Provider<IdentifiedUser> user;
private final EmailTokenVerifier emailTokenVerifier;
private final RegisterNewEmailSender.Factory registerNewEmailFactory;
@@ -85,12 +90,13 @@
private final MyGroupsFactory.Factory myGroupsFactory;
private final ChangeHooks hooks;
+ private final GroupCache groupCache;
@Inject
AccountSecurityImpl(final Provider<ReviewDb> schema,
final Provider<CurrentUser> currentUser, final ContactStore cs,
final AuthConfig ac, final Realm r, final Provider<IdentifiedUser> u,
- final EmailTokenVerifier etv,
+ final EmailTokenVerifier etv, final ProjectCache pc,
final RegisterNewEmailSender.Factory esf, final SshKeyCache skc,
final AccountByEmailCache abec, final AccountCache uac,
final AccountManager am,
@@ -100,13 +106,14 @@
final DeleteExternalIds.Factory deleteExternalIdsFactory,
final ExternalIdDetailFactory.Factory externalIdDetailFactory,
final MyGroupsFactory.Factory myGroupsFactory,
- final ChangeHooks hooks) {
+ final ChangeHooks hooks, final GroupCache groupCache) {
super(schema, currentUser);
contactStore = cs;
authConfig = ac;
realm = r;
user = u;
emailTokenVerifier = etv;
+ projectCache = pc;
registerNewEmailFactory = esf;
sshKeyCache = skc;
byEmailCache = abec;
@@ -122,6 +129,7 @@
this.externalIdDetailFactory = externalIdDetailFactory;
this.myGroupsFactory = myGroupsFactory;
this.hooks = hooks;
+ this.groupCache = groupCache;
}
public void mySshKeys(final AsyncCallback<List<AccountSshKey>> callback) {
@@ -260,24 +268,43 @@
return a != null && a.equals(b);
}
- public void enterAgreement(final ContributorAgreement.Id id,
+ public void enterAgreement(final String agreementName,
final AsyncCallback<VoidResult> callback) {
run(callback, new Action<VoidResult>() {
public VoidResult run(final ReviewDb db) throws OrmException, Failure {
- final ContributorAgreement cla = db.contributorAgreements().get(id);
- if (cla == null || !cla.isActive()) {
+ ContributorAgreement ca = projectCache.getAllProjects().getConfig()
+ .getContributorAgreement(agreementName);
+ if (ca == null) {
throw new Failure(new NoSuchEntityException());
}
- final AccountAgreement a =
- new AccountAgreement(new AccountAgreement.Key(user.get()
- .getAccountId(), id));
- if (cla.isAutoVerify()) {
- a.review(AccountAgreement.Status.VERIFIED, null);
-
- hooks.doClaSignupHook(user.get().getAccount(), cla);
+ if (ca.getAutoVerify() == null) {
+ throw new Failure(new IllegalStateException(
+ "cannot enter a non-autoVerify agreement"));
+ } else if (ca.getAutoVerify().getUUID() == null) {
+ throw new Failure(new NoSuchEntityException());
}
- db.accountAgreements().insert(Collections.singleton(a));
+
+ AccountGroup group = groupCache.get(ca.getAutoVerify().getUUID());
+ if (group == null) {
+ throw new Failure(new NoSuchEntityException());
+ }
+
+ Account account = user.get().getAccount();
+ hooks.doClaSignupHook(account, ca);
+
+ final AccountGroupMember.Key key =
+ new AccountGroupMember.Key(account.getId(), group.getId());
+ AccountGroupMember m = db.accountGroupMembers().get(key);
+ if (m == null) {
+ m = new AccountGroupMember(key);
+ db.accountGroupMembersAudit().insert(
+ Collections.singleton(
+ new AccountGroupMemberAudit(m, account.getId())));
+ db.accountGroupMembers().insert(Collections.singleton(m));
+ accountCache.evict(m.getAccountId());
+ }
+
return VoidResult.INSTANCE;
}
});
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
index 39712e4..712f5a2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
@@ -14,83 +14,71 @@
package com.google.gerrit.httpd.rpc.account;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
import com.google.gerrit.common.data.AgreementInfo;
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
import java.util.List;
import java.util.Map;
class AgreementInfoFactory extends Handler<AgreementInfo> {
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
interface Factory {
AgreementInfoFactory create();
}
- private final ReviewDb db;
- private final GroupCache groupCache;
private final IdentifiedUser user;
+ private final ProjectCache projectCache;
private AgreementInfo info;
@Inject
- AgreementInfoFactory(final ReviewDb db, final GroupCache groupCache,
- final IdentifiedUser user) {
- this.db = db;
- this.groupCache = groupCache;
+ AgreementInfoFactory(final IdentifiedUser user,
+ final ProjectCache projectCache) {
this.user = user;
+ this.projectCache = projectCache;
}
@Override
public AgreementInfo call() throws Exception {
- final List<AccountAgreement> userAccepted =
- db.accountAgreements().byAccount(user.getAccountId()).toList();
+ List<String> accepted = Lists.newArrayList();
+ Map<String, ContributorAgreement> agreements = Maps.newHashMap();
+ Collection<ContributorAgreement> cas =
+ projectCache.getAllProjects().getConfig().getContributorAgreements();
+ for (ContributorAgreement ca : cas) {
+ agreements.put(ca.getName(), ca.forUi());
- Collections.reverse(userAccepted);
-
- final List<AccountGroupAgreement> groupAccepted =
- new ArrayList<AccountGroupAgreement>();
- for (final AccountGroup.UUID groupUUID : user.getEffectiveGroups().getKnownGroups()) {
- AccountGroup group = groupCache.get(groupUUID);
- if (group == null) {
- continue;
+ List<AccountGroup.UUID> groupIds = Lists.newArrayList();
+ for (PermissionRule rule : ca.getAccepted()) {
+ if ((rule.getAction() == Action.ALLOW) && (rule.getGroup() != null)) {
+ if (rule.getGroup().getUUID() == null) {
+ log.warn("group \"" + rule.getGroup().getName() + "\" does not " +
+ " exist, referenced in CLA \"" + ca.getName() + "\"");
+ } else {
+ groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get()));
+ }
+ }
}
-
- final List<AccountGroupAgreement> temp =
- db.accountGroupAgreements().byGroup(group.getId()).toList();
-
- Collections.reverse(temp);
-
- groupAccepted.addAll(temp);
- }
-
- final Map<ContributorAgreement.Id, ContributorAgreement> agreements =
- new HashMap<ContributorAgreement.Id, ContributorAgreement>();
- for (final AccountAgreement a : userAccepted) {
- final ContributorAgreement.Id id = a.getAgreementId();
- if (!agreements.containsKey(id)) {
- agreements.put(id, db.contributorAgreements().get(id));
- }
- }
- for (final AccountGroupAgreement a : groupAccepted) {
- final ContributorAgreement.Id id = a.getAgreementId();
- if (!agreements.containsKey(id)) {
- agreements.put(id, db.contributorAgreements().get(id));
+ if (user.getEffectiveGroups().containsAnyOf(groupIds)) {
+ accepted.add(ca.getName());
}
}
info = new AgreementInfo();
- info.setUserAccepted(userAccepted);
- info.setGroupAccepted(groupAccepted);
+ info.setAccepted(accepted);
info.setAgreements(agreements);
return info;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
index e90c2bf..c7b4c79 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
@@ -18,6 +18,7 @@
import com.google.gerrit.common.data.GroupDetail;
import com.google.gerrit.common.data.GroupList;
import com.google.gerrit.common.data.GroupOptions;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.InactiveAccountException;
import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.common.errors.NoSuchAccountException;
@@ -34,29 +35,27 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.account.GroupIncludeCache;
-import com.google.gerrit.server.account.Realm;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.VoidResult;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import java.util.ArrayList;
import java.util.Collections;
-import java.util.Comparator;
import java.util.HashSet;
-import java.util.List;
import java.util.Set;
class GroupAdminServiceImpl extends BaseServiceImplementation implements
GroupAdminService {
private final AccountCache accountCache;
private final AccountResolver accountResolver;
- private final Realm accountRealm;
private final GroupCache groupCache;
+ private final GroupBackend groupBackend;
private final GroupIncludeCache groupIncludeCache;
private final GroupControl.Factory groupControlFactory;
@@ -70,8 +69,9 @@
final Provider<IdentifiedUser> currentUser,
final AccountCache accountCache,
final GroupIncludeCache groupIncludeCache,
- final AccountResolver accountResolver, final Realm accountRealm,
+ final AccountResolver accountResolver,
final GroupCache groupCache,
+ final GroupBackend groupBackend,
final GroupControl.Factory groupControlFactory,
final CreateGroup.Factory createGroupFactory,
final RenameGroup.Factory renameGroupFactory,
@@ -81,8 +81,8 @@
this.accountCache = accountCache;
this.groupIncludeCache = groupIncludeCache;
this.accountResolver = accountResolver;
- this.accountRealm = accountRealm;
this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
this.groupControlFactory = groupControlFactory;
this.createGroupFactory = createGroupFactory;
this.renameGroupFactory = renameGroupFactory;
@@ -145,13 +145,13 @@
final AccountGroup group = db.accountGroups().get(groupId);
assertAmGroupOwner(db, group);
- final AccountGroup owner =
- groupCache.get(new AccountGroup.NameKey(newOwnerName));
+ GroupReference owner =
+ GroupBackends.findExactSuggestion(groupBackend, newOwnerName);
if (owner == null) {
throw new Failure(new NoSuchEntityException());
}
- group.setOwnerGroupId(owner.getId());
+ group.setOwnerGroupUUID(owner.getUUID());
db.accountGroups().update(Collections.singleton(group));
groupCache.evict(group);
return VoidResult.INSTANCE;
@@ -178,43 +178,13 @@
});
}
- public void changeExternalGroup(final AccountGroup.Id groupId,
- final AccountGroup.ExternalNameKey bindTo,
- final AsyncCallback<VoidResult> callback) {
- run(callback, new Action<VoidResult>() {
- public VoidResult run(final ReviewDb db) throws OrmException, Failure {
- final AccountGroup group = db.accountGroups().get(groupId);
- assertAmGroupOwner(db, group);
- group.setExternalNameKey(bindTo);
- db.accountGroups().update(Collections.singleton(group));
- groupCache.evict(group);
- return VoidResult.INSTANCE;
- }
- });
- }
-
- public void searchExternalGroups(final String searchFilter,
- final AsyncCallback<List<AccountGroup.ExternalNameKey>> callback) {
- final ArrayList<AccountGroup.ExternalNameKey> matches =
- new ArrayList<AccountGroup.ExternalNameKey>(
- accountRealm.lookupGroups(searchFilter));
- Collections.sort(matches, new Comparator<AccountGroup.ExternalNameKey>() {
- @Override
- public int compare(AccountGroup.ExternalNameKey a,
- AccountGroup.ExternalNameKey b) {
- return a.get().compareTo(b.get());
- }
- });
- callback.onSuccess(matches);
- }
-
public void addGroupMember(final AccountGroup.Id groupId,
final String nameOrEmail, final AsyncCallback<GroupDetail> callback) {
run(callback, new Action<GroupDetail>() {
public GroupDetail run(ReviewDb db) throws OrmException, Failure,
NoSuchGroupException {
final GroupControl control = groupControlFactory.validateFor(groupId);
- if (control.getAccountGroup().getType() != AccountGroup.Type.INTERNAL) {
+ if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
throw new Failure(new NameAlreadyUsedException());
}
@@ -249,7 +219,7 @@
public GroupDetail run(ReviewDb db) throws OrmException, Failure,
NoSuchGroupException {
final GroupControl control = groupControlFactory.validateFor(groupId);
- if (control.getAccountGroup().getType() != AccountGroup.Type.INTERNAL) {
+ if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
throw new Failure(new NameAlreadyUsedException());
}
@@ -282,7 +252,7 @@
public VoidResult run(final ReviewDb db) throws OrmException,
NoSuchGroupException, Failure {
final GroupControl control = groupControlFactory.validateFor(groupId);
- if (control.getAccountGroup().getType() != AccountGroup.Type.INTERNAL) {
+ if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
throw new Failure(new NameAlreadyUsedException());
}
@@ -336,7 +306,7 @@
public VoidResult run(final ReviewDb db) throws OrmException,
NoSuchGroupException, Failure {
final GroupControl control = groupControlFactory.validateFor(groupId);
- if (control.getAccountGroup().getType() != AccountGroup.Type.INTERNAL) {
+ if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
throw new Failure(new NameAlreadyUsedException());
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
similarity index 94%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
index 9c4c598..cf443e7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.httpd;
+package com.google.gerrit.httpd.rpc.change;
import com.google.gerrit.server.query.change.QueryProcessor;
import com.google.gerrit.server.query.change.QueryProcessor.OutputFormat;
@@ -29,12 +29,12 @@
import javax.servlet.http.HttpServletResponse;
@Singleton
-public class ChangeQueryServlet extends HttpServlet {
+public class DeprecatedChangeQueryServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final Provider<QueryProcessor> processor;
@Inject
- ChangeQueryServlet(Provider<QueryProcessor> processor) {
+ DeprecatedChangeQueryServlet(Provider<QueryProcessor> processor) {
this.processor = processor;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java
new file mode 100644
index 0000000..91fc5b0
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java
@@ -0,0 +1,83 @@
+// 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.rpc.change;
+
+import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ListChanges;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class ListChangesServlet extends RestApiServlet {
+ private static final long serialVersionUID = 1L;
+ private static final Logger log = LoggerFactory.getLogger(ListChangesServlet.class);
+ private final ParameterParser paramParser;
+ private final Provider<ListChanges> factory;
+
+ @Inject
+ ListChangesServlet(ParameterParser paramParser, Provider<ListChanges> ls) {
+ this.paramParser = paramParser;
+ this.factory = ls;
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ ListChanges impl = factory.get();
+ if (acceptsJson(req)) {
+ impl.setFormat(OutputFormat.JSON_COMPACT);
+ }
+ if (paramParser.parse(impl, req, res)) {
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ if (impl.getFormat().isJson()) {
+ buf.write(JSON_MAGIC);
+ }
+
+ Writer out = new BufferedWriter(new OutputStreamWriter(buf, "UTF-8"));
+ try {
+ impl.query(out);
+ } catch (QueryParseException e) {
+ res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ sendText(req, res, e.getMessage());
+ return;
+ } catch (OrmException e) {
+ log.error("Error querying /changes/", e);
+ res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ return;
+ }
+ out.flush();
+
+ res.setContentType(impl.getFormat().isJson() ? JSON_TYPE : "text/plain");
+ res.setCharacterEncoding("UTF-8");
+ send(req, res, buf.toByteArray());
+ }
+ }
+}
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 47a9395..6b5299a 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));
@@ -133,20 +143,20 @@
detail.setCanEdit(control.getRefControl().canWrite());
- if (detail.getChange().getStatus().isOpen()) {
- List<SubmitRecord> submitRecords = control.canSubmit(db, patch.getId());
- for (SubmitRecord rec : submitRecords) {
- if (rec.labels != null) {
- for (SubmitRecord.Label lbl : rec.labels) {
- aic.want(lbl.appliedBy);
- }
- }
- if (rec.status == SubmitRecord.Status.OK && control.getRefControl().canSubmit()) {
- detail.setCanSubmit(true);
+ List<SubmitRecord> submitRecords = control.getSubmitRecords(db, patch);
+ for (SubmitRecord rec : submitRecords) {
+ if (rec.labels != null) {
+ for (SubmitRecord.Label lbl : rec.labels) {
+ aic.want(lbl.appliedBy);
}
}
- detail.setSubmitRecords(submitRecords);
+ if (detail.getChange().getStatus().isOpen()
+ && rec.status == SubmitRecord.Status.OK
+ && control.getRefControl().canSubmit()) {
+ detail.setCanSubmit(true);
+ }
}
+ detail.setSubmitRecords(submitRecords);
patchsetsById = new HashMap<PatchSet.Id, PatchSet>();
loadPatchSets();
@@ -264,13 +274,16 @@
}
}
- final RevId cprev = loader.patchSet.getRevision();
final Set<Change.Id> descendants = new HashSet<Change.Id>();
- if (cprev != null) {
- for (PatchSetAncestor a : db.patchSetAncestors().descendantsOf(cprev)) {
- final Change.Id ck = a.getPatchSet().getParentKey();
- if (descendants.add(ck)) {
- changesToGet.add(a.getPatchSet().getParentKey());
+ RevId cprev;
+ for (PatchSet p : detail.getPatchSets()) {
+ cprev = p.getRevision();
+ if (cprev != null) {
+ for (PatchSetAncestor a : db.patchSetAncestors().descendantsOf(cprev)) {
+ final Change.Id ck = a.getPatchSet().getParentKey();
+ if (descendants.add(ck)) {
+ changesToGet.add(ck);
+ }
}
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
index 66c11c0..ecd8b54 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
@@ -19,8 +19,8 @@
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.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtjsonrpc.common.VoidResult;
@@ -38,7 +38,7 @@
private final ChangeControl.Factory changeControlFactory;
private final ReviewDb db;
private final GitRepositoryManager gitManager;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final PatchSet.Id patchSetId;
@@ -46,7 +46,7 @@
DeleteDraftChange(final ReviewDb db,
final ChangeControl.Factory changeControlFactory,
final GitRepositoryManager gitManager,
- final ReplicationQueue replication,
+ final GitReferenceUpdated replication,
@Assisted final PatchSet.Id patchSetId) {
this.changeControlFactory = changeControlFactory;
this.db = db;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
index de3ff2f..95a8e26 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
@@ -31,6 +31,7 @@
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
@@ -108,18 +109,19 @@
final PatchList list;
- if (psIdBase != null) {
- oldId = toObjectId(psIdBase);
- newId = toObjectId(psIdNew);
+ try {
+ if (psIdBase != null) {
+ oldId = toObjectId(psIdBase);
+ newId = toObjectId(psIdNew);
- projectKey = control.getProject().getNameKey();
+ projectKey = control.getProject().getNameKey();
- list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
- } else { // OK, means use base to compare
- list = patchListCache.get(control.getChange(), patchSet);
- if (list == null) {
- throw new NoSuchEntityException();
+ list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
+ } else { // OK, means use base to compare
+ list = patchListCache.get(control.getChange(), patchSet);
}
+ } catch (PatchListNotAvailableException e) {
+ throw new NoSuchEntityException();
}
final List<Patch> patches = list.toPatchList(patchSet.getId());
@@ -185,7 +187,8 @@
return new PatchListKey(projectKey, oldId, newId, whitespace);
}
- private PatchList listFor(final PatchListKey key) {
+ private PatchList listFor(PatchListKey key)
+ throws PatchListNotAvailableException {
return patchListCache.get(key);
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
index 638bfe3..37a1c92 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
@@ -83,7 +83,8 @@
final Change.Id changeId = patchSetId.getParentKey();
final ChangeControl control = changeControlFactory.validateFor(changeId);
change = control.getChange();
- patchSetInfo = infoFactory.get(db, patchSetId);
+ PatchSet patchSet = db.patchSets().get(patchSetId);
+ patchSetInfo = infoFactory.get(change, patchSet);
drafts = db.patchComments().draftByPatchSetAuthor(patchSetId, user.getAccountId()).toList();
aic.want(change.getOwner());
@@ -119,7 +120,7 @@
.toList();
boolean couldSubmit = false;
- List<SubmitRecord> submitRecords = control.canSubmit(db, patchSetId);
+ List<SubmitRecord> submitRecords = control.canSubmit(db, patchSet);
for (SubmitRecord rec : submitRecords) {
if (rec.status == SubmitRecord.Status.OK) {
couldSubmit = true;
@@ -144,6 +145,7 @@
switch (lbl.status) {
case OK:
+ case MAY:
ok++;
break;
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/RebaseChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
index 3c29074..e71e302 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
@@ -15,17 +15,17 @@
package com.google.gerrit.httpd.rpc.changedetail;
import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.common.data.ChangeDetail;
import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.mail.RebasedPatchSetSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -54,7 +54,7 @@
private final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory;
private final ChangeDetailFactory.Factory changeDetailFactory;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final PatchSet.Id patchSetId;
@@ -65,7 +65,7 @@
private final PersonIdent myIdent;
- private final ApprovalTypes approvalTypes;
+ private final ApprovalsUtil approvalsUtil;
@Inject
RebaseChange(final ChangeControl.Factory changeControlFactory,
@@ -75,9 +75,9 @@
@Assisted final PatchSet.Id patchSetId, final ChangeHookRunner hooks,
final GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
- final ReplicationQueue replication,
+ final GitReferenceUpdated replication,
@GerritPersonIdent final PersonIdent myIdent,
- final ApprovalTypes approvalTypes) {
+ final ApprovalsUtil approvalsUtil) {
this.changeControlFactory = changeControlFactory;
this.db = db;
this.currentUser = currentUser;
@@ -92,7 +92,7 @@
this.replication = replication;
this.myIdent = myIdent;
- this.approvalTypes = approvalTypes;
+ this.approvalsUtil = approvalsUtil;
}
@Override
@@ -103,7 +103,7 @@
ChangeUtil.rebaseChange(patchSetId, currentUser, db,
rebasedPatchSetSenderFactory, hooks, gitManager, patchSetInfoFactory,
- replication, myIdent, changeControlFactory, approvalTypes);
+ replication, myIdent, changeControlFactory, approvalsUtil);
return changeDetailFactory.create(patchSetId.getParentKey()).call();
}
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/RevertChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
index 9fb9ae1..60a03bd 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
@@ -24,8 +24,8 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.mail.RevertedSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -54,7 +54,7 @@
private final IdentifiedUser currentUser;
private final RevertedSender.Factory revertedSenderFactory;
private final ChangeDetailFactory.Factory changeDetailFactory;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final PatchSet.Id patchSetId;
@Nullable
@@ -76,7 +76,7 @@
@Assisted @Nullable final String message, final ChangeHooks hooks,
final GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
- final ReplicationQueue replication,
+ final GitReferenceUpdated replication,
@GerritPersonIdent final PersonIdent myIdent) {
this.changeControlFactory = changeControlFactory;
this.db = db;
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-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
index 66b3278..8ad9a10 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptFactory.java
@@ -35,6 +35,7 @@
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListEntry;
import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
@@ -141,6 +142,9 @@
} catch (RepositoryNotFoundException e) {
log.error("Repository " + projectKey + " not found", e);
throw new NoSuchChangeException(changeId, e);
+ } catch (IOException e) {
+ log.error("Cannot open repository " + projectKey, e);
+ throw new NoSuchChangeException(changeId, e);
}
try {
final PatchList list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
@@ -151,12 +155,12 @@
content.getOldName(), //
content.getNewName());
- try {
return b.toPatchScript(content, comments, history);
- } catch (IOException e) {
- log.error("File content unavailable", e);
- throw new NoSuchChangeException(changeId, e);
- }
+ } catch (PatchListNotAvailableException e) {
+ throw new NoSuchChangeException(changeId, e);
+ } catch (IOException e) {
+ log.error("File content unavailable", e);
+ throw new NoSuchChangeException(changeId, e);
} finally {
git.close();
}
@@ -166,7 +170,8 @@
return new PatchListKey(projectKey, aId, bId, whitespace);
}
- private PatchList listFor(final PatchListKey key) {
+ private PatchList listFor(final PatchListKey key)
+ throws PatchListNotAvailableException {
return patchListCache.get(key);
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
index 44c3141..97e9bb4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
@@ -22,8 +22,8 @@
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.RefControl;
@@ -59,7 +59,7 @@
private final ListBranches.Factory listBranchesFactory;
private final IdentifiedUser identifiedUser;
private final GitRepositoryManager repoManager;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated referenceUpdated;
private final ChangeHooks hooks;
private final Project.NameKey projectName;
@@ -71,7 +71,7 @@
final ListBranches.Factory listBranchesFactory,
final IdentifiedUser identifiedUser,
final GitRepositoryManager repoManager,
- final ReplicationQueue replication,
+ GitReferenceUpdated referenceUpdated,
final ChangeHooks hooks,
@Assisted Project.NameKey projectName,
@@ -81,7 +81,7 @@
this.listBranchesFactory = listBranchesFactory;
this.identifiedUser = identifiedUser;
this.repoManager = repoManager;
- this.replication = replication;
+ this.referenceUpdated = referenceUpdated;
this.hooks = hooks;
this.projectName = projectName;
@@ -144,7 +144,7 @@
case FAST_FORWARD:
case NEW:
case NO_CHANGE:
- replication.scheduleUpdate(name.getParentKey(), refname);
+ referenceUpdated.fire(name.getParentKey(), refname);
hooks.doRefUpdatedHook(name, u, identifiedUser.getAccount());
break;
default: {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
index 777329b..c991c47 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -22,9 +22,9 @@
import com.google.gerrit.common.errors.InvalidNameException;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.NoSuchProjectException;
@@ -60,7 +60,7 @@
private final ProjectAccessFactory.Factory projectAccessFactory;
private final ProjectControl.Factory projectControlFactory;
private final ProjectCache projectCache;
- private final GroupCache groupCache;
+ private final GroupBackend groupBackend;
private final MetaDataUpdate.User metaDataUpdateFactory;
private final Project.NameKey projectName;
@@ -71,7 +71,7 @@
@Inject
ChangeProjectAccess(final ProjectAccessFactory.Factory projectAccessFactory,
final ProjectControl.Factory projectControlFactory,
- final ProjectCache projectCache, final GroupCache groupCache,
+ final ProjectCache projectCache, final GroupBackend groupBackend,
final MetaDataUpdate.User metaDataUpdateFactory,
@Assisted final Project.NameKey projectName,
@@ -81,7 +81,7 @@
this.projectAccessFactory = projectAccessFactory;
this.projectControlFactory = projectControlFactory;
this.projectCache = projectCache;
- this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
this.metaDataUpdateFactory = metaDataUpdateFactory;
this.projectName = projectName;
@@ -198,12 +198,12 @@
private void lookupGroup(PermissionRule rule) throws NoSuchGroupException {
GroupReference ref = rule.getGroup();
if (ref.getUUID() == null) {
- AccountGroup.NameKey name = new AccountGroup.NameKey(ref.getName());
- AccountGroup group = groupCache.get(name);
+ final GroupReference group =
+ GroupBackends.findBestSuggestion(groupBackend, ref.getName());
if (group == null) {
- throw new NoSuchGroupException(name);
+ throw new NoSuchGroupException(ref.getName());
}
- ref.setUUID(group.getGroupUUID());
+ ref.setUUID(group.getUUID());
}
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
index bd6e460..a2b62cc 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
@@ -65,7 +65,8 @@
}
@Override
- public ProjectDetail call() throws NoSuchProjectException, OrmException {
+ public ProjectDetail call() throws NoSuchProjectException, OrmException,
+ IOException {
final Project.NameKey projectName = update.getNameKey();
projectControlFactory.ownerFor(projectName);
@@ -74,6 +75,8 @@
md = metaDataUpdateFactory.create(projectName);
} catch (RepositoryNotFoundException notFound) {
throw new NoSuchProjectException(projectName);
+ } catch (IOException e) {
+ throw new OrmException(e);
}
try {
// TODO We really should take advantage of the Git commit DAG and
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
index 039a301..e0a2f9c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
@@ -28,6 +28,8 @@
import org.eclipse.jgit.lib.Constants;
+import java.util.Collections;
+
public class CreateProjectHandler extends Handler<VoidResult> {
interface Factory {
@@ -74,7 +76,7 @@
}
args.projectDescription = "";
args.submitType = SubmitType.MERGE_IF_NECESSARY;
- args.branch = Constants.MASTER;
+ args.branch = Collections.emptyList();
args.createEmptyCommit = emptyCommit;
args.permissionsOnly = permissionsOnly;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
index 073e2d7..f2b3ca3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
@@ -18,11 +18,13 @@
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -34,6 +36,7 @@
import java.io.IOException;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.Set;
class DeleteBranches extends Handler<Set<Branch.NameKey>> {
@@ -47,9 +50,10 @@
private final ProjectControl.Factory projectControlFactory;
private final GitRepositoryManager repoManager;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final IdentifiedUser identifiedUser;
private final ChangeHooks hooks;
+ private final ReviewDb db;
private final Project.NameKey projectName;
private final Set<Branch.NameKey> toRemove;
@@ -57,9 +61,10 @@
@Inject
DeleteBranches(final ProjectControl.Factory projectControlFactory,
final GitRepositoryManager repoManager,
- final ReplicationQueue replication,
+ final GitReferenceUpdated replication,
final IdentifiedUser identifiedUser,
final ChangeHooks hooks,
+ final ReviewDb db,
@Assisted Project.NameKey name, @Assisted Set<Branch.NameKey> toRemove) {
this.projectControlFactory = projectControlFactory;
@@ -67,6 +72,7 @@
this.replication = replication;
this.identifiedUser = identifiedUser;
this.hooks = hooks;
+ this.db = db;
this.projectName = name;
this.toRemove = toRemove;
@@ -74,17 +80,23 @@
@Override
public Set<Branch.NameKey> call() throws NoSuchProjectException,
- RepositoryNotFoundException {
+ RepositoryNotFoundException, OrmException, IOException {
final ProjectControl projectControl =
projectControlFactory.controlFor(projectName);
- for (Branch.NameKey k : toRemove) {
+ final Iterator<Branch.NameKey> branchIt = toRemove.iterator();
+ while (branchIt.hasNext()) {
+ final Branch.NameKey k = branchIt.next();
if (!projectName.equals(k.getParentKey())) {
throw new IllegalArgumentException("All keys must be from same project");
}
if (!projectControl.controlForRef(k).canDelete()) {
throw new IllegalStateException("Cannot delete " + k.getShortName());
}
+
+ if (db.changes().byBranchOpenAll(k).iterator().hasNext()) {
+ branchIt.remove();
+ }
}
final Set<Branch.NameKey> deleted = new HashSet<Branch.NameKey>();
@@ -109,7 +121,7 @@
case FAST_FORWARD:
case FORCED:
deleted.add(branchKey);
- replication.scheduleUpdate(projectName, refname);
+ replication.fire(projectName, refname);
hooks.doRefUpdatedHook(branchKey, u, identifiedUser.getAccount());
break;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
index 68fde45..2366423 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
@@ -62,7 +62,7 @@
}
@Override
- public ListBranchesResult call() throws NoSuchProjectException {
+ public ListBranchesResult call() throws NoSuchProjectException, IOException {
final ProjectControl pctl = projectControlFactory.validateFor( //
projectName, //
ProjectControl.OWNER | ProjectControl.VISIBLE);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java
new file mode 100644
index 0000000..2757640
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java
@@ -0,0 +1,67 @@
+// 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.rpc.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.project.ListProjects;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URLDecoder;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class ListProjectsServlet extends RestApiServlet {
+ private static final long serialVersionUID = 1L;
+ private final ParameterParser paramParser;
+ private final Provider<ListProjects> factory;
+
+ @Inject
+ ListProjectsServlet(ParameterParser paramParser, Provider<ListProjects> ls) {
+ this.paramParser = paramParser;
+ this.factory = ls;
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse res)
+ throws IOException {
+ ListProjects impl = factory.get();
+ if (!Strings.isNullOrEmpty(req.getPathInfo())) {
+ impl.setMatchPrefix(URLDecoder.decode(req.getPathInfo(), "UTF-8"));
+ }
+ if (acceptsJson(req)) {
+ impl.setFormat(OutputFormat.JSON_COMPACT);
+ }
+ if (paramParser.parse(impl, req, res)) {
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ if (impl.getFormat().isJson()) {
+ res.setContentType(JSON_TYPE);
+ buf.write(JSON_MAGIC);
+ } else {
+ res.setContentType("text/plain");
+ }
+ impl.display(buf);
+ res.setCharacterEncoding("UTF-8");
+ send(req, res, buf.toByteArray());
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index 7ac4ec3..f934d11 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -23,7 +23,7 @@
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -51,7 +51,7 @@
ProjectAccessFactory create(@Assisted Project.NameKey name);
}
- private final GroupCache groupCache;
+ private final GroupBackend groupBackend;
private final ProjectCache projectCache;
private final ProjectControl.Factory projectControlFactory;
private final GroupControl.Factory groupControlFactory;
@@ -62,7 +62,7 @@
private ProjectControl pc;
@Inject
- ProjectAccessFactory(final GroupCache groupCache,
+ ProjectAccessFactory(final GroupBackend groupBackend,
final ProjectCache projectCache,
final ProjectControl.Factory projectControlFactory,
final GroupControl.Factory groupControlFactory,
@@ -70,7 +70,7 @@
final AllProjectsName allProjectsName,
@Assisted final Project.NameKey name) {
- this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
this.projectCache = projectCache;
this.projectControlFactory = projectControlFactory;
this.groupControlFactory = groupControlFactory;
@@ -94,7 +94,7 @@
try {
config = ProjectConfig.read(md);
- if (config.updateGroupNames(groupCache)) {
+ if (config.updateGroupNames(groupBackend)) {
md.setMessage("Update group names\n");
if (config.commit(md)) {
projectCache.evict(config.getProject());
@@ -110,6 +110,7 @@
md.close();
}
+ final RefControl metaConfigControl = pc.controlForRef(GitRepositoryManager.REF_CONFIG);
List<AccessSection> local = new ArrayList<AccessSection>();
Set<String> ownerOf = new HashSet<String>();
Map<AccountGroup.UUID, Boolean> visibleGroups =
@@ -125,7 +126,7 @@
} else if (RefConfigSection.isValid(name)) {
RefControl rc = pc.controlForRef(name);
- if (rc.isOwner()) {
+ if (rc.isOwner() || metaConfigControl.isVisible()) {
local.add(section);
ownerOf.add(name);
@@ -195,8 +196,7 @@
detail.setLocal(local);
detail.setOwnerOf(ownerOf);
- detail.setConfigVisible(pc.isOwner()
- || pc.controlForRef(GitRepositoryManager.REF_CONFIG).isVisible());
+ detail.setConfigVisible(pc.isOwner() || metaConfigControl.isVisible());
return detail;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
index d782da4..2741640 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
@@ -51,7 +51,7 @@
}
@Override
- public ProjectDetail call() throws NoSuchProjectException {
+ public ProjectDetail call() throws NoSuchProjectException, IOException {
final ProjectControl pc =
projectControlFactory.validateFor(projectName, ProjectControl.OWNER
| ProjectControl.VISIBLE);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
index e12cfb1..1c22d83 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
@@ -14,7 +14,6 @@
package com.google.gerrit.httpd.rpc.project;
-
import com.google.gerrit.common.data.ProjectDetail;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.Project;
@@ -22,6 +21,7 @@
import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -50,6 +50,7 @@
try {
result.add(projectDetailFactory.create(projectName).call());
} catch (NoSuchProjectException e) {
+ } catch (IOException e) {
}
}
Collections.sort(result, new Comparator<ProjectDetail>() {
diff --git a/gerrit-launcher/.gitignore b/gerrit-launcher/.gitignore
index 194bedc..980a6b1 100644
--- a/gerrit-launcher/.gitignore
+++ b/gerrit-launcher/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-launcher.iml
\ No newline at end of file
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
index 7d07652..e700351 100644
--- a/gerrit-launcher/pom.xml
+++ b/gerrit-launcher/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-launcher</artifactId>
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..9cca559 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
@@ -31,9 +31,10 @@
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
import java.util.Enumeration;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@@ -196,7 +197,7 @@
throw e;
}
- final ArrayList<URL> jars = new ArrayList<URL>();
+ final SortedMap<String, URL> jars = new TreeMap<String, URL>();
try {
final ZipFile zf = new ZipFile(path);
try {
@@ -208,6 +209,7 @@
}
if (ze.getName().startsWith("WEB-INF/lib/")) {
+ String name = ze.getName().substring("WEB-INF/lib/".length());
final File tmp = createTempFile(safeName(ze), ".jar");
final FileOutputStream out = new FileOutputStream(tmp);
try {
@@ -224,7 +226,7 @@
} finally {
out.close();
}
- jars.add(tmp.toURI().toURL());
+ jars.put(name, tmp.toURI().toURL());
}
}
} finally {
@@ -237,13 +239,38 @@
if (jars.isEmpty()) {
return GerritLauncher.class.getClassLoader();
}
- Collections.sort(jars, new Comparator<URL>() {
- public int compare(URL o1, URL o2) {
- return o1.toString().compareTo(o2.toString());
- }
- });
- return new URLClassLoader(jars.toArray(new URL[jars.size()]));
+ // The extension API needs to be its own ClassLoader, along
+ // with a few of its dependencies. Try to construct this first.
+ List<URL> extapi = new ArrayList<URL>();
+ move(jars, "gerrit-extension-api-", extapi);
+ move(jars, "guice-", extapi);
+ move(jars, "javax.inject-1.jar", extapi);
+ move(jars, "aopalliance-1.0.jar", extapi);
+ move(jars, "guice-servlet-", extapi);
+ move(jars, "servlet-api-", extapi);
+
+ ClassLoader parent = ClassLoader.getSystemClassLoader();
+ if (!extapi.isEmpty()) {
+ parent = new URLClassLoader(
+ extapi.toArray(new URL[extapi.size()]),
+ parent);
+ }
+ return new URLClassLoader(
+ jars.values().toArray(new URL[jars.size()]),
+ parent);
+ }
+
+ private static void move(SortedMap<String, URL> jars,
+ String prefix,
+ List<URL> extapi) {
+ SortedMap<String, URL> matches = jars.tailMap(prefix);
+ if (!matches.isEmpty()) {
+ String first = matches.firstKey();
+ if (first.startsWith(prefix)) {
+ extapi.add(jars.remove(first));
+ }
+ }
}
private static String safeName(final ZipEntry ze) {
@@ -423,36 +450,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-main/.gitignore b/gerrit-main/.gitignore
index 194bedc..c847710 100644
--- a/gerrit-main/.gitignore
+++ b/gerrit-main/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-main.iml
\ No newline at end of file
diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml
index 9d8320c..bb2d763 100644
--- a/gerrit-main/pom.xml
+++ b/gerrit-main/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-main</artifactId>
diff --git a/gerrit-openid/.gitignore b/gerrit-openid/.gitignore
index 194bedc..158faf1 100644
--- a/gerrit-openid/.gitignore
+++ b/gerrit-openid/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-openid.iml
\ No newline at end of file
diff --git a/gerrit-openid/pom.xml b/gerrit-openid/pom.xml
index ed2625e..fa4ab95 100644
--- a/gerrit-openid/pom.xml
+++ b/gerrit-openid/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-openid</artifactId>
diff --git a/gerrit-patch-commonsnet/.gitignore b/gerrit-patch-commonsnet/.gitignore
index 194bedc..121f8e90 100644
--- a/gerrit-patch-commonsnet/.gitignore
+++ b/gerrit-patch-commonsnet/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-patch-commonsnet.iml
\ No newline at end of file
diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml
index 75ee12e..f1a8b3e 100644
--- a/gerrit-patch-commonsnet/pom.xml
+++ b/gerrit-patch-commonsnet/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-patch-commonsnet</artifactId>
diff --git a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
index 7d7bc49..1f08a81 100644
--- a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
+++ b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
@@ -18,7 +18,11 @@
import org.apache.commons.codec.binary.Base64;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.SocketException;
import java.security.InvalidKeyException;
@@ -50,7 +54,22 @@
}
_socket_ = sslFactory(verify).createSocket(_socket_, hostname, port, true);
- _connectAction_();
+
+ // XXX: Can't call _connectAction_() because SMTP server doesn't
+ // give banner information again after STARTTLS, thus SMTP._connectAction_()
+ // will wait on __getReply() forever, see source code of commons-net-2.2.
+ //
+ // The lines below are copied from SocketClient._connectAction_() and
+ // SMTP._connectAction_() in commons-net-2.2.
+ _socket_.setSoTimeout(_timeout_);
+ _input_ = _socket_.getInputStream();
+ _output_ = _socket_.getOutputStream();
+ _reader =
+ new BufferedReader(new InputStreamReader(_input_,
+ UTF_8));
+ _writer =
+ new BufferedWriter(new OutputStreamWriter(_output_,
+ UTF_8));
return true;
}
diff --git a/gerrit-patch-jgit/.gitignore b/gerrit-patch-jgit/.gitignore
index 194bedc..7c4c433 100644
--- a/gerrit-patch-jgit/.gitignore
+++ b/gerrit-patch-jgit/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-patch-jgit.iml
\ No newline at end of file
diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml
index f8190f5..65223fb 100644
--- a/gerrit-patch-jgit/pom.xml
+++ b/gerrit-patch-jgit/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-patch-jgit</artifactId>
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/.gitignore b/gerrit-pgm/.gitignore
index 194bedc..dafe355 100644
--- a/gerrit-pgm/.gitignore
+++ b/gerrit-pgm/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-pgm.iml
\ No newline at end of file
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
index 1463d15..a015219 100644
--- a/gerrit-pgm/pom.xml
+++ b/gerrit-pgm/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-pgm</artifactId>
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..ff5ee17 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
@@ -17,13 +17,14 @@
import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.ehcache.EhcachePoolImpl;
import com.google.gerrit.httpd.CacheBasedWebSession;
import com.google.gerrit.httpd.GitOverHttpModule;
import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider;
+import com.google.gerrit.httpd.RequestContextFilter;
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;
@@ -34,6 +35,7 @@
import com.google.gerrit.pgm.util.RuntimeShutdown;
import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.AuthConfigModule;
import com.google.gerrit.server.config.CanonicalWebUrlModule;
@@ -41,11 +43,12 @@
import com.google.gerrit.server.config.GerritGlobalModule;
import com.google.gerrit.server.config.MasterNodeStartup;
import com.google.gerrit.server.contact.HttpContactStoreConnection;
-import com.google.gerrit.server.git.PushReplication;
import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
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) {
@@ -152,6 +157,7 @@
manager.start();
RuntimeShutdown.add(new Runnable() {
+ @Override
public void run() {
log.info("caught shutdown, cleaning up");
if (runId != null) {
@@ -204,10 +210,10 @@
modules.add(new ChangeHookRunner.Module());
modules.add(new ReceiveCommitsExecutorModule());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
- modules.add(new EhcachePoolImpl.Module());
+ modules.add(new DefaultCacheFactory.Module());
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,13 +237,15 @@
private void initSshd() {
sshInjector = createSshInjector();
+ sysInjector.getInstance(PluginGuiceEnvironment.class)
+ .setSshInjector(sshInjector);
manager.add(sshInjector);
}
private Injector createSshInjector() {
final List<Module> modules = new ArrayList<Module>();
if (sshd) {
- modules.add(new SshModule());
+ modules.add(sysInjector.getInstance(SshModule.class));
if (slave) {
modules.add(new SlaveCommandModule());
} else {
@@ -252,6 +260,9 @@
private void initHttpd() {
webInjector = createWebInjector();
+ sysInjector.getInstance(PluginGuiceEnvironment.class)
+ .setHttpInjector(webInjector);
+
sysInjector.getInstance(HttpCanonicalWebUrlProvider.class)
.setHttpServletRequest(
webInjector.getProvider(HttpServletRequest.class));
@@ -262,10 +273,12 @@
private Injector createWebInjector() {
final List<Module> modules = new ArrayList<Module>();
+ modules.add(RequestContextFilter.module());
modules.add(CacheBasedWebSession.module());
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 5f0bc80..f100372 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
@@ -17,7 +17,6 @@
import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.ehcache.EhcachePoolImpl;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.pgm.util.SiteProgram;
@@ -27,7 +26,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountCacheImpl;
import com.google.gerrit.server.account.GroupCacheImpl;
-import com.google.gerrit.server.cache.CachePool;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.config.ApprovalTypesProvider;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.CanonicalWebUrlProvider;
@@ -100,7 +99,7 @@
install(AccountCacheImpl.module());
install(GroupCacheImpl.module());
- install(new EhcachePoolImpl.Module());
+ install(new DefaultCacheFactory.Module());
install(new FactoryModule() {
@Override
protected void configure() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
index d967969..a5ce908 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
@@ -47,6 +47,7 @@
manager.add(dbInjector);
manager.start();
RuntimeShutdown.add(new Runnable() {
+ @Override
public void run() {
try {
System.in.close();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
index f06946f..2d56453 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
@@ -191,6 +191,11 @@
}
@Override
+ public boolean isBatch() {
+ return ui.isBatch();
+ }
+
+ @Override
public void pruneSchema(StatementExecutor e, List<String> prune) {
for (String p : prune) {
if (!pruneList.contains(p)) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
index 451ed30..cabdc64 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
@@ -43,7 +43,7 @@
@Option(name = "--all", usage = "recompile all rules")
private boolean all;
- @Option(name = "--quiet", usage = "supress some messsages")
+ @Option(name = "--quiet", usage = "suppress some messages")
private boolean quiet;
@Argument(index = 0, multiValued = true, metaVar = "PROJECT", usage = "project to compile rules for")
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
index c09329a..795ba5b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
@@ -33,7 +33,6 @@
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TextProgressMonitor;
@@ -109,7 +108,7 @@
final Repository git;
try {
git = gitManager.openRepository(project);
- } catch (RepositoryNotFoundException e) {
+ } catch (IOException e) {
return;
}
try {
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..d85ff20 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
@@ -17,8 +17,8 @@
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.launcher.GerritLauncher;
-import com.google.gerrit.lifecycle.LifecycleListener;
import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -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..ee3030d 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,12 @@
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);
+ mkdir(site.data_dir);
for (InitStep step : steps) {
step.run();
@@ -78,12 +81,9 @@
savePublic(flags.cfg);
saveSecure(flags.sec);
- if (!site.replication_config.exists()) {
- site.replication_config.createNewFile();
- }
-
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/java/com/google/gerrit/pgm/util/ErrorLogFile.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
index 14c8d9f..68762bb 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
@@ -14,7 +14,7 @@
package com.google.gerrit.pgm.util;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.server.config.SitePaths;
import org.apache.log4j.Appender;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
index 23f36a1..57cc7c4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
@@ -16,7 +16,7 @@
import static java.util.concurrent.TimeUnit.HOURS;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.WorkQueue;
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..e87ebb8
--- /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-plugin-api.iml
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
new file mode 100644
index 0000000..84f6f7b
--- /dev/null
+++ b/gerrit-plugin-api/pom.xml
@@ -0,0 +1,110 @@
+<?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-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>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-ehcache/.gitignore b/gerrit-plugin-archetype/.gitignore
similarity index 99%
rename from gerrit-ehcache/.gitignore
rename to gerrit-plugin-archetype/.gitignore
index 20251d4..80d6257 100644
--- a/gerrit-ehcache/.gitignore
+++ b/gerrit-plugin-archetype/.gitignore
@@ -1,5 +1,5 @@
/target
/.classpath
/.project
-/.settings/org.eclipse.m2e.core.prefs
/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
new file mode 100644
index 0000000..dd1794b
--- /dev/null
+++ b/gerrit-plugin-archetype/pom.xml
@@ -0,0 +1,53 @@
+<?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-archetype</artifactId>
+ <name>Gerrit Code Review - Plugin Archetype</name>
+
+ <properties>
+ <defaultGerritApiVersion>${project.version}</defaultGerritApiVersion>
+ </properties>
+
+ <build>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ <filtering>true</filtering>
+ <includes>
+ <include>META-INF/maven/archetype-metadata.xml</include>
+ </includes>
+ </resource>
+ <resource>
+ <directory>src/main/resources</directory>
+ <filtering>false</filtering>
+ <excludes>
+ <exclude>META-INF/maven/archetype-metadata.xml</exclude>
+ </excludes>
+ </resource>
+ </resources>
+ </build>
+
+</project>
diff --git a/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
new file mode 100644
index 0000000..88328be
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -0,0 +1,65 @@
+<?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.
+-->
+<archetype-descriptor name="com.sap.ldi.qi.itests">
+ <requiredProperties>
+ <requiredProperty key="pluginName"/>
+
+ <requiredProperty key="Gerrit-Module">
+ <defaultValue>Y</defaultValue>
+ </requiredProperty>
+ <requiredProperty key="Gerrit-SshModule">
+ <defaultValue>Y</defaultValue>
+ </requiredProperty>
+ <requiredProperty key="Gerrit-HttpModule">
+ <defaultValue>Y</defaultValue>
+ </requiredProperty>
+
+ <requiredProperty key="Implementation-Vendor"/>
+ <requiredProperty key="Implementation-Url"/>
+
+ <requiredProperty key="gerritApiType">
+ <defaultValue>plugin</defaultValue>
+ </requiredProperty>
+ <requiredProperty key="gerritApiVersion">
+ <defaultValue>${defaultGerritApiVersion}</defaultValue>
+ </requiredProperty>
+ </requiredProperties>
+
+ <fileSets>
+ <fileSet filtered="true" packaged="true">
+ <directory>src/main/java</directory>
+ <includes>
+ <include>**/*.java</include>
+ </includes>
+ </fileSet>
+
+ <fileSet filtered="true">
+ <directory>src/main/resources/Documentation</directory>
+ <includes>
+ <include>**/*.md</include>
+ </includes>
+ </fileSet>
+
+ <fileSet>
+ <directory></directory>
+ <includes>
+ <include>.gitignore</include>
+ <include>LICENSE</include>
+ </includes>
+ </fileSet>
+ </fileSets>
+</archetype-descriptor>
diff --git a/gerrit-ehcache/.gitignore b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore
similarity index 99%
copy from gerrit-ehcache/.gitignore
copy to gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore
index 20251d4..80d6257 100644
--- a/gerrit-ehcache/.gitignore
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore
@@ -1,5 +1,5 @@
/target
/.classpath
/.project
-/.settings/org.eclipse.m2e.core.prefs
/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/LICENSE b/gerrit-plugin-archetype/src/main/resources/archetype-resources/LICENSE
new file mode 100644
index 0000000..11069ed
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+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.
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
new file mode 100644
index 0000000..92099fa
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml
@@ -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.
+-->
+<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>
+
+ <groupId>${groupId}</groupId>
+ <artifactId>${artifactId}</artifactId>
+ <packaging>jar</packaging>
+ <version>${version}</version>
+ <name>${pluginName}</name>
+
+ <properties>
+ <Gerrit-ApiType>${gerritApiType}</Gerrit-ApiType>
+ <Gerrit-ApiVersion>${gerritApiVersion}</Gerrit-ApiVersion>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>2.4</version>
+ <configuration>
+ <archive>
+ <manifestEntries>
+#if ($Gerrit-Module.equalsIgnoreCase("Y"))
+ <Gerrit-Module>${package}.Module</Gerrit-Module>
+#end
+#if ($Gerrit-SshModule.equalsIgnoreCase("Y"))
+ <Gerrit-SshModule>${package}.SshModule</Gerrit-SshModule>
+#end
+#if ($Gerrit-HttpModule.equalsIgnoreCase("Y"))
+ <Gerrit-HttpModule>${package}.HttpModule</Gerrit-HttpModule>
+#end
+
+ <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor>
+ <Implementation-URL>${Implementation-Url}</Implementation-URL>
+
+ <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title>
+ <Implementation-Version>${project.version}</Implementation-Version>
+
+ <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType>
+ <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>2.3.2</version>
+ <configuration>
+ <source>1.6</source>
+ <target>1.6</target>
+ <encoding>UTF-8</encoding>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.google.gerrit</groupId>
+ <artifactId>gerrit-${Gerrit-ApiType}-api</artifactId>
+ <version>${Gerrit-ApiVersion}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.8.1</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <repositories>
+ <repository>
+ <id>gerrit-api-repository</id>
+#if ($gerritApiVersion.endsWith("SNAPSHOT"))
+ <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url>
+#else
+ <url>https://gerrit-api.commondatastorage.googleapis.com/release/</url>
+#end
+ </repository>
+ </repositories>
+</project>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
similarity index 70%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
copy to gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
index 3370b08..2840112 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/HttpModule.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 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.
@@ -12,8 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package ${package};
-public interface CachePool {
- public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+import com.google.inject.servlet.ServletModule;
+
+class HttpModule extends ServletModule {
+ @Override
+ protected void configureServlets() {
+ // TODO
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
similarity index 71%
rename from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
rename to gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
index 3370b08..0d28349 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/Module.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 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.
@@ -12,8 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package ${package};
-public interface CachePool {
- public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+import com.google.inject.AbstractModule;
+
+class Module extends AbstractModule {
+ @Override
+ protected void configure() {
+ // TODO
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java
similarity index 66%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
copy to gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java
index 3370b08..aa15ca5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/java/SshModule.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 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.
@@ -12,8 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package ${package};
-public interface CachePool {
- public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+import com.google.gerrit.sshd.PluginCommandModule;
+
+class SshModule extends PluginCommandModule {
+ @Override
+ protected void configureCommands() {
+ // command("my-command").to(MyCommand.class);
+ }
}
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/cmd-start.md b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/cmd-start.md
new file mode 100644
index 0000000..beecb90
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/cmd-start.md
@@ -0,0 +1 @@
+TODO: command documentation
diff --git a/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/config.md b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..bde3084
--- /dev/null
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/src/main/resources/Documentation/config.md
@@ -0,0 +1 @@
+TODO: config documentation
diff --git a/gerrit-prettify/.gitignore b/gerrit-prettify/.gitignore
index 194bedc..8cf95ef 100644
--- a/gerrit-prettify/.gitignore
+++ b/gerrit-prettify/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-prettify.iml
\ No newline at end of file
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
index f5bd3d6..9354274 100644
--- a/gerrit-prettify/pom.xml
+++ b/gerrit-prettify/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-prettify</artifactId>
diff --git a/gerrit-reviewdb/.gitignore b/gerrit-reviewdb/.gitignore
index 194bedc..812ddd0 100644
--- a/gerrit-reviewdb/.gitignore
+++ b/gerrit-reviewdb/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-reviewdb.iml
\ No newline at end of file
diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml
index 24d6a1b..f9fb49e 100644
--- a/gerrit-reviewdb/pom.xml
+++ b/gerrit-reviewdb/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-reviewdb</artifactId>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AbstractAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AbstractAgreement.java
deleted file mode 100644
index 0ed7410..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AbstractAgreement.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (C) 2009 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.reviewdb.client;
-
-import java.sql.Timestamp;
-
-/** Base for {@link AccountAgreement} or {@link AccountGroupAgreement}. */
-public interface AbstractAgreement {
- public static enum Status {
- NEW('n'),
-
- VERIFIED('V'),
-
- REJECTED('R');
-
- private final char code;
-
- private Status(final char c) {
- code = c;
- }
-
- public char getCode() {
- return code;
- }
-
- public static Status forCode(final char c) {
- for (final Status s : Status.values()) {
- if (s.code == c) {
- return s;
- }
- }
- return null;
- }
- }
-
- public ContributorAgreement.Id getAgreementId();
-
- public Timestamp getAcceptedOn();
-
- public Status getStatus();
-
- public Timestamp getReviewedOn();
-
- public Account.Id getReviewedBy();
-
- public String getReviewComments();
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountAgreement.java
deleted file mode 100644
index baa9b5c..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountAgreement.java
+++ /dev/null
@@ -1,122 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-
-import java.sql.Timestamp;
-
-/** Electronic acceptance of a {@link ContributorAgreement} by {@link Account} */
-public final class AccountAgreement implements AbstractAgreement {
- public static class Key extends CompoundKey<Account.Id> {
- private static final long serialVersionUID = 1L;
-
- @Column(id = 1)
- protected Account.Id accountId;
-
- @Column(id = 2)
- protected ContributorAgreement.Id claId;
-
- protected Key() {
- accountId = new Account.Id();
- claId = new ContributorAgreement.Id();
- }
-
- public Key(final Account.Id account, final ContributorAgreement.Id cla) {
- this.accountId = account;
- this.claId = cla;
- }
-
- @Override
- public Account.Id getParentKey() {
- return accountId;
- }
-
- public ContributorAgreement.Id getContributorAgreementId() {
- return claId;
- }
-
- @Override
- public com.google.gwtorm.client.Key<?>[] members() {
- return new com.google.gwtorm.client.Key<?>[] {claId};
- }
- }
-
- @Column(id = 1, name = Column.NONE)
- protected Key key;
-
- @Column(id = 2)
- protected Timestamp acceptedOn;
-
- @Column(id = 3)
- protected char status;
-
- @Column(id = 4, notNull = false)
- protected Account.Id reviewedBy;
-
- @Column(id = 5, notNull = false)
- protected Timestamp reviewedOn;
-
- @Column(id = 6, notNull = false, length = Integer.MAX_VALUE)
- protected String reviewComments;
-
- protected AccountAgreement() {
- }
-
- public AccountAgreement(final AccountAgreement.Key k) {
- key = k;
- acceptedOn = new Timestamp(System.currentTimeMillis());
- status = Status.NEW.getCode();
- }
-
- public AccountAgreement.Key getKey() {
- return key;
- }
-
- public ContributorAgreement.Id getAgreementId() {
- return key.claId;
- }
-
- public Timestamp getAcceptedOn() {
- return acceptedOn;
- }
-
- public Status getStatus() {
- return Status.forCode(status);
- }
-
- public Timestamp getReviewedOn() {
- return reviewedOn;
- }
-
- public Account.Id getReviewedBy() {
- return reviewedBy;
- }
-
- public String getReviewComments() {
- return reviewComments;
- }
-
- public void setReviewComments(final String s) {
- reviewComments = s;
- }
-
- public void review(final Status newStatus, final Account.Id by) {
- status = newStatus.getCode();
- reviewedBy = by;
- reviewedOn = new Timestamp(System.currentTimeMillis());
- }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
index b9ff4fb..185255c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
@@ -118,10 +118,10 @@
* (show latest patch set on top).
*/
@Column(id = 10)
- protected boolean displayPatchSetsInReverseOrder;
+ protected boolean reversePatchSetOrder;
@Column(id = 11)
- protected boolean displayPersonNameInReviewCategory;
+ protected boolean showUsernameInReviewCategory;
public AccountGeneralPreferences() {
}
@@ -188,20 +188,20 @@
copySelfOnEmail = includeSelfOnEmail;
}
- public boolean isDisplayPatchSetsInReverseOrder() {
- return displayPatchSetsInReverseOrder;
+ public boolean isReversePatchSetOrder() {
+ return reversePatchSetOrder;
}
- public void setDisplayPatchSetsInReverseOrder(final boolean displayPatchSetsInReverseOrder) {
- this.displayPatchSetsInReverseOrder = displayPatchSetsInReverseOrder;
+ public void setReversePatchSetOrder(final boolean reversePatchSetOrder) {
+ this.reversePatchSetOrder = reversePatchSetOrder;
}
- public boolean isDisplayPersonNameInReviewCategory() {
- return displayPersonNameInReviewCategory;
+ public boolean isShowUsernameInReviewCategory() {
+ return showUsernameInReviewCategory;
}
- public void setDisplayPersonNameInReviewCategory(final boolean displayPersonNameInReviewCategory) {
- this.displayPersonNameInReviewCategory = displayPersonNameInReviewCategory;
+ public void setShowUsernameInReviewCategory(final boolean showUsernameInReviewCategory) {
+ this.showUsernameInReviewCategory = showUsernameInReviewCategory;
}
public DateFormat getDateFormat() {
@@ -231,8 +231,8 @@
showSiteHeader = true;
useFlashClipboard = true;
copySelfOnEmail = false;
- displayPatchSetsInReverseOrder = false;
- displayPersonNameInReviewCategory = false;
+ reversePatchSetOrder = false;
+ showUsernameInReviewCategory = false;
downloadUrl = null;
downloadCommand = null;
dateFormat = null;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
index 8e2541a..9f67fe8 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
@@ -51,7 +51,7 @@
StringKey<com.google.gwtorm.client.Key<?>> {
private static final long serialVersionUID = 1L;
- @Column(id = 1, length = 40)
+ @Column(id = 1)
protected String uuid;
protected UUID() {
@@ -79,32 +79,6 @@
}
}
- /** Distinguished name, within organization directory server. */
- public static class ExternalNameKey extends
- StringKey<com.google.gwtorm.client.Key<?>> {
- private static final long serialVersionUID = 1L;
-
- @Column(id = 1)
- protected String name;
-
- protected ExternalNameKey() {
- }
-
- public ExternalNameKey(final String n) {
- name = n;
- }
-
- @Override
- public String get() {
- return name;
- }
-
- @Override
- protected void set(String newValue) {
- name = newValue;
- }
- }
-
/** Synthetic key to link to within the database */
public static class Id extends IntKey<com.google.gwtorm.client.Key<?>> {
private static final long serialVersionUID = 1L;
@@ -157,20 +131,7 @@
* who is a member of the owner group. These groups are not treated special
* in the code.
*/
- INTERNAL,
-
- /**
- * Group defined by external LDAP database.
- * <p>
- * A group whose membership is determined by the LDAP directory that we
- * connect to for user and group information. In UI contexts the membership
- * of the group is not displayed, as it may be exceedingly large, or might
- * contain users who have never logged into this server before (and thus
- * have no matching account record). Adding or removing users from an LDAP
- * group requires making edits through the LDAP directory, and cannot be
- * done through our UI.
- */
- LDAP;
+ INTERNAL;
}
/** Common UUID assigned to the "Project Owners" placeholder group. */
@@ -193,14 +154,6 @@
@Column(id = 2)
protected Id groupId;
- /**
- * Identity of the group whose members can manage this group.
- * <p>
- * This can be a self-reference to indicate the group's members manage itself.
- */
- @Column(id = 3)
- protected Id ownerGroupId;
-
/** A textual description of the group's purpose. */
@Column(id = 4, length = Integer.MAX_VALUE, notNull = false)
protected String description;
@@ -209,10 +162,6 @@
@Column(id = 5, length = 8)
protected String groupType;
- /** Distinguished name in the directory server. */
- @Column(id = 6, notNull = false)
- protected ExternalNameKey externalName;
-
@Column(id = 7)
protected boolean visibleToAll;
@@ -220,6 +169,14 @@
@Column(id = 9)
protected UUID groupUUID;
+ /**
+ * Identity of the group whose members can manage this group.
+ * <p>
+ * This can be a self-reference to indicate the group's members manage itself.
+ */
+ @Column(id = 10)
+ protected UUID ownerGroupUUID;
+
protected AccountGroup() {
}
@@ -227,9 +184,9 @@
final AccountGroup.Id newId, final AccountGroup.UUID uuid) {
name = newName;
groupId = newId;
- ownerGroupId = groupId;
visibleToAll = false;
groupUUID = uuid;
+ ownerGroupUUID = groupUUID;
setType(Type.INTERNAL);
}
@@ -257,12 +214,12 @@
description = d;
}
- public AccountGroup.Id getOwnerGroupId() {
- return ownerGroupId;
+ public AccountGroup.UUID getOwnerGroupUUID() {
+ return ownerGroupUUID;
}
- public void setOwnerGroupId(final AccountGroup.Id id) {
- ownerGroupId = id;
+ public void setOwnerGroupUUID(final AccountGroup.UUID uuid) {
+ ownerGroupUUID = uuid;
}
public Type getType() {
@@ -273,14 +230,6 @@
groupType = t.name();
}
- public ExternalNameKey getExternalNameKey() {
- return externalName;
- }
-
- public void setExternalNameKey(final ExternalNameKey k) {
- externalName = k;
- }
-
public void setVisibleToAll(final boolean visibleToAll) {
this.visibleToAll = visibleToAll;
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupAgreement.java
deleted file mode 100644
index c712b3d..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupAgreement.java
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright (C) 2009 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.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-
-import java.sql.Timestamp;
-
-/**
- * Acceptance of a {@link ContributorAgreement} by an {@link AccountGroup}.
- */
-public final class AccountGroupAgreement implements AbstractAgreement {
- public static class Key extends CompoundKey<AccountGroup.Id> {
- private static final long serialVersionUID = 1L;
-
- @Column(id = 1)
- protected AccountGroup.Id groupId;
-
- @Column(id = 2)
- protected ContributorAgreement.Id claId;
-
- protected Key() {
- groupId = new AccountGroup.Id();
- claId = new ContributorAgreement.Id();
- }
-
- public Key(final AccountGroup.Id group, final ContributorAgreement.Id cla) {
- this.groupId = group;
- this.claId = cla;
- }
-
- @Override
- public AccountGroup.Id getParentKey() {
- return groupId;
- }
-
- public ContributorAgreement.Id getContributorAgreementId() {
- return claId;
- }
-
- @Override
- public com.google.gwtorm.client.Key<?>[] members() {
- return new com.google.gwtorm.client.Key<?>[] {claId};
- }
- }
-
- @Column(id = 1, name = Column.NONE)
- protected Key key;
-
- @Column(id = 2)
- protected Timestamp acceptedOn;
-
- @Column(id = 3)
- protected char status;
-
- @Column(id = 4, notNull = false)
- protected Account.Id reviewedBy;
-
- @Column(id = 5, notNull = false)
- protected Timestamp reviewedOn;
-
- @Column(id = 6, notNull = false, length = Integer.MAX_VALUE)
- protected String reviewComments;
-
- protected AccountGroupAgreement() {
- }
-
- public AccountGroupAgreement(final AccountGroupAgreement.Key k) {
- key = k;
- acceptedOn = new Timestamp(System.currentTimeMillis());
- status = Status.NEW.getCode();
- }
-
- public AccountGroupAgreement.Key getKey() {
- return key;
- }
-
- public ContributorAgreement.Id getAgreementId() {
- return key.claId;
- }
-
- public Timestamp getAcceptedOn() {
- return acceptedOn;
- }
-
- public Status getStatus() {
- return Status.forCode(status);
- }
-
- public Timestamp getReviewedOn() {
- return reviewedOn;
- }
-
- public Account.Id getReviewedBy() {
- return reviewedBy;
- }
-
- public String getReviewComments() {
- return reviewComments;
- }
-
- public void setReviewComments(final String s) {
- reviewComments = s;
- }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
index 8ba8912..523134b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
@@ -80,9 +80,14 @@
public AccountGroupMemberAudit(final AccountGroupMember m,
final Account.Id adder) {
+ this(m, adder, now());
+ }
+
+ public AccountGroupMemberAudit(final AccountGroupMember m,
+ final Account.Id adder, Timestamp addedOn) {
final Account.Id who = m.getAccountId();
final AccountGroup.Id group = m.getAccountGroupId();
- key = new AccountGroupMemberAudit.Key(who, group, now());
+ key = new AccountGroupMemberAudit.Key(who, group, addedOn);
addedBy = adder;
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
index 93e6fb3..e592101 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
@@ -22,7 +22,7 @@
public final class AccountProjectWatch {
public enum NotifyType {
- NEW_CHANGES, ALL_COMMENTS, SUBMITTED_CHANGES
+ NEW_CHANGES, ALL_COMMENTS, SUBMITTED_CHANGES, ALL
}
public static final String FILTER_ALL = "*";
@@ -159,6 +159,12 @@
case SUBMITTED_CHANGES:
notifySubmittedChanges = v;
break;
+
+ case ALL:
+ notifyNewChanges = v;
+ notifyAllComments = v;
+ notifySubmittedChanges = v;
+ break;
}
}
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContributorAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContributorAgreement.java
deleted file mode 100644
index 12e7459..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContributorAgreement.java
+++ /dev/null
@@ -1,140 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.IntKey;
-
-/**
- * An agreement {@link Account} must acknowledge to contribute changes.
- *
- * @see AccountAgreement
- */
-public final class ContributorAgreement {
- public static class Id extends IntKey<com.google.gwtorm.client.Key<?>> {
- private static final long serialVersionUID = 1L;
-
- @Column(id = 1, name = "cla_id")
- protected int id;
-
- protected Id() {
- }
-
- public Id(final int id) {
- this.id = id;
- }
-
- @Override
- public int get() {
- return id;
- }
-
- @Override
- protected void set(int newValue) {
- id = newValue;
- }
- }
-
- @Column(id = 1)
- protected Id id;
-
- /** Is this an active agreement contributors can use. */
- @Column(id = 2)
- protected boolean active;
-
- /** Does this agreement require the {@link Account} to have contact details? */
- @Column(id = 3)
- protected boolean requireContactInformation;
-
- /** Does this agreement automatically verify new accounts? */
- @Column(id = 4)
- protected boolean autoVerify;
-
- /** A short name for the agreement. */
- @Column(id = 5, length = 40)
- protected String shortName;
-
- /** A short one-line description text to appear next to the name. */
- @Column(id = 6, notNull = false)
- protected String shortDescription;
-
- /** Web address of the agreement documentation. */
- @Column(id = 7)
- protected String agreementUrl;
-
- protected ContributorAgreement() {
- }
-
- /**
- * Create a new agreement.
- *
- * @param newId unique id, see
- * {@link com.google.gerrit.reviewdb.server.ReviewDb#nextAccountId()}.
- * @param name a short title/name for the agreement.
- */
- public ContributorAgreement(final ContributorAgreement.Id newId,
- final String name) {
- id = newId;
- shortName = name;
- }
-
- public ContributorAgreement.Id getId() {
- return id;
- }
-
- public boolean isActive() {
- return active;
- }
-
- public void setActive(final boolean a) {
- active = a;
- }
-
- public boolean isAutoVerify() {
- return autoVerify;
- }
-
- public void setAutoVerify(final boolean g) {
- autoVerify = g;
- }
-
- public boolean isRequireContactInformation() {
- return requireContactInformation;
- }
-
- public void setRequireContactInformation(final boolean r) {
- requireContactInformation = r;
- }
-
- public String getShortName() {
- return shortName;
- }
-
- public String getShortDescription() {
- return shortDescription;
- }
-
- public void setShortDescription(final String d) {
- shortDescription = d;
- }
-
- public String getAgreementUrl() {
- return agreementUrl;
- }
-
- public void setAgreementUrl(final String h) {
- agreementUrl = h;
- }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
index 2b23b7a..29a426b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
@@ -15,7 +15,6 @@
package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.Id;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAgreementAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAgreementAccess.java
deleted file mode 100644
index 86ea372..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAgreementAccess.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
-import com.google.gerrit.reviewdb.client.Account.Id;
-import com.google.gerrit.reviewdb.client.AccountAgreement.Key;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountAgreementAccess extends
- Access<AccountAgreement, AccountAgreement.Key> {
- @PrimaryKey("key")
- AccountAgreement get(AccountAgreement.Key key) throws OrmException;
-
- @Query("WHERE key.accountId = ? ORDER BY acceptedOn")
- ResultSet<AccountAgreement> byAccount(Account.Id id) throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
index 8574758..fffcf9e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
@@ -16,7 +16,6 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.Account.Id;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
index b263552..bf6c0ef 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
@@ -16,8 +16,6 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.Account.Id;
-import com.google.gerrit.reviewdb.client.AccountExternalId.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
index 9e88244..1de80f3 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
@@ -29,10 +29,6 @@
@Query("WHERE groupUUID = ?")
ResultSet<AccountGroup> byUUID(AccountGroup.UUID uuid) throws OrmException;
- @Query("WHERE externalName = ?")
- ResultSet<AccountGroup> byExternalName(AccountGroup.ExternalNameKey name)
- throws OrmException;
-
@Query
ResultSet<AccountGroup> all() throws OrmException;
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAgreementAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAgreementAccess.java
deleted file mode 100644
index ecab70d..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAgreementAccess.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2009 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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement.Key;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountGroupAgreementAccess extends
- Access<AccountGroupAgreement, AccountGroupAgreement.Key> {
- @PrimaryKey("key")
- AccountGroupAgreement get(AccountGroupAgreement.Key key) throws OrmException;
-
- @Query("WHERE key.groupId = ? ORDER BY acceptedOn")
- ResultSet<AccountGroupAgreement> byGroup(AccountGroup.Id id)
- throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
index c8c7b40..3ee4ba0 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
@@ -16,8 +16,6 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupInclude;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
index 3cf24f3..b3f4f88 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
@@ -16,8 +16,6 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
index 53ecaae..e070d69 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
@@ -17,8 +17,6 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupMember.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
index f26996d..48c4e2d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
@@ -17,8 +17,6 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
index d0bc419..30e685c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
@@ -16,7 +16,6 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gerrit.reviewdb.client.AccountGroup.NameKey;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java
index ef8133b..80b2dc4 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java
@@ -17,8 +17,6 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountPatchReview;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Account.Id;
-import com.google.gerrit.reviewdb.client.AccountPatchReview.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
index 046d5a5..c073468 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
@@ -17,9 +17,6 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Account.Id;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch.Key;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
index d756a44..b31b5b6 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
@@ -16,7 +16,6 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.client.Account.Id;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java
index 31e6c62..db9886e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java
@@ -15,7 +15,6 @@
package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
-import com.google.gerrit.reviewdb.client.ApprovalCategory.Id;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java
index acdda5e8..0bc9981 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java
@@ -16,7 +16,6 @@
import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.client.ApprovalCategory.Id;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
index 4660291..66df78a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
@@ -18,9 +18,6 @@
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.client.Change.Id;
-import com.google.gerrit.reviewdb.client.Change.Key;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
index 6db2675..0126a31 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
@@ -17,8 +17,6 @@
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.client.Change.Id;
-import com.google.gerrit.reviewdb.client.ChangeMessage.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ContributorAgreementAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ContributorAgreementAccess.java
deleted file mode 100644
index 3c7f47a..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ContributorAgreementAccess.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement.Id;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-/** Access interface for {@link ContributorAgreement}. */
-public interface ContributorAgreementAccess extends
- Access<ContributorAgreement, ContributorAgreement.Id> {
- @PrimaryKey("id")
- ContributorAgreement get(ContributorAgreement.Id key) throws OrmException;
-
- @Query("WHERE active = true ORDER BY shortName")
- ResultSet<ContributorAgreement> active() throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
index 3be8563..a5842de 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
@@ -18,8 +18,6 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
index c04fa06..7e0b90c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
@@ -17,7 +17,6 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.client.Change.Id;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
index e163bc4..f2b1cb7 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
@@ -17,7 +17,6 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetAncestor;
import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor.Id;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
index b30c5bfa..dae8e6d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
@@ -18,8 +18,6 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.PatchSetApproval.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 616a656..149626b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -49,9 +49,6 @@
@Relation(id = 4)
ApprovalCategoryValueAccess approvalCategoryValues();
- @Relation(id = 5)
- ContributorAgreementAccess contributorAgreements();
-
@Relation(id = 6)
AccountAccess accounts();
@@ -61,9 +58,6 @@
@Relation(id = 8)
AccountSshKeyAccess accountSshKeys();
- @Relation(id = 9)
- AccountAgreementAccess accountAgreements();
-
@Relation(id = 10)
AccountGroupAccess accountGroups();
@@ -82,9 +76,6 @@
@Relation(id = 15)
AccountGroupIncludeAuditAccess accountGroupIncludesAudit();
- @Relation(id = 16)
- AccountGroupAgreementAccess accountGroupAgreements();
-
@Relation(id = 17)
AccountDiffPreferenceAccess accountDiffPreferences();
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
index 07f255c..470f8c6 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
@@ -15,7 +15,6 @@
package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.client.CurrentSchemaVersion.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
index 88e703a..4010dae 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
@@ -17,8 +17,6 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.StarredChange;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.StarredChange.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
index e1aa907..0090df8 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
@@ -16,8 +16,6 @@
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
-import com.google.gerrit.reviewdb.client.Branch.NameKey;
-import com.google.gerrit.reviewdb.client.SubmoduleSubscription.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
index 3bb07cb..4b2ed74 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
@@ -15,7 +15,6 @@
package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.SystemConfig;
-import com.google.gerrit.reviewdb.client.SystemConfig.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
index 51babb3..92b9b10 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
@@ -16,8 +16,6 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.TrackingId;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.TrackingId.Key;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index 5d0e15c..1244695 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -16,11 +16,6 @@
-- *********************************************************************
--- AccountAgreementAccess
--- @PrimaryKey covers: byAccount
-
-
--- *********************************************************************
-- AccountExternalIdAccess
-- covers: byAccount
CREATE INDEX account_external_ids_byAccount
@@ -32,12 +27,6 @@
-- *********************************************************************
--- AccountGroupAccess
-CREATE INDEX account_groups_ownedByGroup
-ON account_groups (owner_group_id);
-
-
--- *********************************************************************
-- AccountGroupMemberAccess
-- @PrimaryKey covers: byAccount
CREATE INDEX account_group_members_byGroup
@@ -135,13 +124,6 @@
-- *********************************************************************
--- ContributorAgreementAccess
--- covers: active
-CREATE INDEX contributor_agreements_active
-ON contributor_agreements (active, short_name);
-
-
--- *********************************************************************
-- PatchLineCommentAccess
-- @PrimaryKey covers: published, draft
CREATE INDEX patch_comment_drafts
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index 97ad126..26c6d19 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -88,11 +88,6 @@
-- *********************************************************************
--- AccountAgreementAccess
--- @PrimaryKey covers: byAccount
-
-
--- *********************************************************************
-- AccountExternalIdAccess
-- covers: byAccount
CREATE INDEX account_external_ids_byAccount
@@ -104,12 +99,6 @@
-- *********************************************************************
--- AccountGroupAccess
-CREATE INDEX account_groups_ownedByGroup
-ON account_groups (owner_group_id);
-
-
--- *********************************************************************
-- AccountGroupMemberAccess
-- @PrimaryKey covers: byAccount
CREATE INDEX account_group_members_byGroup
@@ -216,13 +205,6 @@
-- *********************************************************************
--- ContributorAgreementAccess
--- covers: active
-CREATE INDEX contributor_agreements_active
-ON contributor_agreements (active, short_name);
-
-
--- *********************************************************************
-- PatchLineCommentAccess
-- @PrimaryKey covers: published, draft
CREATE INDEX patch_comment_drafts
diff --git a/gerrit-server/.gitignore b/gerrit-server/.gitignore
index 194bedc..9324efe 100644
--- a/gerrit-server/.gitignore
+++ b/gerrit-server/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-server.iml
\ No newline at end of file
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index 58e43cf..af18173 100644
--- a/gerrit-server/pom.xml
+++ b/gerrit-server/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-server</artifactId>
@@ -110,6 +110,11 @@
</dependency>
<dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+
+ <dependency>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-antlr</artifactId>
<version>${project.version}</version>
@@ -123,6 +128,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 +175,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 31837b0..832bd23 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
@@ -16,12 +16,12 @@
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -50,7 +50,6 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RefUpdate;
@@ -133,6 +132,8 @@
private final EventFactory eventFactory;
+ private final SitePaths sitePaths;
+
/**
* Create a new ChangeHookRunner.
*
@@ -149,7 +150,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 +158,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()));
@@ -192,16 +194,6 @@
}
/**
- * Get the Repository for the given change, or null on error.
- *
- * @param change Change to get repo for,
- * @return Repository or null.
- */
- private Repository openRepository(final Change change) {
- return openRepository(change.getProject());
- }
-
- /**
* Get the Repository for the given project name, or null on error.
*
* @param name Project to get repo for,
@@ -210,7 +202,7 @@
private Repository openRepository(final Project.NameKey name) {
try {
return repoManager.openRepository(name);
- } catch (RepositoryNotFoundException err) {
+ } catch (IOException err) {
log.warn("Cannot open repository " + name.get(), err);
return null;
}
@@ -370,7 +362,7 @@
final List<String> args = new ArrayList<String>();
addArg(args, "--submitter", getDisplayName(account));
addArg(args, "--user-id", account.getId().toString());
- addArg(args, "--cla-id", cla.getId().toString());
+ addArg(args, "--cla-name", cla.getName());
runHook(claSignedHook, args);
}
@@ -491,10 +483,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/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
index c424a26..dc258ca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -14,12 +14,12 @@
package com.google.gerrit.common;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index 5544216..496a273 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -14,11 +14,11 @@
package com.google.gerrit.common;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Branch.NameKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java
index b720a25..1a3ad9b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java
@@ -14,75 +14,90 @@
package com.google.gerrit.lifecycle;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.inject.Binding;
import com.google.inject.Injector;
+import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
+import com.google.inject.util.Providers;
import org.slf4j.LoggerFactory;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
import java.util.List;
/** Tracks and executes registered {@link LifecycleListener}s. */
public class LifecycleManager {
- private final LinkedHashMap<LifecycleListener, Boolean> listeners =
- new LinkedHashMap<LifecycleListener, Boolean>();
+ private final List<Provider<LifecycleListener>> listeners = newList();
+ private final List<RegistrationHandle> handles = newList();
- private boolean started;
+ /** Index of the last listener to start successfully; -1 when not started. */
+ private int startedIndex = -1;
+
+ /** Add a handle that must be cleared during stop. */
+ public void add(RegistrationHandle handle) {
+ handles.add(handle);
+ }
/** Add a single listener. */
- public void add(final LifecycleListener listener) {
- listeners.put(listener, true);
+ public void add(LifecycleListener listener) {
+ listeners.add(Providers.of(listener));
+ }
+
+ /** Add a single listener. */
+ public void add(Provider<LifecycleListener> listener) {
+ listeners.add(listener);
}
/** Add all {@link LifecycleListener}s registered in the Injector. */
- public void add(final Injector injector) {
- if (started) {
- throw new IllegalStateException("Already started");
- }
- for (final Binding<LifecycleListener> binding : get(injector)) {
- add(binding.getProvider().get());
+ public void add(Injector injector) {
+ Preconditions.checkState(startedIndex < 0, "Already started");
+ for (Binding<LifecycleListener> binding : get(injector)) {
+ add(binding.getProvider());
}
}
/** Add all {@link LifecycleListener}s registered in the Injectors. */
- public void add(final Injector... injectors) {
- for (final Injector i : injectors) {
+ public void add(Injector... injectors) {
+ for (Injector i : injectors) {
add(i);
}
}
/** Start all listeners, in the order they were registered. */
public void start() {
- if (!started) {
- started = true;
- for (LifecycleListener obj : listeners.keySet()) {
- obj.start();
- }
+ for (int i = startedIndex + 1; i < listeners.size(); i++) {
+ LifecycleListener listener = listeners.get(i).get();
+ startedIndex = i;
+ listener.start();
}
}
/** Stop all listeners, in the reverse order they were registered. */
public void stop() {
- if (started) {
- final List<LifecycleListener> t =
- new ArrayList<LifecycleListener>(listeners.keySet());
+ for (int i = handles.size() - 1; 0 <= i; i--) {
+ handles.get(i).remove();
+ }
+ handles.clear();
- for (int i = t.size() - 1; 0 <= i; i--) {
- final LifecycleListener obj = t.get(i);
- try {
- obj.stop();
- } catch (Throwable err) {
- LoggerFactory.getLogger(obj.getClass()).warn("Failed to stop", err);
- }
+ for (int i = startedIndex; 0 <= i; i--) {
+ LifecycleListener obj = listeners.get(i).get();
+ try {
+ obj.stop();
+ } catch (Throwable err) {
+ LoggerFactory.getLogger(obj.getClass()).warn("Failed to stop", err);
}
-
- started = false;
+ startedIndex = i - 1;
}
}
private static List<Binding<LifecycleListener>> get(Injector i) {
return i.findBindingsByType(new TypeLiteral<LifecycleListener>() {});
}
+
+ private static <T> List<T> newList() {
+ return Lists.newArrayListWithCapacity(4);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
index dcc8a35..04682f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
@@ -1,5 +1,6 @@
package com.google.gerrit.lifecycle;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.inject.AbstractModule;
import com.google.inject.Singleton;
import com.google.inject.binder.LinkedBindingBuilder;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
index fd72e0c..fbee145 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
@@ -189,6 +189,8 @@
git = gitMgr.openRepository(project);
} catch (RepositoryNotFoundException e) {
throw new CompileException("Cannot open repository " + project, e);
+ } catch (IOException e) {
+ throw new CompileException("Cannot open repository " + project, e);
}
try {
ObjectLoader ldr = git.open(rulesId, Constants.OBJ_BLOB);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 8ab9471..1185fd3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -16,19 +16,25 @@
import static com.google.gerrit.rules.StoredValue.create;
+import com.google.common.collect.Maps;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.query.change.ChangeData;
import com.googlecode.prolog_cafe.lang.Prolog;
import com.googlecode.prolog_cafe.lang.SystemException;
@@ -37,21 +43,26 @@
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
+import java.io.IOException;
+import java.util.Map;
+
public final class StoredValues {
public static final StoredValue<ReviewDb> REVIEW_DB = create(ReviewDb.class);
public static final StoredValue<Change> CHANGE = create(Change.class);
- public static final StoredValue<PatchSet.Id> PATCH_SET_ID = create(PatchSet.Id.class);
+ public static final StoredValue<ChangeData> CHANGE_DATA = create(ChangeData.class);
+ public static final StoredValue<PatchSet> PATCH_SET = create(PatchSet.class);
public static final StoredValue<ChangeControl> CHANGE_CONTROL = create(ChangeControl.class);
public static final StoredValue<PatchSetInfo> PATCH_SET_INFO = new StoredValue<PatchSetInfo>() {
@Override
public PatchSetInfo createValue(Prolog engine) {
- PatchSet.Id patchSetId = StoredValues.PATCH_SET_ID.get(engine);
+ Change change = StoredValues.CHANGE.get(engine);
+ PatchSet ps = StoredValues.PATCH_SET.get(engine);
PrologEnvironment env = (PrologEnvironment) engine.control;
PatchSetInfoFactory patchInfoFactory =
env.getInjector().getInstance(PatchSetInfoFactory.class);
try {
- return patchInfoFactory.get(REVIEW_DB.get(engine), patchSetId);
+ return patchInfoFactory.get(change, ps);
} catch (PatchSetInfoNotAvailableException e) {
throw new SystemException(e.getMessage());
}
@@ -70,8 +81,10 @@
ObjectId b = ObjectId.fromString(psInfo.getRevId());
Whitespace ws = Whitespace.IGNORE_NONE;
PatchListKey plKey = new PatchListKey(projectKey, a, b, ws);
- PatchList patchList = plCache.get(plKey);
- if (patchList == null) {
+ PatchList patchList;
+ try {
+ patchList = plCache.get(plKey);
+ } catch (PatchListNotAvailableException e) {
throw new SystemException("Cannot create " + plKey);
}
return patchList;
@@ -91,6 +104,8 @@
repo = gitMgr.openRepository(projectKey);
} catch (RepositoryNotFoundException e) {
throw new SystemException(e.getMessage());
+ } catch (IOException e) {
+ throw new SystemException(e.getMessage());
}
env.addToCleanup(new Runnable() {
@Override
@@ -102,6 +117,23 @@
}
};
+ public static final StoredValue<AnonymousUser> ANONYMOUS_USER =
+ new StoredValue<AnonymousUser>() {
+ @Override
+ protected AnonymousUser createValue(Prolog engine) {
+ PrologEnvironment env = (PrologEnvironment) engine.control;
+ return env.getInjector().getInstance(AnonymousUser.class);
+ }
+ };
+
+ public static final StoredValue<Map<Account.Id, IdentifiedUser>> USERS =
+ new StoredValue<Map<Account.Id, IdentifiedUser>>() {
+ @Override
+ protected Map<Account.Id, IdentifiedUser> createValue(Prolog engine) {
+ return Maps.newHashMap();
+ }
+ };
+
private StoredValues() {
}
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java b/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
index 4d1d631..ae76a6e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
@@ -26,8 +26,5 @@
SSH_COMMAND,
/** Access from a Git client using any Git protocol. */
- GIT,
-
- /** Access through replication */
- REPLICATION;
+ GIT;
}
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 3417111..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
@@ -14,23 +14,51 @@
package com.google.gerrit.server;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.Id;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
import java.io.IOException;
import java.util.Collections;
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 {
- /* Resync the changeOpen status which is cached in the approvals table for
- performance reasons*/
- public static void syncChangeStatus(final ReviewDb db, final Change change)
+ private final ReviewDb db;
+ private final ApprovalTypes approvalTypes;
+
+ @Inject
+ ApprovalsUtil(ReviewDb db, ApprovalTypes approvalTypes) {
+ this.db = db;
+ this.approvalTypes = approvalTypes;
+ }
+
+ /**
+ * Resync the changeOpen status which is cached in the approvals table for
+ * performance reasons
+ */
+ public void syncChangeStatus(final Change change)
throws OrmException {
final List<PatchSetApproval> approvals =
db.patchSetApprovals().byChange(change.getId()).toList();
@@ -44,14 +72,13 @@
* Moves the PatchSetApprovals to the last PatchSet on the change while
* keeping the vetos.
*
- * @param db The review database
* @param change Change to update
- * @param approvalTypes The approval types
* @throws OrmException
* @throws IOException
+ * @return List<PatchSetApproval> The previous approvals
*/
- public static void copyVetosToLatestPatchSet(final ReviewDb db, Change change,
- ApprovalTypes approvalTypes) throws OrmException, IOException {
+ public List<PatchSetApproval> copyVetosToLatestPatchSet(Change change)
+ throws OrmException, IOException {
PatchSet.Id source;
if (change.getNumberOfPatchSets() > 1) {
source = new PatchSet.Id(change.getId(), change.getNumberOfPatchSets() - 1);
@@ -60,15 +87,65 @@
}
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;
+ }
+
+
+ /** Attach reviewers to a change. */
+ public void addReviewers(Change change, PatchSet ps, PatchSetInfo info,
+ Set<Account.Id> wantReviewers) throws OrmException {
+ Set<Id> existing = Sets.<Account.Id> newHashSet();
+ addReviewers(change, ps, info, wantReviewers, existing);
+ }
+
+ /** Attach reviewers to a change. */
+ public void addReviewers(Change change, PatchSet ps, PatchSetInfo info,
+ Set<Account.Id> wantReviewers, Set<Account.Id> existingReviewers)
+ throws OrmException {
+ List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
+ if (allTypes.isEmpty()) {
+ return;
+ }
+
+ Set<Account.Id> need = Sets.newHashSet(wantReviewers);
+ Account.Id authorId = info.getAuthor() != null
+ ? info.getAuthor().getAccount()
+ : null;
+ if (authorId != null) {
+ need.add(authorId);
+ }
+
+ Account.Id committerId = info.getCommitter() != null
+ ? info.getCommitter().getAccount()
+ : null;
+ if (committerId != null) {
+ need.add(committerId);
+ }
+ need.remove(change.getOwner());
+ need.removeAll(existingReviewers);
+
+ List<PatchSetApproval> cells = Lists.newArrayListWithCapacity(need.size());
+ ApprovalCategory.Id catId = allTypes.get(allTypes.size() - 1).getCategory().getId();
+ for (Account.Id account : need) {
+ PatchSetApproval psa = new PatchSetApproval(
+ new PatchSetApproval.Key(ps.getId(), account, catId),
+ (short) 0);
+ psa.cache(change);
+ cells.add(psa);
+ }
+ db.patchSetApprovals().insert(cells);
}
}
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 9dc572d..4b98447 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
@@ -16,7 +16,6 @@
import com.google.gerrit.common.ChangeHookRunner;
import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Change.Status;
@@ -30,9 +29,9 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.TrackingFooter;
import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeOp;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.mail.EmailException;
import com.google.gerrit.server.mail.RebasedPatchSetSender;
import com.google.gerrit.server.mail.ReplacePatchSetSender;
@@ -241,9 +240,9 @@
RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
final ChangeHookRunner hooks, GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
- final ReplicationQueue replication, PersonIdent myIdent,
+ final GitReferenceUpdated replication, PersonIdent myIdent,
final ChangeControl.Factory changeControlFactory,
- final ApprovalTypes approvalTypes) throws NoSuchChangeException,
+ final ApprovalsUtil approvalsUtil) throws NoSuchChangeException,
EmailException, OrmException, MissingObjectException,
IncorrectObjectTypeException, IOException,
PatchSetInfoNotAvailableException, InvalidChangeOperationException {
@@ -382,20 +381,14 @@
+ ": " + ru.getResult());
}
- replication.scheduleUpdate(change.getProject(), ru.getName());
+ replication.fire(change.getProject(), ru.getName());
- ApprovalsUtil.copyVetosToLatestPatchSet(db, change, approvalTypes);
-
- 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 {
@@ -403,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());
@@ -425,7 +424,7 @@
final RevertedSender.Factory revertedSenderFactory,
final ChangeHooks hooks, GitRepositoryManager gitManager,
final PatchSetInfoFactory patchSetInfoFactory,
- final ReplicationQueue replication, PersonIdent myIdent)
+ final GitReferenceUpdated replication, PersonIdent myIdent)
throws NoSuchChangeException, EmailException, OrmException,
MissingObjectException, IncorrectObjectTypeException, IOException,
PatchSetInfoNotAvailableException {
@@ -496,7 +495,7 @@
throw new IOException("Failed to create ref " + ps.getRefName()
+ " in " + git.getDirectory() + ": " + ru.getResult());
}
- replication.scheduleUpdate(db.changes().get(changeId).getProject(),
+ replication.fire(db.changes().get(changeId).getProject(),
ru.getName());
final ChangeMessage cmsg =
@@ -526,7 +525,7 @@
public static void deleteDraftChange(final PatchSet.Id patchSetId,
GitRepositoryManager gitManager,
- final ReplicationQueue replication, final ReviewDb db)
+ final GitReferenceUpdated replication, final ReviewDb db)
throws NoSuchChangeException, OrmException, IOException {
final Change.Id changeId = patchSetId.getParentKey();
final Change change = db.changes().get(changeId);
@@ -547,7 +546,7 @@
public static void deleteOnlyDraftPatchSet(final PatchSet patch,
final Change change, GitRepositoryManager gitManager,
- final ReplicationQueue replication, final ReviewDb db)
+ final GitReferenceUpdated replication, final ReviewDb db)
throws NoSuchChangeException, OrmException, IOException {
final PatchSet.Id patchSetId = patch.getId();
if (patch == null || !patch.isDraft()) {
@@ -570,7 +569,7 @@
throw new IOException("Failed to delete ref " + patch.getRefName() +
" in " + repo.getDirectory() + ": " + update.getResult());
}
- replication.scheduleUpdate(change.getProject(), update.getName());
+ replication.fire(change.getProject(), update.getName());
} finally {
repo.close();
}
@@ -594,7 +593,7 @@
}
db.changeMessages().insert(Collections.singleton(cmsg));
- ApprovalsUtil.syncChangeStatus(db, change);
+ new ApprovalsUtil(db, null).syncChangeStatus(change);
// Email the reviewers
final ReplyToChangeSender cm = senderFactory.create(change);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index 6e519c4..050f7e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountDiffPreference;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -24,8 +25,9 @@
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.MaterializedGroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.config.AuthConfig;
@@ -46,13 +48,10 @@
import java.net.MalformedURLException;
import java.net.SocketAddress;
import java.net.URL;
-import java.util.AbstractSet;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
@@ -70,7 +69,7 @@
private final Provider<String> canonicalUrl;
private final Realm realm;
private final AccountCache accountCache;
- private final MaterializedGroupMembership.Factory groupMembershipFactory;
+ private final GroupBackend groupBackend;
@Inject
GenericFactory(
@@ -79,14 +78,14 @@
final @AnonymousCowardName String anonymousCowardName,
final @CanonicalWebUrl Provider<String> canonicalUrl,
final Realm realm, final AccountCache accountCache,
- final MaterializedGroupMembership.Factory groupMembershipFactory) {
+ final GroupBackend groupBackend) {
this.capabilityControlFactory = capabilityControlFactory;
this.authConfig = authConfig;
this.anonymousCowardName = anonymousCowardName;
this.canonicalUrl = canonicalUrl;
this.realm = realm;
this.accountCache = accountCache;
- this.groupMembershipFactory = groupMembershipFactory;
+ this.groupBackend = groupBackend;
}
public IdentifiedUser create(final Account.Id id) {
@@ -96,14 +95,14 @@
public IdentifiedUser create(Provider<ReviewDb> db, Account.Id id) {
return new IdentifiedUser(capabilityControlFactory, AccessPath.UNKNOWN,
authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
- groupMembershipFactory, null, db, id);
+ groupBackend, null, db, id);
}
public IdentifiedUser create(AccessPath accessPath,
Provider<SocketAddress> remotePeerProvider, Account.Id id) {
return new IdentifiedUser(capabilityControlFactory, accessPath,
authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
- groupMembershipFactory, remotePeerProvider, null, id);
+ groupBackend, remotePeerProvider, null, id);
}
}
@@ -121,7 +120,7 @@
private final Provider<String> canonicalUrl;
private final Realm realm;
private final AccountCache accountCache;
- private final MaterializedGroupMembership.Factory groupMembershipFactory;
+ private final GroupBackend groupBackend;
private final Provider<SocketAddress> remotePeerProvider;
private final Provider<ReviewDb> dbProvider;
@@ -133,7 +132,7 @@
final @AnonymousCowardName String anonymousCowardName,
final @CanonicalWebUrl Provider<String> canonicalUrl,
final Realm realm, final AccountCache accountCache,
- final MaterializedGroupMembership.Factory groupMembershipFactory,
+ final GroupBackend groupBackend,
final @RemotePeer Provider<SocketAddress> remotePeerProvider,
final Provider<ReviewDb> dbProvider) {
@@ -143,7 +142,7 @@
this.canonicalUrl = canonicalUrl;
this.realm = realm;
this.accountCache = accountCache;
- this.groupMembershipFactory = groupMembershipFactory;
+ this.groupBackend = groupBackend;
this.remotePeerProvider = remotePeerProvider;
this.dbProvider = dbProvider;
@@ -153,40 +152,22 @@
final Account.Id id) {
return new IdentifiedUser(capabilityControlFactory, accessPath,
authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
- groupMembershipFactory, remotePeerProvider, dbProvider, id);
+ groupBackend, remotePeerProvider, dbProvider, id);
}
}
private static final Logger log =
LoggerFactory.getLogger(IdentifiedUser.class);
- private static final Set<AccountGroup.UUID> registeredGroups =
- new AbstractSet<AccountGroup.UUID>() {
- private final List<AccountGroup.UUID> groups =
- Collections.unmodifiableList(Arrays.asList(new AccountGroup.UUID[] {
- AccountGroup.ANONYMOUS_USERS, AccountGroup.REGISTERED_USERS}));
-
- @Override
- public boolean contains(Object o) {
- return groups.contains(o);
- }
-
- @Override
- public Iterator<AccountGroup.UUID> iterator() {
- return groups.iterator();
- }
-
- @Override
- public int size() {
- return groups.size();
- }
- };
+ private static final GroupMembership registeredGroups =
+ new ListGroupMembership(ImmutableSet.of(
+ AccountGroup.ANONYMOUS_USERS,
+ AccountGroup.REGISTERED_USERS));
private final Provider<String> canonicalUrl;
- private final Realm realm;
private final AccountCache accountCache;
- private final MaterializedGroupMembership.Factory groupMembershipFactory;
private final AuthConfig authConfig;
+ private final GroupBackend groupBackend;
private final String anonymousCowardName;
@Nullable
@@ -210,14 +191,13 @@
final String anonymousCowardName,
final Provider<String> canonicalUrl,
final Realm realm, final AccountCache accountCache,
- final MaterializedGroupMembership.Factory groupMembershipFactory,
+ final GroupBackend groupBackend,
@Nullable final Provider<SocketAddress> remotePeerProvider,
@Nullable final Provider<ReviewDb> dbProvider, final Account.Id id) {
super(capabilityControlFactory, accessPath);
this.canonicalUrl = canonicalUrl;
- this.realm = realm;
this.accountCache = accountCache;
- this.groupMembershipFactory = groupMembershipFactory;
+ this.groupBackend = groupBackend;
this.authConfig = authConfig;
this.anonymousCowardName = anonymousCowardName;
this.remotePeerProvider = remotePeerProvider;
@@ -225,7 +205,8 @@
this.accountId = id;
}
- private AccountState state() {
+ // TODO(cranger): maybe get the state through the accountCache instead.
+ public AccountState state() {
if (state == null) {
state = accountCache.get(getAccountId());
}
@@ -272,12 +253,11 @@
public GroupMembership getEffectiveGroups() {
if (effectiveGroups == null) {
if (authConfig.isIdentityTrustable(state().getExternalIds())) {
- effectiveGroups = realm.groups(state());
+ effectiveGroups = groupBackend.membershipsOf(this);
} else {
- effectiveGroups = groupMembershipFactory.create(registeredGroups);
+ effectiveGroups = registeredGroups;
}
}
-
return effectiveGroups;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
new file mode 100644
index 0000000..eba59c8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
@@ -0,0 +1,64 @@
+// 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.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.inject.Inject;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * User identity for plugin code that needs an identity.
+ * <p>
+ * An InternalUser has no real identity, it acts as the server and can access
+ * anything it wants, anytime it wants, given the JVM's own direct access to
+ * data. Plugins may use this when they need to have a CurrentUser with read
+ * permission on anything.
+ */
+public class InternalUser extends CurrentUser {
+ public interface Factory {
+ InternalUser create();
+ }
+
+ @Inject
+ protected InternalUser(CapabilityControl.Factory capabilityControlFactory) {
+ super(capabilityControlFactory, AccessPath.UNKNOWN);
+ }
+
+ @Override
+ public GroupMembership getEffectiveGroups() {
+ return GroupMembership.EMPTY;
+ }
+
+ @Override
+ public Set<Change.Id> getStarredChanges() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Collection<AccountProjectWatch> getNotificationFilters() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public String toString() {
+ return "InternalUser";
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/OutputFormat.java b/gerrit-server/src/main/java/com/google/gerrit/server/OutputFormat.java
new file mode 100644
index 0000000..7e1ec4b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/OutputFormat.java
@@ -0,0 +1,71 @@
+// 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.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gwtjsonrpc.server.SqlTimestampDeserializer;
+
+import java.sql.Timestamp;
+
+/** Standard output format used by an API call. */
+public enum OutputFormat {
+ /**
+ * The output is a human readable text format. It may also be regular enough
+ * to be machine readable. Whether or not the text format is machine readable
+ * and will be committed to as a long term format that tools can build upon is
+ * specific to each API call.
+ */
+ TEXT,
+
+ /**
+ * Pretty-printed JSON format. This format uses whitespace to make the output
+ * readable by a human, but is also machine readable with a JSON library. The
+ * structure of the output is a long term format that tools can rely upon.
+ */
+ JSON,
+
+ /**
+ * Same as {@link #JSON}, but with unnecessary whitespace removed to save
+ * generation time and copy costs. Typically JSON_COMPACT format is used by a
+ * browser based HTML client running over the network.
+ */
+ JSON_COMPACT;
+
+ /** @return true when the format is either JSON or JSON_COMPACT. */
+ public boolean isJson() {
+ return this == JSON_COMPACT || this == JSON;
+ }
+
+ /** @return a new Gson instance configured according to the format. */
+ public GsonBuilder newGsonBuilder() {
+ if (!isJson()) {
+ throw new IllegalStateException(String.format("%s is not JSON", this));
+ }
+ GsonBuilder gb = new GsonBuilder()
+ .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .registerTypeAdapter(Timestamp.class, new SqlTimestampDeserializer());
+ if (this == OutputFormat.JSON) {
+ gb.setPrettyPrinting();
+ }
+ return gb;
+ }
+
+ /** @return a new Gson instance configured according to the format. */
+ public Gson newGson() {
+ return newGsonBuilder().create();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
index 352f540..75ba0d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.account.CapabilityControl;
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/ReplicationUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
deleted file mode 100644
index fc6ac9c..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright (C) 2009 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.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.ListGroupMembership;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Set;
-
-public class ReplicationUser extends CurrentUser {
- /** Magic set of groups enabling read of any project and reference. */
- public static final GroupMembership EVERYTHING_VISIBLE =
- new ListGroupMembership(Collections.<AccountGroup.UUID>emptySet());
-
- public interface Factory {
- ReplicationUser create(@Assisted GroupMembership authGroups);
- }
-
- private final GroupMembership effectiveGroups;
-
- @Inject
- protected ReplicationUser(CapabilityControl.Factory capabilityControlFactory,
- @Assisted GroupMembership authGroups) {
- super(capabilityControlFactory, AccessPath.REPLICATION);
- effectiveGroups = authGroups;
- }
-
- @Override
- public GroupMembership getEffectiveGroups() {
- return effectiveGroups;
- }
-
- @Override
- public Set<Change.Id> getStarredChanges() {
- return Collections.emptySet();
- }
-
- @Override
- public Collection<AccountProjectWatch> getNotificationFilters() {
- return Collections.emptySet();
- }
-
- public boolean isEverythingVisible() {
- return getEffectiveGroups() == EVERYTHING_VISIBLE;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
index 72fb2e8..4827ed5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -14,12 +14,14 @@
package com.google.gerrit.server.account;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Module;
@@ -27,45 +29,58 @@
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.util.Collections;
-import java.util.HashSet;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
/** Translates an email address to a set of matching accounts. */
@Singleton
public class AccountByEmailCacheImpl implements AccountByEmailCache {
+ private static final Logger log = LoggerFactory
+ .getLogger(AccountByEmailCacheImpl.class);
private static final String CACHE_NAME = "accounts_byemail";
public static Module module() {
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<String, Set<Account.Id>>> type =
- new TypeLiteral<Cache<String, Set<Account.Id>>>() {};
- core(type, CACHE_NAME).populateWith(Loader.class);
+ cache(CACHE_NAME,
+ String.class,
+ new TypeLiteral<Set<Account.Id>>() {})
+ .loader(Loader.class);
bind(AccountByEmailCacheImpl.class);
bind(AccountByEmailCache.class).to(AccountByEmailCacheImpl.class);
}
};
}
- private final Cache<String, Set<Account.Id>> cache;
+ private final LoadingCache<String, Set<Account.Id>> cache;
@Inject
AccountByEmailCacheImpl(
- @Named(CACHE_NAME) final Cache<String, Set<Account.Id>> cache) {
+ @Named(CACHE_NAME) LoadingCache<String, Set<Account.Id>> cache) {
this.cache = cache;
}
public Set<Account.Id> get(final String email) {
- return cache.get(email);
+ try {
+ return cache.get(email);
+ } catch (ExecutionException e) {
+ log.warn("Cannot resolve accounts by email", e);
+ return Collections.emptySet();
+ }
}
public void evict(final String email) {
- cache.remove(email);
+ if (email != null) {
+ cache.invalidate(email);
+ }
}
- static class Loader extends EntryCreator<String, Set<Account.Id>> {
+ static class Loader extends CacheLoader<String, Set<Account.Id>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -74,10 +89,10 @@
}
@Override
- public Set<Account.Id> createEntry(final String email) throws Exception {
+ public Set<Account.Id> load(String email) throws Exception {
final ReviewDb db = schema.open();
try {
- final HashSet<Account.Id> r = new HashSet<Account.Id>();
+ Set<Account.Id> r = Sets.newHashSet();
for (Account a : db.accounts().byPreferredEmail(email)) {
r.add(a.getId());
}
@@ -85,30 +100,10 @@
.byEmailAddress(email)) {
r.add(a.getAccountId());
}
- return pack(r);
+ return ImmutableSet.copyOf(r);
} finally {
db.close();
}
}
-
- @Override
- public Set<Account.Id> missing(final String key) {
- return Collections.emptySet();
- }
-
- private static Set<Account.Id> pack(final Set<Account.Id> c) {
- switch (c.size()) {
- case 0:
- return Collections.emptySet();
- case 1:
- return one(c);
- default:
- return Collections.unmodifiableSet(new HashSet<Account.Id>(c));
- }
- }
-
- private static <T> Set<T> one(final Set<T> c) {
- return Collections.singleton(c.iterator().next());
- }
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 819ec31..4217f9f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -14,14 +14,16 @@
package com.google.gerrit.server.account;
+import com.google.common.base.Optional;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
@@ -30,14 +32,21 @@
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
/** Caches important (but small) account state to avoid database hits. */
@Singleton
public class AccountCacheImpl implements AccountCache {
+ private static final Logger log = LoggerFactory
+ .getLogger(AccountCacheImpl.class);
+
private static final String BYID_NAME = "accounts";
private static final String BYUSER_NAME = "accounts_byname";
@@ -45,13 +54,13 @@
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<Account.Id, AccountState>> byIdType =
- new TypeLiteral<Cache<Account.Id, AccountState>>() {};
- core(byIdType, BYID_NAME).populateWith(ByIdLoader.class);
+ cache(BYID_NAME, Account.Id.class, AccountState.class)
+ .loader(ByIdLoader.class);
- final TypeLiteral<Cache<String, Account.Id>> byUsernameType =
- new TypeLiteral<Cache<String, Account.Id>>() {};
- core(byUsernameType, BYUSER_NAME).populateWith(ByNameLoader.class);
+ cache(BYUSER_NAME,
+ String.class,
+ new TypeLiteral<Optional<Account.Id>>() {})
+ .loader(ByNameLoader.class);
bind(AccountCacheImpl.class);
bind(AccountCache.class).to(AccountCacheImpl.class);
@@ -59,54 +68,76 @@
};
}
- private final Cache<Account.Id, AccountState> byId;
- private final Cache<String, Account.Id> byName;
+ private final LoadingCache<Account.Id, AccountState> byId;
+ private final LoadingCache<String, Optional<Account.Id>> byName;
@Inject
- AccountCacheImpl(@Named(BYID_NAME) Cache<Account.Id, AccountState> byId,
- @Named(BYUSER_NAME) Cache<String, Account.Id> byUsername) {
+ AccountCacheImpl(@Named(BYID_NAME) LoadingCache<Account.Id, AccountState> byId,
+ @Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername) {
this.byId = byId;
this.byName = byUsername;
}
- public AccountState get(final Account.Id accountId) {
- return byId.get(accountId);
+ public AccountState get(Account.Id accountId) {
+ try {
+ return byId.get(accountId);
+ } catch (ExecutionException e) {
+ log.warn("Cannot load AccountState for " + accountId, e);
+ return missing(accountId);
+ }
}
@Override
public AccountState getByUsername(String username) {
- Account.Id id = byName.get(username);
- return id != null ? byId.get(id) : null;
+ try {
+ Optional<Account.Id> id = byName.get(username);
+ return id != null && id.isPresent() ? byId.get(id.get()) : null;
+ } catch (ExecutionException e) {
+ log.warn("Cannot load AccountState for " + username, e);
+ return null;
+ }
}
- public void evict(final Account.Id accountId) {
- byId.remove(accountId);
+ public void evict(Account.Id accountId) {
+ if (accountId != null) {
+ byId.invalidate(accountId);
+ }
}
public void evictByUsername(String username) {
- byName.remove(username);
+ if (username != null) {
+ byName.invalidate(username);
+ }
}
- static class ByIdLoader extends EntryCreator<Account.Id, AccountState> {
+ private static AccountState missing(Account.Id accountId) {
+ Account account = new Account(accountId);
+ Collection<AccountExternalId> ids = Collections.emptySet();
+ Set<AccountGroup.UUID> anon = ImmutableSet.of(AccountGroup.ANONYMOUS_USERS);
+ return new AccountState(account, anon, ids);
+ }
+
+ static class ByIdLoader extends CacheLoader<Account.Id, AccountState> {
private final SchemaFactory<ReviewDb> schema;
private final GroupCache groupCache;
- private final Cache<String, Account.Id> byName;
+ private final LoadingCache<String, Optional<Account.Id>> byName;
@Inject
ByIdLoader(SchemaFactory<ReviewDb> sf, GroupCache groupCache,
- @Named(BYUSER_NAME) Cache<String, Account.Id> byUsername) {
+ @Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername) {
this.schema = sf;
this.groupCache = groupCache;
this.byName = byUsername;
}
@Override
- public AccountState createEntry(final Account.Id key) throws Exception {
+ public AccountState load(Account.Id key) throws Exception {
final ReviewDb db = schema.open();
try {
final AccountState state = load(db, key);
- if (state.getUserName() != null) {
- byName.put(state.getUserName(), state.getAccount().getId());
+ String user = state.getUserName();
+ if (user != null) {
+ byName.put(user, Optional.of(state.getAccount().getId()));
}
return state;
} finally {
@@ -142,18 +173,9 @@
return new AccountState(account, internalGroups, externalIds);
}
-
- @Override
- public AccountState missing(final Account.Id accountId) {
- final Account account = new Account(accountId);
- final Collection<AccountExternalId> ids = Collections.emptySet();
- final Set<AccountGroup.UUID> anonymous =
- Collections.singleton(AccountGroup.ANONYMOUS_USERS);
- return new AccountState(account, anonymous, ids);
- }
}
- static class ByNameLoader extends EntryCreator<String, Account.Id> {
+ static class ByNameLoader extends CacheLoader<String, Optional<Account.Id>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -162,14 +184,17 @@
}
@Override
- public Account.Id createEntry(final String username) throws Exception {
+ public Optional<Account.Id> load(String username) throws Exception {
final ReviewDb db = schema.open();
try {
final AccountExternalId.Key key = new AccountExternalId.Key( //
AccountExternalId.SCHEME_USERNAME, //
username);
final AccountExternalId id = db.accountExternalIds().get(key);
- return id != null ? id.getAccountId() : null;
+ if (id != null) {
+ return Optional.of(id.getAccountId());
+ }
+ return Optional.absent();
} finally {
db.close();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
index b297ed8..32b4e2c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
@@ -14,11 +14,14 @@
package com.google.gerrit.server.account;
+import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.AccountsSection;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -27,16 +30,19 @@
/** Access control management for one account's access to other accounts. */
public class AccountControl {
public static class Factory {
+ private final ProjectCache projectCache;
private final GroupControl.Factory groupControlFactory;
private final Provider<CurrentUser> user;
private final IdentifiedUser.GenericFactory userFactory;
private final AccountVisibility accountVisibility;
@Inject
- Factory(final GroupControl.Factory groupControlFactory,
+ Factory(final ProjectCache projectCache,
+ final GroupControl.Factory groupControlFactory,
final Provider<CurrentUser> user,
final IdentifiedUser.GenericFactory userFactory,
final AccountVisibility accountVisibility) {
+ this.projectCache = projectCache;
this.groupControlFactory = groupControlFactory;
this.user = user;
this.userFactory = userFactory;
@@ -44,20 +50,24 @@
}
public AccountControl get() {
- return new AccountControl(groupControlFactory, user.get(), userFactory,
- accountVisibility);
+ return new AccountControl(projectCache, groupControlFactory, user.get(),
+ userFactory, accountVisibility);
}
}
+ private final AccountsSection accountsSection;
private final GroupControl.Factory groupControlFactory;
private final CurrentUser currentUser;
private final IdentifiedUser.GenericFactory userFactory;
private final AccountVisibility accountVisibility;
- AccountControl(final GroupControl.Factory groupControlFactory,
+ AccountControl(final ProjectCache projectCache,
+ final GroupControl.Factory groupControlFactory,
final CurrentUser currentUser,
final IdentifiedUser.GenericFactory userFactory,
final AccountVisibility accountVisibility) {
+ this.accountsSection =
+ projectCache.getAllProjects().getConfig().getAccountsSection();
this.groupControlFactory = groupControlFactory;
this.currentUser = currentUser;
this.userFactory = userFactory;
@@ -73,10 +83,21 @@
* effective groups.
*/
public boolean canSee(final Account otherUser) {
+ return canSee(otherUser.getId());
+ }
+
+ /**
+ * Returns true if the otherUser is allowed to see the current user, based
+ * on the account visibility policy. Depending on the group membership
+ * realms supported, this may not be able to determine SAME_GROUP or
+ * VISIBLE_GROUP correctly (defaulting to not being visible). This is because
+ * {@link GroupMembership#getKnownGroups()} may only return a subset of the
+ * effective groups.
+ */
+ public boolean canSee(final Account.Id otherUser) {
// Special case: I can always see myself.
if (currentUser instanceof IdentifiedUser
- && ((IdentifiedUser) currentUser).getAccountId()
- .equals(otherUser.getId())) {
+ && ((IdentifiedUser) currentUser).getAccountId().equals(otherUser)) {
return true;
}
@@ -87,6 +108,12 @@
Set<AccountGroup.UUID> usersGroups = groupsOf(otherUser);
usersGroups.remove(AccountGroup.ANONYMOUS_USERS);
usersGroups.remove(AccountGroup.REGISTERED_USERS);
+ for (PermissionRule rule : accountsSection.getSameGroupVisibility()) {
+ if (rule.isBlock() || rule.isDeny()) {
+ usersGroups.remove(rule.getGroup().getUUID());
+ }
+ }
+
if (currentUser.getEffectiveGroups().containsAnyOf(usersGroups)) {
return true;
}
@@ -115,7 +142,7 @@
return false;
}
- private Set<AccountGroup.UUID> groupsOf(Account account) {
- return userFactory.create(account.getId()).getEffectiveGroups().getKnownGroups();
+ private Set<AccountGroup.UUID> groupsOf(Account.Id account) {
+ return userFactory.create(account).getEffectiveGroups().getKnownGroups();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 6c216a8..e469c34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -426,6 +426,56 @@
}
}
+ /**
+ * Unlink an authentication identity from an existing account.
+ *
+ * @param from account to unlink the identity from.
+ * @param who the identity to delete
+ * @return the result of unlinking the identity from the user.
+ * @throws AccountException the identity belongs to a different account, or it
+ * cannot be unlinked at this time.
+ */
+ public AuthResult unlink(final Account.Id from, AuthRequest who)
+ throws AccountException {
+ try {
+ final ReviewDb db = schema.open();
+ try {
+ who = realm.unlink(db, from, who);
+
+ final AccountExternalId.Key key = id(who);
+ AccountExternalId extId = db.accountExternalIds().get(key);
+ if (extId != null) {
+ if (!extId.getAccountId().equals(from)) {
+ throw new AccountException("Identity in use by another account");
+ }
+ db.accountExternalIds().delete(Collections.singleton(extId));
+
+ if (who.getEmailAddress() != null) {
+ final Account a = db.accounts().get(from);
+ if (a.getPreferredEmail() != null
+ && a.getPreferredEmail().equals(who.getEmailAddress())) {
+ a.setPreferredEmail(null);
+ db.accounts().update(Collections.singleton(a));
+ }
+ byEmailCache.evict(who.getEmailAddress());
+ byIdCache.evict(from);
+ }
+
+ } else {
+ throw new AccountException("Identity not found");
+ }
+
+ return new AuthResult(from, key, false);
+
+ } finally {
+ db.close();
+ }
+ } catch (OrmException e) {
+ throw new AccountException("Cannot unlink identity", e);
+ }
+ }
+
+
private static AccountExternalId.Key id(final AuthRequest who) {
return new AccountExternalId.Key(who.getExternalId());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index 56a95ba..c90f3e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -15,26 +15,20 @@
package com.google.gerrit.server.account;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.inject.Inject;
-import java.util.Collections;
-import java.util.List;
import java.util.Set;
public class DefaultRealm implements Realm {
private final EmailExpander emailExpander;
private final AccountByEmailCache byEmail;
- private final MaterializedGroupMembership.Factory groupMembershipFactory;
@Inject
DefaultRealm(final EmailExpander emailExpander,
- final AccountByEmailCache byEmail,
- final MaterializedGroupMembership.Factory groupMembershipFactory) {
+ final AccountByEmailCache byEmail) {
this.emailExpander = emailExpander;
this.byEmail = byEmail;
- this.groupMembershipFactory = groupMembershipFactory;
}
@Override
@@ -57,12 +51,12 @@
}
@Override
- public void onCreateAccount(final AuthRequest who, final Account account) {
+ public AuthRequest unlink(ReviewDb db, Account.Id from, AuthRequest who) {
+ return who;
}
@Override
- public GroupMembership groups(final AccountState who) {
- return groupMembershipFactory.create(who.getInternalGroups());
+ public void onCreateAccount(final AuthRequest who, final Account account) {
}
@Override
@@ -75,9 +69,4 @@
}
return null;
}
-
- @Override
- public Set<AccountGroup.ExternalNameKey> lookupGroups(String name) {
- return Collections.emptySet();
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
new file mode 100644
index 0000000..b4e770f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.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.server.account;
+
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+
+import java.util.Collection;
+
+import javax.annotation.Nullable;
+
+/**
+ * Implementations of GroupBackend provide lookup and membership accessors
+ * to a group system.
+ */
+@ExtensionPoint
+public interface GroupBackend {
+ /** @return {@code true} if the backend can operate on the UUID. */
+ boolean handles(AccountGroup.UUID uuid);
+
+ /**
+ * Looks up a group in the backend. If the group does not exist, null is
+ * returned.
+ *
+ * @param uuid the group identifier
+ * @return the group
+ */
+ @Nullable
+ GroupDescription.Basic get(AccountGroup.UUID uuid);
+
+ /** @return suggestions for the group name sorted by name. */
+ Collection<GroupReference> suggest(String name);
+
+ /** @return the group membership checker for the backend. */
+ GroupMembership membershipsOf(IdentifiedUser user);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
new file mode 100644
index 0000000..cdbb0e4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
@@ -0,0 +1,89 @@
+// 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.account;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.data.GroupReference;
+
+import java.util.Collection;
+import java.util.Comparator;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility class for dealing with a GroupBackend.
+ */
+public class GroupBackends {
+
+ public static final Comparator<GroupReference> GROUP_REF_NAME_COMPARATOR =
+ new Comparator<GroupReference>() {
+ @Override
+ public int compare(GroupReference a, GroupReference b) {
+ return a.getName().compareTo(b.getName());
+ }
+ };
+
+ /**
+ * Runs {@link GroupBackend#suggest(String)} and filters the result to return
+ * the best suggestion, or null if one does not exist.
+ *
+ * @param groupBackend the group backend
+ * @param name the name for which to suggest groups
+ * @return the best single GroupReference suggestion
+ */
+ @Nullable
+ public static GroupReference findBestSuggestion(
+ GroupBackend groupBackend, String name) {
+ Collection<GroupReference> refs = groupBackend.suggest(name);
+ if (refs.size() == 1) {
+ return Iterables.getOnlyElement(refs);
+ }
+
+ for (GroupReference ref : refs) {
+ if (isExactSuggestion(ref, name)) {
+ return ref;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Runs {@link GroupBackend#suggest(String)} and filters the result to return
+ * the exact suggestion, or null if one does not exist.
+ *
+ * @param groupBackend the group backend
+ * @param name the name for which to suggest groups
+ * @return the exact single GroupReference suggestion
+ */
+ @Nullable
+ public static GroupReference findExactSuggestion(
+ GroupBackend groupBackend, String name) {
+ Collection<GroupReference> refs = groupBackend.suggest(name);
+ for (GroupReference ref : refs) {
+ if (isExactSuggestion(ref, name)) {
+ return ref;
+ }
+ }
+ return null;
+ }
+
+ /** Returns whether the GroupReference is an exact suggestion for the name. */
+ public static boolean isExactSuggestion(GroupReference ref, String name) {
+ return ref.getName().equalsIgnoreCase(name) || ref.getUUID().get().equals(name);
+ }
+
+ private GroupBackends() {
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
index b092ac4..3b9e85f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
@@ -16,8 +16,6 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
-import java.util.Collection;
-
import javax.annotation.Nullable;
/** Tracks group objects in memory for efficient access. */
@@ -34,8 +32,6 @@
@Nullable
public AccountGroup get(AccountGroup.UUID uuid);
- public Collection<AccountGroup> get(AccountGroup.ExternalNameKey externalName);
-
/** @return sorted iteration of groups. */
public abstract Iterable<AccountGroup> all();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
index d29a5e5..b301839 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -14,12 +14,15 @@
package com.google.gerrit.server.account;
+import com.google.common.base.Optional;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupName;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Module;
@@ -27,48 +30,41 @@
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
-import java.util.ArrayList;
-import java.util.Collection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.util.Collections;
import java.util.List;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.ExecutionException;
/** Tracks group objects in memory for efficient access. */
@Singleton
public class GroupCacheImpl implements GroupCache {
+ private static final Logger log = LoggerFactory
+ .getLogger(GroupCacheImpl.class);
+
private static final String BYID_NAME = "groups";
private static final String BYNAME_NAME = "groups_byname";
private static final String BYUUID_NAME = "groups_byuuid";
- private static final String BYEXT_NAME = "groups_byext";
- private static final String BYNAME_LIST = "groups_byname_list";
public static Module module() {
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<AccountGroup.Id, AccountGroup>> byId =
- new TypeLiteral<Cache<AccountGroup.Id, AccountGroup>>() {};
- core(byId, BYID_NAME).populateWith(ByIdLoader.class);
+ cache(BYID_NAME,
+ AccountGroup.Id.class,
+ new TypeLiteral<Optional<AccountGroup>>() {})
+ .loader(ByIdLoader.class);
- final TypeLiteral<Cache<AccountGroup.NameKey, AccountGroup>> byName =
- new TypeLiteral<Cache<AccountGroup.NameKey, AccountGroup>>() {};
- core(byName, BYNAME_NAME).populateWith(ByNameLoader.class);
+ cache(BYNAME_NAME,
+ String.class,
+ new TypeLiteral<Optional<AccountGroup>>() {})
+ .loader(ByNameLoader.class);
- final TypeLiteral<Cache<AccountGroup.UUID, AccountGroup>> byUUID =
- new TypeLiteral<Cache<AccountGroup.UUID, AccountGroup>>() {};
- core(byUUID, BYUUID_NAME).populateWith(ByUUIDLoader.class);
-
- final TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>> byExternalName =
- new TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>>() {};
- core(byExternalName, BYEXT_NAME) //
- .populateWith(ByExternalNameLoader.class);
-
- final TypeLiteral<Cache<ListKey, SortedSet<AccountGroup.NameKey>>> listType =
- new TypeLiteral<Cache<ListKey, SortedSet<AccountGroup.NameKey>>>() {};
- core(listType, BYNAME_LIST).populateWith(Lister.class);
+ cache(BYUUID_NAME,
+ String.class,
+ new TypeLiteral<Optional<AccountGroup>>() {})
+ .loader(ByUUIDLoader.class);
bind(GroupCacheImpl.class);
bind(GroupCache.class).to(GroupCacheImpl.class);
@@ -76,94 +72,113 @@
};
}
- private final Cache<AccountGroup.Id, AccountGroup> byId;
- private final Cache<AccountGroup.NameKey, AccountGroup> byName;
- private final Cache<AccountGroup.UUID, AccountGroup> byUUID;
- private final Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName;
- private final Cache<ListKey,SortedSet<AccountGroup.NameKey>> list;
- private final Lock listLock;
+ private final LoadingCache<AccountGroup.Id, Optional<AccountGroup>> byId;
+ private final LoadingCache<String, Optional<AccountGroup>> byName;
+ private final LoadingCache<String, Optional<AccountGroup>> byUUID;
+ private final SchemaFactory<ReviewDb> schema;
@Inject
GroupCacheImpl(
- @Named(BYID_NAME) Cache<AccountGroup.Id, AccountGroup> byId,
- @Named(BYNAME_NAME) Cache<AccountGroup.NameKey, AccountGroup> byName,
- @Named(BYUUID_NAME) Cache<AccountGroup.UUID, AccountGroup> byUUID,
- @Named(BYEXT_NAME) Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName,
- @Named(BYNAME_LIST) final Cache<ListKey, SortedSet<AccountGroup.NameKey>> list) {
+ @Named(BYID_NAME) LoadingCache<AccountGroup.Id, Optional<AccountGroup>> byId,
+ @Named(BYNAME_NAME) LoadingCache<String, Optional<AccountGroup>> byName,
+ @Named(BYUUID_NAME) LoadingCache<String, Optional<AccountGroup>> byUUID,
+ SchemaFactory<ReviewDb> schema) {
this.byId = byId;
this.byName = byName;
this.byUUID = byUUID;
- this.byExternalName = byExternalName;
- this.list = list;
- this.listLock = new ReentrantLock(true /* fair */);
+ this.schema = schema;
}
+ @Override
public AccountGroup get(final AccountGroup.Id groupId) {
- return byId.get(groupId);
+ try {
+ Optional<AccountGroup> g = byId.get(groupId);
+ return g.isPresent() ? g.get() : missing(groupId);
+ } catch (ExecutionException e) {
+ log.warn("Cannot load group "+groupId, e);
+ return missing(groupId);
+ }
}
+ @Override
public void evict(final AccountGroup group) {
- byId.remove(group.getId());
- byName.remove(group.getNameKey());
- byUUID.remove(group.getGroupUUID());
- byExternalName.remove(group.getExternalNameKey());
+ if (group.getId() != null) {
+ byId.invalidate(group.getId());
+ }
+ if (group.getNameKey() != null) {
+ byName.invalidate(group.getNameKey().get());
+ }
+ if (group.getGroupUUID() != null) {
+ byUUID.invalidate(group.getGroupUUID().get());
+ }
}
+ @Override
public void evictAfterRename(final AccountGroup.NameKey oldName,
final AccountGroup.NameKey newName) {
- byName.remove(oldName);
- updateGroupList(oldName, newName);
+ if (oldName != null) {
+ byName.invalidate(oldName.get());
+ }
+ if (newName != null) {
+ byName.invalidate(newName.get());
+ }
}
- public AccountGroup get(final AccountGroup.NameKey name) {
- return byName.get(name);
+ @Override
+ public AccountGroup get(AccountGroup.NameKey name) {
+ if (name == null) {
+ return null;
+ }
+ try {
+ return byName.get(name.get()).orNull();
+ } catch (ExecutionException e) {
+ log.warn(String.format("Cannot lookup group %s by name", name.get()), e);
+ return null;
+ }
}
- public AccountGroup get(final AccountGroup.UUID uuid) {
- return byUUID.get(uuid);
- }
-
- public Collection<AccountGroup> get(
- final AccountGroup.ExternalNameKey externalName) {
- return byExternalName.get(externalName);
+ @Override
+ public AccountGroup get(AccountGroup.UUID uuid) {
+ if (uuid == null) {
+ return null;
+ }
+ try {
+ return byUUID.get(uuid.get()).orNull();
+ } catch (ExecutionException e) {
+ log.warn(String.format("Cannot lookup group %s by name", uuid.get()), e);
+ return null;
+ }
}
@Override
public Iterable<AccountGroup> all() {
- final List<AccountGroup> groups = new ArrayList<AccountGroup>();
- for (final AccountGroup.NameKey groupName : list.get(ListKey.ALL)) {
- final AccountGroup group = get(groupName);
- if (group != null) {
- groups.add(group);
+ try {
+ ReviewDb db = schema.open();
+ try {
+ return Collections.unmodifiableList(db.accountGroups().all().toList());
+ } finally {
+ db.close();
}
+ } catch (OrmException e) {
+ log.warn("Cannot list internal groups", e);
+ return Collections.emptyList();
}
- return Collections.unmodifiableList(groups);
}
@Override
- public void onCreateGroup(final AccountGroup.NameKey newGroupName) {
- updateGroupList(null, newGroupName);
+ public void onCreateGroup(AccountGroup.NameKey newGroupName) {
+ byName.invalidate(newGroupName.get());
}
- private void updateGroupList(final AccountGroup.NameKey nameToRemove,
- final AccountGroup.NameKey nameToAdd) {
- listLock.lock();
- try {
- SortedSet<AccountGroup.NameKey> n = list.get(ListKey.ALL);
- n = new TreeSet<AccountGroup.NameKey>(n);
- if (nameToRemove != null) {
- n.remove(nameToRemove);
- }
- if (nameToAdd != null) {
- n.add(nameToAdd);
- }
- list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
- } finally {
- listLock.unlock();
- }
+ private static AccountGroup missing(AccountGroup.Id key) {
+ AccountGroup.NameKey name = new AccountGroup.NameKey("Deleted Group" + key);
+ AccountGroup g = new AccountGroup(name, key, null);
+ g.setType(AccountGroup.Type.SYSTEM);
+ return g;
}
- static class ByIdLoader extends EntryCreator<AccountGroup.Id, AccountGroup> {
+ static class ByIdLoader extends
+ CacheLoader<AccountGroup.Id, Optional<AccountGroup>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -172,32 +187,18 @@
}
@Override
- public AccountGroup createEntry(final AccountGroup.Id key) throws Exception {
+ public Optional<AccountGroup> load(final AccountGroup.Id key)
+ throws Exception {
final ReviewDb db = schema.open();
try {
- final AccountGroup group = db.accountGroups().get(key);
- if (group != null) {
- return group;
- } else {
- return missing(key);
- }
+ return Optional.fromNullable(db.accountGroups().get(key));
} finally {
db.close();
}
}
-
- @Override
- public AccountGroup missing(final AccountGroup.Id key) {
- final AccountGroup.NameKey name =
- new AccountGroup.NameKey("Deleted Group" + key.toString());
- final AccountGroup g = new AccountGroup(name, key, null);
- g.setType(AccountGroup.Type.SYSTEM);
- return g;
- }
}
- static class ByNameLoader extends
- EntryCreator<AccountGroup.NameKey, AccountGroup> {
+ static class ByNameLoader extends CacheLoader<String, Optional<AccountGroup>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -206,25 +207,23 @@
}
@Override
- public AccountGroup createEntry(final AccountGroup.NameKey key)
+ public Optional<AccountGroup> load(String name)
throws Exception {
- final AccountGroupName r;
final ReviewDb db = schema.open();
try {
- r = db.accountGroupNames().get(key);
+ AccountGroup.NameKey key = new AccountGroup.NameKey(name);
+ AccountGroupName r = db.accountGroupNames().get(key);
if (r != null) {
- return db.accountGroups().get(r.getId());
- } else {
- return null;
+ return Optional.fromNullable(db.accountGroups().get(r.getId()));
}
+ return Optional.absent();
} finally {
db.close();
}
}
}
- static class ByUUIDLoader extends
- EntryCreator<AccountGroup.UUID, AccountGroup> {
+ static class ByUUIDLoader extends CacheLoader<String, Optional<AccountGroup>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -233,74 +232,23 @@
}
@Override
- public AccountGroup createEntry(final AccountGroup.UUID uuid)
+ public Optional<AccountGroup> load(String uuid)
throws Exception {
final ReviewDb db = schema.open();
try {
- List<AccountGroup> r = db.accountGroups().byUUID(uuid).toList();
+ List<AccountGroup> r;
+
+ r = db.accountGroups().byUUID(new AccountGroup.UUID(uuid)).toList();
if (r.size() == 1) {
- return r.get(0);
+ return Optional.of(r.get(0));
+ } else if (r.size() == 0) {
+ return Optional.absent();
} else {
- return null;
+ throw new OrmDuplicateKeyException("Duplicate group UUID " + uuid);
}
} finally {
db.close();
}
}
}
-
- static class ByExternalNameLoader extends
- EntryCreator<AccountGroup.ExternalNameKey, Collection<AccountGroup>> {
- private final SchemaFactory<ReviewDb> schema;
-
- @Inject
- ByExternalNameLoader(final SchemaFactory<ReviewDb> sf) {
- schema = sf;
- }
-
- @Override
- public Collection<AccountGroup> createEntry(
- final AccountGroup.ExternalNameKey key) throws Exception {
- final ReviewDb db = schema.open();
- try {
- return db.accountGroups().byExternalName(key).toList();
- } finally {
- db.close();
- }
- }
- }
-
- static class ListKey {
- static final ListKey ALL = new ListKey();
-
- private ListKey() {
- }
- }
-
- static class Lister extends EntryCreator<ListKey, SortedSet<AccountGroup.NameKey>> {
- private final SchemaFactory<ReviewDb> schema;
-
- @Inject
- Lister(final SchemaFactory<ReviewDb> sf) {
- schema = sf;
- }
-
- @Override
- public SortedSet<AccountGroup.NameKey> createEntry(ListKey key)
- throws Exception {
- final ReviewDb db = schema.open();
- try {
- final List<AccountGroupName> groupNames =
- db.accountGroupNames().all().toList();
- final SortedSet<AccountGroup.NameKey> groups =
- new TreeSet<AccountGroup.NameKey>();
- for (final AccountGroupName groupName : groupNames) {
- groups.add(groupName.getNameKey());
- }
- return Collections.unmodifiableSortedSet(groups);
- } finally {
- db.close();
- }
- }
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index f7451a8..d9b12ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.account;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescriptions;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -27,11 +29,14 @@
public static class Factory {
private final GroupCache groupCache;
private final Provider<CurrentUser> user;
+ private final GroupBackend groupBackend;
@Inject
- Factory(final GroupCache gc, final Provider<CurrentUser> cu) {
+ Factory(final GroupCache gc, final Provider<CurrentUser> cu,
+ final GroupBackend gb) {
groupCache = gc;
user = cu;
+ groupBackend = gb;
}
public GroupControl controlFor(final AccountGroup.Id groupId)
@@ -40,20 +45,20 @@
if (group == null) {
throw new NoSuchGroupException(groupId);
}
- return new GroupControl(groupCache, user.get(), group);
+ return new GroupControl(user.get(), group);
}
public GroupControl controlFor(final AccountGroup.UUID groupId)
throws NoSuchGroupException {
- final AccountGroup group = groupCache.get(groupId);
+ final GroupDescription.Basic group = groupBackend.get(groupId);
if (group == null) {
throw new NoSuchGroupException(groupId);
}
- return new GroupControl(groupCache, user.get(), group);
+ return new GroupControl(user.get(), group);
}
public GroupControl controlFor(final AccountGroup group) {
- return new GroupControl(groupCache, user.get(), group);
+ return new GroupControl(user.get(), group);
}
public GroupControl validateFor(final AccountGroup.Id groupId)
@@ -66,25 +71,23 @@
}
}
- private final GroupCache groupCache;
private final CurrentUser user;
- private final AccountGroup group;
+ private final GroupDescription.Basic group;
private Boolean isOwner;
- GroupControl(GroupCache g, CurrentUser who, AccountGroup gc) {
- groupCache = g;
+ GroupControl(CurrentUser who, GroupDescription.Basic gd) {
user = who;
- group = gc;
+ group = gd;
+ }
+
+ GroupControl(CurrentUser who, AccountGroup ag) {
+ this(who, GroupDescriptions.forAccountGroup(ag));
}
public CurrentUser getCurrentUser() {
return user;
}
- public AccountGroup getAccountGroup() {
- return group;
- }
-
/** Can this user see this group exists? */
public boolean isVisible() {
return group.isVisibleToAll()
@@ -93,9 +96,11 @@
}
public boolean isOwner() {
- if (isOwner == null) {
- AccountGroup g = groupCache.get(group.getOwnerGroupId());
- AccountGroup.UUID ownerUUID = g != null ? g.getGroupUUID() : null;
+ AccountGroup accountGroup = GroupDescriptions.toAccountGroup(group);
+ if (accountGroup == null) {
+ isOwner = false;
+ } else if (isOwner == null) {
+ AccountGroup.UUID ownerUUID = accountGroup.getOwnerGroupUUID();
isOwner = getCurrentUser().getEffectiveGroups().contains(ownerUUID)
|| getCurrentUser().getCapabilities().canAdministrateServer();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
index 7f88134..2e500bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
@@ -14,7 +14,9 @@
package com.google.gerrit.server.account;
+import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -39,6 +41,7 @@
private final ReviewDb db;
private final GroupControl.Factory groupControl;
private final GroupCache groupCache;
+ private final GroupBackend groupBackend;
private final AccountInfoCacheFactory aic;
private final GroupInfoCacheFactory gic;
@@ -48,12 +51,14 @@
@Inject
GroupDetailFactory(final ReviewDb db,
final GroupControl.Factory groupControl, final GroupCache groupCache,
+ final GroupBackend groupBackend,
final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
final GroupInfoCacheFactory.Factory groupInfoCacheFactory,
@Assisted final AccountGroup.Id groupId) {
this.db = db;
this.groupControl = groupControl;
this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
this.aic = accountInfoCacheFactory.create();
this.gic = groupInfoCacheFactory.create();
@@ -63,10 +68,13 @@
@Override
public GroupDetail call() throws OrmException, NoSuchGroupException {
control = groupControl.validateFor(groupId);
- final AccountGroup group = control.getAccountGroup();
+ final AccountGroup group = groupCache.get(groupId);
final GroupDetail detail = new GroupDetail();
detail.setGroup(group);
- detail.setOwnerGroup(groupCache.get(group.getOwnerGroupId()));
+ GroupDescription.Basic ownerGroup = groupBackend.get(group.getOwnerGroupUUID());
+ if (ownerGroup != null) {
+ detail.setOwnerGroup(GroupReference.forGroup(ownerGroup));
+ }
switch (group.getType()) {
case INTERNAL:
detail.setMembers(loadMembers());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 791d0f5..7fbba45 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -14,12 +14,14 @@
package com.google.gerrit.server.account;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupInclude;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Module;
@@ -27,24 +29,30 @@
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.util.Collection;
import java.util.Collections;
-import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
/** Tracks group inclusions in memory for efficient access. */
@Singleton
public class GroupIncludeCacheImpl implements GroupIncludeCache {
+ private static final Logger log = LoggerFactory
+ .getLogger(GroupIncludeCacheImpl.class);
private static final String BYINCLUDE_NAME = "groups_byinclude";
public static Module module() {
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>>> byInclude =
- new TypeLiteral<Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>>>() {};
- core(byInclude, BYINCLUDE_NAME).populateWith(ByIncludeLoader.class);
+ cache(BYINCLUDE_NAME,
+ AccountGroup.UUID.class,
+ new TypeLiteral<Set<AccountGroup.UUID>>() {})
+ .loader(ByIncludeLoader.class);
bind(GroupIncludeCacheImpl.class);
bind(GroupIncludeCache.class).to(GroupIncludeCacheImpl.class);
@@ -52,24 +60,31 @@
};
}
- private final Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>> byInclude;
+ private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> byInclude;
@Inject
GroupIncludeCacheImpl(
- @Named(BYINCLUDE_NAME) Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>> byInclude) {
+ @Named(BYINCLUDE_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> byInclude) {
this.byInclude = byInclude;
}
public Collection<AccountGroup.UUID> getByInclude(AccountGroup.UUID groupId) {
- return byInclude.get(groupId);
+ try {
+ return byInclude.get(groupId);
+ } catch (ExecutionException e) {
+ log.warn("Cannot load included groups", e);
+ return Collections.emptySet();
+ }
}
public void evictInclude(AccountGroup.UUID groupId) {
- byInclude.remove(groupId);
+ if (groupId != null) {
+ byInclude.invalidate(groupId);
+ }
}
static class ByIncludeLoader extends
- EntryCreator<AccountGroup.UUID, Collection<AccountGroup.UUID>> {
+ CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -78,32 +93,28 @@
}
@Override
- public Collection<AccountGroup.UUID> createEntry(final AccountGroup.UUID key) throws Exception {
+ public Set<AccountGroup.UUID> load(AccountGroup.UUID key) throws Exception {
final ReviewDb db = schema.open();
try {
List<AccountGroup> group = db.accountGroups().byUUID(key).toList();
if (group.size() != 1) {
- return Collections.emptyList();
+ return Collections.emptySet();
}
- Set<AccountGroup.Id> ids = new HashSet<AccountGroup.Id>();
- for (AccountGroupInclude agi : db.accountGroupIncludes().byInclude(group.get(0).getId())) {
+ Set<AccountGroup.Id> ids = Sets.newHashSet();
+ for (AccountGroupInclude agi : db.accountGroupIncludes()
+ .byInclude(group.get(0).getId())) {
ids.add(agi.getGroupId());
}
- Set<AccountGroup.UUID> groupArray = new HashSet<AccountGroup.UUID> ();
+ Set<AccountGroup.UUID> groupArray = Sets.newHashSet();
for (AccountGroup g : db.accountGroups().get(ids)) {
groupArray.add(g.getGroupUUID());
}
- return Collections.unmodifiableCollection(groupArray);
+ return ImmutableSet.copyOf(groupArray);
} finally {
db.close();
}
}
-
- @Override
- public Collection<AccountGroup.UUID> missing(final AccountGroup.UUID key) {
- return Collections.emptyList();
- }
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
index 9bb571e..d536c09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
@@ -24,7 +24,6 @@
* the presence of a user in a particular group.
*/
public interface GroupMembership {
-
public static final GroupMembership EMPTY =
new ListGroupMembership(Collections.<AccountGroup.UUID>emptySet());
@@ -45,7 +44,7 @@
* This may not return all groups the {@link #contains(AccountGroup.UUID)}
* would return {@code true} for, but will at least contain all top level
* groups. This restriction stems from the API of some group systems, which
- * make it expensive to enumate the members of a group.
+ * make it expensive to enumerate the members of a group.
*/
Set<AccountGroup.UUID> getKnownGroups();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
similarity index 89%
rename from gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
index 81ff656..d448fff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
@@ -25,11 +25,12 @@
import java.util.Set;
/**
- * Creates a GroupMembership object from materialized collection of groups.
+ * Creates a GroupMembership checker for the internal group system, which
+ * starts with the seed groups and includes all child groups.
*/
-public class MaterializedGroupMembership implements GroupMembership {
+public class IncludingGroupMembership implements GroupMembership {
public interface Factory {
- MaterializedGroupMembership create(Iterable<AccountGroup.UUID> groupIds);
+ IncludingGroupMembership create(Iterable<AccountGroup.UUID> groupIds);
}
private final GroupIncludeCache groupIncludeCache;
@@ -37,7 +38,7 @@
private final Queue<AccountGroup.UUID> groupQueue;
@Inject
- MaterializedGroupMembership(
+ IncludingGroupMembership(
GroupIncludeCache groupIncludeCache,
@Assisted Iterable<AccountGroup.UUID> seedGroups) {
this.groupIncludeCache = groupIncludeCache;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
new file mode 100644
index 0000000..2335ded
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -0,0 +1,95 @@
+// 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.account;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.Collection;
+
+/**
+ * Implementation of GroupBackend for the internal group system.
+ */
+@Singleton
+public class InternalGroupBackend implements GroupBackend {
+ private static final Function<AccountGroup, GroupReference> ACT_GROUP_TO_GROUP_REF =
+ new Function<AccountGroup, GroupReference>() {
+ @Override
+ public GroupReference apply(AccountGroup group) {
+ return GroupReference.forGroup(group);
+ }
+ };
+
+ private final GroupControl.Factory groupControlFactory;
+ private final GroupCache groupCache;
+ private final IncludingGroupMembership.Factory groupMembershipFactory;
+
+
+ @Inject
+ InternalGroupBackend(GroupControl.Factory groupControlFactory,
+ GroupCache groupCache,
+ IncludingGroupMembership.Factory groupMembershipFactory) {
+ this.groupControlFactory = groupControlFactory;
+ this.groupCache = groupCache;
+ this.groupMembershipFactory = groupMembershipFactory;
+ }
+
+ @Override
+ public boolean handles(AccountGroup.UUID uuid) {
+ return uuid.get().startsWith("global:")
+ || uuid.get().matches("[0-9a-f]{40}");
+ }
+
+ @Override
+ public GroupDescription.Internal get(AccountGroup.UUID uuid) {
+ if (!handles(uuid)) {
+ return null;
+ }
+
+ AccountGroup g = groupCache.get(uuid);
+ if (g == null) {
+ return null;
+ }
+ return GroupDescriptions.forAccountGroup(g);
+ }
+
+ @Override
+ public Collection<GroupReference> suggest(final String name) {
+ Iterable<AccountGroup> filtered = Iterables.filter(groupCache.all(),
+ new Predicate<AccountGroup>() {
+ @Override
+ public boolean apply(AccountGroup group) {
+ // startsWithIgnoreCase && isVisible
+ return group.getName().regionMatches(true, 0, name, 0, name.length())
+ && groupControlFactory.controlFor(group).isVisible();
+ }
+ });
+ return Lists.newArrayList(Iterables.transform(filtered, ACT_GROUP_TO_GROUP_REF));
+ }
+
+ @Override
+ public GroupMembership membershipsOf(IdentifiedUser user) {
+ return groupMembershipFactory.create(user.state().getInternalGroups());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java
index 237d381..346f406 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.account;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.AccountGroup;
import java.util.Set;
@@ -47,6 +48,6 @@
@Override
public Set<AccountGroup.UUID> getKnownGroups() {
- return ImmutableSet.copyOf(groups);
+ return Sets.newHashSet(groups);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
index 5f94df2..1ff1a3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
@@ -107,7 +107,10 @@
final AccountGroup group = new AccountGroup(nameKey, groupId, uuid);
group.setVisibleToAll(visibleToAll);
if (ownerGroupId != null) {
- group.setOwnerGroupId(ownerGroupId);
+ AccountGroup ownerGroup = groupCache.get(ownerGroupId);
+ if (ownerGroup != null) {
+ group.setOwnerGroupUUID(ownerGroup.getGroupUUID());
+ }
}
if (groupDescription != null) {
group.setDescription(groupDescription);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
index fc7c0be..e44d46e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -15,11 +15,8 @@
package com.google.gerrit.server.account;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import java.util.Set;
-
public interface Realm {
/** Can the end-user modify this field of their own account? */
public boolean allowsEdit(Account.FieldName field);
@@ -29,9 +26,10 @@
public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who)
throws AccountException;
- public void onCreateAccount(AuthRequest who, Account account);
+ public AuthRequest unlink(ReviewDb db, Account.Id to, AuthRequest who)
+ throws AccountException;
- public GroupMembership groups(AccountState who);
+ public void onCreateAccount(AuthRequest who, Account account);
/**
* Locate an account whose local username is the given account name.
@@ -42,9 +40,4 @@
* user by that email address.
*/
public Account.Id lookup(String accountName);
-
- /**
- * Search for matching external groups.
- */
- public Set<AccountGroup.ExternalNameKey> lookupGroups(String name);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
new file mode 100644
index 0000000..1974961
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -0,0 +1,161 @@
+// 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.account;
+
+import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Universal implementation of the GroupBackend that works with the injected
+ * set of GroupBackends.
+ */
+@Singleton
+public class UniversalGroupBackend implements GroupBackend {
+ private static final Logger log =
+ LoggerFactory.getLogger(UniversalGroupBackend.class);
+
+ private final DynamicSet<GroupBackend> backends;
+
+ @Inject
+ UniversalGroupBackend(DynamicSet<GroupBackend> backends) {
+ this.backends = backends;
+ }
+
+ @Nullable
+ private GroupBackend backend(AccountGroup.UUID uuid) {
+ if (uuid != null) {
+ for (GroupBackend g : backends) {
+ if (g.handles(uuid)) {
+ return g;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean handles(AccountGroup.UUID uuid) {
+ return backend(uuid) != null;
+ }
+
+ @Override
+ public GroupDescription.Basic get(AccountGroup.UUID uuid) {
+ GroupBackend b = backend(uuid);
+ if (b == null) {
+ log.warn("Unknown GroupBackend for UUID: " + uuid);
+ return null;
+ }
+ return b.get(uuid);
+ }
+
+ @Override
+ public Collection<GroupReference> suggest(String name) {
+ Set<GroupReference> groups = Sets.newTreeSet(GROUP_REF_NAME_COMPARATOR);
+ for (GroupBackend g : backends) {
+ groups.addAll(g.suggest(name));
+ }
+ return groups;
+ }
+
+ @Override
+ public GroupMembership membershipsOf(IdentifiedUser user) {
+ return new UniversalGroupMembership(user);
+ }
+
+ private class UniversalGroupMembership implements GroupMembership {
+ private final Map<GroupBackend, GroupMembership> memberships;
+
+ private UniversalGroupMembership(IdentifiedUser user) {
+ ImmutableMap.Builder<GroupBackend, GroupMembership> builder =
+ ImmutableMap.builder();
+ for (GroupBackend g : backends) {
+ builder.put(g, g.membershipsOf(user));
+ }
+ this.memberships = builder.build();
+ }
+
+ @Nullable
+ private GroupMembership membership(AccountGroup.UUID uuid) {
+ if (uuid != null) {
+ for (Map.Entry<GroupBackend, GroupMembership> m : memberships.entrySet()) {
+ if (m.getKey().handles(uuid)) {
+ return m.getValue();
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean contains(AccountGroup.UUID uuid) {
+ GroupMembership m = membership(uuid);
+ if (m == null) {
+ log.warn("Unknown GroupMembership for UUID: " + uuid);
+ return false;
+ }
+ return m.contains(uuid);
+ }
+
+ @Override
+ public boolean containsAnyOf(Iterable<AccountGroup.UUID> uuids) {
+ Multimap<GroupMembership, AccountGroup.UUID> lookups =
+ ArrayListMultimap.create();
+ for (AccountGroup.UUID uuid : uuids) {
+ GroupMembership m = membership(uuid);
+ if (m == null) {
+ log.warn("Unknown GroupMembership for UUID: " + uuid);
+ continue;
+ }
+ lookups.put(m, uuid);
+ }
+ for (Map.Entry<GroupMembership, Collection<AccountGroup.UUID>> entry :
+ lookups.asMap().entrySet()) {
+ if (entry.getKey().containsAnyOf(entry.getValue())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Set<AccountGroup.UUID> getKnownGroups() {
+ Set<AccountGroup.UUID> groups = Sets.newHashSet();
+ for (GroupMembership m : memberships.values()) {
+ groups.addAll(m.getKnownGroups());
+ }
+ return groups;
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
index e81bfc2..2265bc2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -17,7 +17,6 @@
import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.account.AccountException;
-import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
@@ -47,7 +46,8 @@
import javax.net.ssl.SSLSocketFactory;
@Singleton class Helper {
- private final GroupCache groupCache;
+ static final String LDAP_UUID = "ldap:";
+
private final Config config;
private final String server;
private final String username;
@@ -58,8 +58,7 @@
private final String readTimeOutMillis;
@Inject
- Helper(@GerritServerConfig final Config config, final GroupCache groupCache) {
- this.groupCache = groupCache;
+ Helper(@GerritServerConfig final Config config) {
this.config = config;
this.server = LdapRealm.required(config, "server");
this.username = LdapRealm.optional(config, "username");
@@ -195,12 +194,7 @@
final Set<AccountGroup.UUID> actual = new HashSet<AccountGroup.UUID>();
for (String dn : groupDNs) {
- for (AccountGroup group : groupCache
- .get(new AccountGroup.ExternalNameKey(dn))) {
- if (group.getType() == AccountGroup.Type.LDAP) {
- actual.add(group.getGroupUUID());
- }
- }
+ actual.add(new AccountGroup.UUID(LDAP_UUID + dn));
}
if (actual.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
new file mode 100644
index 0000000..5c30e5c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -0,0 +1,227 @@
+// 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.auth.ldap;
+
+import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR;
+import static com.google.gerrit.server.auth.ldap.Helper.LDAP_UUID;
+import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_CACHE;
+import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_EXIST_CACHE;
+
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
+import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.name.Named;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+import javax.naming.InvalidNameException;
+import javax.naming.NamingException;
+import javax.naming.directory.DirContext;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+/**
+ * Implementation of GroupBackend for the LDAP group system.
+ */
+public class LdapGroupBackend implements GroupBackend {
+ private static final Logger log = LoggerFactory.getLogger(LdapGroupBackend.class);
+
+ private static final String LDAP_NAME = "ldap/";
+ private static final String GROUPNAME = "groupname";
+
+ private final Helper helper;
+ private final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache;
+ private final LoadingCache<String, Boolean> existsCache;
+ private final Provider<CurrentUser> userProvider;
+
+ @Inject
+ LdapGroupBackend(
+ Helper helper,
+ @Named(GROUP_CACHE) LoadingCache<String, Set<AccountGroup.UUID>> membershipCache,
+ @Named(GROUP_EXIST_CACHE) LoadingCache<String, Boolean> existsCache,
+ Provider<CurrentUser> userProvider) {
+ this.helper = helper;
+ this.membershipCache = membershipCache;
+ this.existsCache = existsCache;
+ this.userProvider = userProvider;
+ }
+
+ private static boolean isLdapUUID(AccountGroup.UUID uuid) {
+ return uuid.get().startsWith(LDAP_UUID);
+ }
+
+ private static GroupReference groupReference(LdapQuery.Result res)
+ throws NamingException {
+ return new GroupReference(
+ new AccountGroup.UUID(LDAP_UUID + res.getDN()),
+ LDAP_NAME + cnFor(res.getDN()));
+ }
+
+ private static String cnFor(String dn) {
+ try {
+ LdapName name = new LdapName(dn);
+ if (!name.isEmpty()) {
+ String cn = name.get(name.size() - 1);
+ int index = cn.indexOf('=');
+ if (index >= 0) {
+ cn = cn.substring(index + 1);
+ }
+ return cn;
+ }
+ } catch (InvalidNameException e) {
+ log.warn("Cannot parse LDAP dn for cn", e);
+ }
+ return dn;
+ }
+
+ @Override
+ public boolean handles(AccountGroup.UUID uuid) {
+ return isLdapUUID(uuid);
+ }
+
+ @Override
+ public GroupDescription.Basic get(final AccountGroup.UUID uuid) {
+ if (!handles(uuid)) {
+ return null;
+ }
+
+ String groupDn = uuid.get().substring(LDAP_UUID.length());
+ CurrentUser user = userProvider.get();
+ if (!(user instanceof IdentifiedUser)
+ || !membershipsOf((IdentifiedUser) user).contains(uuid)) {
+ try {
+ if (!existsCache.get(groupDn)) {
+ return null;
+ }
+ } catch (ExecutionException e) {
+ log.warn(String.format("Cannot lookup group %s in LDAP", groupDn), e);
+ return null;
+ }
+ }
+
+ final String name = LDAP_NAME + cnFor(groupDn);
+ return new GroupDescription.Basic() {
+ @Override
+ public AccountGroup.UUID getGroupUUID() {
+ return uuid;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean isVisibleToAll() {
+ return false;
+ }
+ };
+ }
+
+ @Override
+ public Collection<GroupReference> suggest(String name) {
+ AccountGroup.UUID uuid = new AccountGroup.UUID(name);
+ if (isLdapUUID(uuid)) {
+ GroupDescription.Basic g = get(uuid);
+ if (g == null) {
+ return Collections.emptySet();
+ }
+ return Collections.singleton(GroupReference.forGroup(g));
+ } else if (name.startsWith(LDAP_NAME)) {
+ return suggestLdap(name.substring(LDAP_NAME.length()));
+ }
+ return Collections.emptySet();
+ }
+
+ @Override
+ public GroupMembership membershipsOf(IdentifiedUser user) {
+ String id = findId(user.state().getExternalIds());
+ if (id == null) {
+ return GroupMembership.EMPTY;
+ }
+
+ try {
+ return new ListGroupMembership(membershipCache.get(id));
+ } catch (ExecutionException e) {
+ log.warn(String.format("Cannot lookup membershipsOf %s in LDAP", id), e);
+ return GroupMembership.EMPTY;
+ }
+ }
+
+ private static String findId(final Collection<AccountExternalId> ids) {
+ for (final AccountExternalId i : ids) {
+ if (i.isScheme(AccountExternalId.SCHEME_GERRIT)) {
+ return i.getSchemeRest();
+ }
+ }
+ return null;
+ }
+
+
+ private Set<GroupReference> suggestLdap(String name) {
+ if (name.isEmpty()) {
+ return Collections.emptySet();
+ }
+
+ Set<GroupReference> out = Sets.newTreeSet(GROUP_REF_NAME_COMPARATOR);
+ try {
+ DirContext ctx = helper.open();
+ try {
+ // Do exact lookups until there are at least 3 characters.
+ name = Rdn.escapeValue(name) + ((name.length() >= 3) ? "*" : "");
+ LdapSchema schema = helper.getSchema(ctx);
+ ParameterizedString filter = ParameterizedString.asis(
+ schema.groupPattern.replace(GROUPNAME, name).toString());
+ Set<String> returnAttrs = Collections.<String>emptySet();
+ Map<String, String> params = Collections.emptyMap();
+ for (String groupBase : schema.groupBases) {
+ LdapQuery query = new LdapQuery(
+ groupBase, schema.groupScope, filter, returnAttrs);
+ for (LdapQuery.Result res : query.query(ctx, params)) {
+ out.add(groupReference(res));
+ }
+ }
+ } finally {
+ try {
+ ctx.close();
+ } catch (NamingException e) {
+ log.warn("Cannot close LDAP query handle", e);
+ }
+ }
+ } catch (NamingException e) {
+ log.warn("Cannot query LDAP for groups matching requested name", e);
+ }
+ return out;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
index 6eb2f54..29533b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
@@ -16,10 +16,12 @@
import static java.util.concurrent.TimeUnit.HOURS;
+import com.google.common.base.Optional;
+import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.Realm;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
import com.google.inject.Scopes;
import com.google.inject.TypeLiteral;
@@ -29,20 +31,31 @@
public class LdapModule extends CacheModule {
static final String USERNAME_CACHE = "ldap_usernames";
static final String GROUP_CACHE = "ldap_groups";
+ static final String GROUP_EXIST_CACHE = "ldap_group_existence";
+
@Override
protected void configure() {
- final TypeLiteral<Cache<String, Set<AccountGroup.UUID>>> groups =
- new TypeLiteral<Cache<String, Set<AccountGroup.UUID>>>() {};
- core(groups, GROUP_CACHE).maxAge(1, HOURS) //
- .populateWith(LdapRealm.MemberLoader.class);
+ cache(GROUP_CACHE,
+ String.class,
+ new TypeLiteral<Set<AccountGroup.UUID>>() {})
+ .expireAfterWrite(1, HOURS)
+ .loader(LdapRealm.MemberLoader.class);
- final TypeLiteral<Cache<String, Account.Id>> usernames =
- new TypeLiteral<Cache<String, Account.Id>>() {};
- core(usernames, USERNAME_CACHE) //
- .populateWith(LdapRealm.UserLoader.class);
+ cache(USERNAME_CACHE,
+ String.class,
+ new TypeLiteral<Optional<Account.Id>>() {})
+ .loader(LdapRealm.UserLoader.class);
+
+ cache(GROUP_EXIST_CACHE,
+ String.class,
+ new TypeLiteral<Boolean>() {})
+ .expireAfterWrite(1, HOURS)
+ .loader(LdapRealm.ExistenceLoader.class);
bind(Realm.class).to(LdapRealm.class).in(Scopes.SINGLETON);
bind(Helper.class);
+
+ DynamicSet.bind(binder(), GroupBackend.class).to(LdapGroupBackend.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index e085d1e..72eb7ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -16,7 +16,10 @@
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
-import com.google.common.collect.Iterables;
+import com.google.common.base.Optional;
+import com.google.common.base.Strings;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -24,20 +27,13 @@
import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountException;
-import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.EmailExpander;
-import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.MaterializedGroupMembership;
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.auth.AuthenticationUnavailableException;
-import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
-import com.google.gerrit.server.cache.Cache;
-import com.google.gerrit.server.cache.EntryCreator;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -48,15 +44,16 @@
import org.slf4j.LoggerFactory;
import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import javax.naming.CompositeName;
+import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
@@ -65,34 +62,30 @@
static final Logger log = LoggerFactory.getLogger(LdapRealm.class);
static final String LDAP = "com.sun.jndi.ldap.LdapCtxFactory";
static final String USERNAME = "username";
- private static final String GROUPNAME = "groupname";
private final Helper helper;
private final AuthConfig authConfig;
private final EmailExpander emailExpander;
- private final Cache<String, Account.Id> usernameCache;
+ private final LoadingCache<String, Optional<Account.Id>> usernameCache;
private final Set<Account.FieldName> readOnlyAccountFields;
private final Config config;
- private final Cache<String, Set<AccountGroup.UUID>> membershipCache;
- private final MaterializedGroupMembership.Factory groupMembershipFactory;
+ private final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache;
@Inject
LdapRealm(
final Helper helper,
final AuthConfig authConfig,
final EmailExpander emailExpander,
- @Named(LdapModule.GROUP_CACHE) final Cache<String, Set<AccountGroup.UUID>> membershipCache,
- @Named(LdapModule.USERNAME_CACHE) final Cache<String, Account.Id> usernameCache,
- @GerritServerConfig final Config config,
- final MaterializedGroupMembership.Factory groupMembershipFactory) {
+ @Named(LdapModule.GROUP_CACHE) final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache,
+ @Named(LdapModule.USERNAME_CACHE) final LoadingCache<String, Optional<Account.Id>> usernameCache,
+ @GerritServerConfig final Config config) {
this.helper = helper;
this.authConfig = authConfig;
this.emailExpander = emailExpander;
this.usernameCache = usernameCache;
this.membershipCache = membershipCache;
this.config = config;
- this.groupMembershipFactory = groupMembershipFactory;
this.readOnlyAccountFields = new HashSet<Account.FieldName>();
@@ -189,6 +182,7 @@
return r.isEmpty() ? null : r;
}
+ @Override
public AuthRequest authenticate(final AuthRequest who)
throws AccountException {
if (config.getBoolean("ldap", "localUsernameToLowerCase", false)) {
@@ -255,66 +249,30 @@
}
@Override
+ public AuthRequest unlink(ReviewDb db, Account.Id from, AuthRequest who) {
+ return who;
+ }
+
+ @Override
public void onCreateAccount(final AuthRequest who, final Account account) {
- usernameCache.put(who.getLocalUser(), account.getId());
+ usernameCache.put(who.getLocalUser(), Optional.of(account.getId()));
}
@Override
- public GroupMembership groups(final AccountState who) {
- return groupMembershipFactory.create(Iterables.concat(
- membershipCache.get(findId(who.getExternalIds())),
- who.getInternalGroups()));
- }
-
- private static String findId(final Collection<AccountExternalId> ids) {
- for (final AccountExternalId i : ids) {
- if (i.isScheme(AccountExternalId.SCHEME_GERRIT)) {
- return i.getSchemeRest();
- }
+ public Account.Id lookup(String accountName) {
+ if (Strings.isNullOrEmpty(accountName)) {
+ return null;
}
- return null;
- }
-
- @Override
- public Account.Id lookup(final String accountName) {
- return usernameCache.get(accountName);
- }
-
- @Override
- public Set<AccountGroup.ExternalNameKey> lookupGroups(String name) {
- final Set<AccountGroup.ExternalNameKey> out;
- final Map<String, String> params = Collections.<String, String> emptyMap();
-
- out = new HashSet<AccountGroup.ExternalNameKey>();
try {
- final DirContext ctx = helper.open();
- try {
- final LdapSchema schema = helper.getSchema(ctx);
- final ParameterizedString filter =
- ParameterizedString.asis(schema.groupPattern
- .replace(GROUPNAME, name).toString());
- for (String groupBase : schema.groupBases) {
- final LdapQuery query =
- new LdapQuery(groupBase, schema.groupScope, filter, Collections
- .<String> emptySet());
- for (LdapQuery.Result res : query.query(ctx, params)) {
- out.add(new AccountGroup.ExternalNameKey(res.getDN()));
- }
- }
- } finally {
- try {
- ctx.close();
- } catch (NamingException e) {
- log.warn("Cannot close LDAP query handle", e);
- }
- }
- } catch (NamingException e) {
- log.warn("Cannot query LDAP for groups matching requested name", e);
+ Optional<Account.Id> id = usernameCache.get(accountName);
+ return id != null ? id.orNull() : null;
+ } catch (ExecutionException e) {
+ log.warn(String.format("Cannot lookup account %s in LDAP", accountName), e);
+ return null;
}
- return out;
}
- static class UserLoader extends EntryCreator<String, Account.Id> {
+ static class UserLoader extends CacheLoader<String, Optional<Account.Id>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -323,25 +281,23 @@
}
@Override
- public Account.Id createEntry(final String username) throws Exception {
+ public Optional<Account.Id> load(String username) throws Exception {
+ final ReviewDb db = schema.open();
try {
- final ReviewDb db = schema.open();
- try {
- final AccountExternalId extId =
- db.accountExternalIds().get(
- new AccountExternalId.Key(SCHEME_GERRIT, username));
- return extId != null ? extId.getAccountId() : null;
- } finally {
- db.close();
+ final AccountExternalId extId =
+ db.accountExternalIds().get(
+ new AccountExternalId.Key(SCHEME_GERRIT, username));
+ if (extId != null) {
+ return Optional.of(extId.getAccountId());
}
- } catch (OrmException e) {
- log.warn("Cannot query for username in database", e);
- return null;
+ return Optional.absent();
+ } finally {
+ db.close();
}
}
}
- static class MemberLoader extends EntryCreator<String, Set<AccountGroup.UUID>> {
+ static class MemberLoader extends CacheLoader<String, Set<AccountGroup.UUID>> {
private final Helper helper;
@Inject
@@ -350,8 +306,7 @@
}
@Override
- public Set<AccountGroup.UUID> createEntry(final String username)
- throws Exception {
+ public Set<AccountGroup.UUID> load(String username) throws Exception {
final DirContext ctx = helper.open();
try {
return helper.queryForGroups(ctx, username, null);
@@ -363,10 +318,34 @@
}
}
}
+ }
+
+ static class ExistenceLoader extends CacheLoader<String, Boolean> {
+ private final Helper helper;
+
+ @Inject
+ ExistenceLoader(final Helper helper) {
+ this.helper = helper;
+ }
@Override
- public Set<AccountGroup.UUID> missing(final String key) {
- return Collections.emptySet();
+ public Boolean load(final String groupDn) throws Exception {
+ final DirContext ctx = helper.open();
+ try {
+ Name compositeGroupName = new CompositeName().add(groupDn);
+ try {
+ ctx.getAttributes(compositeGroupName);
+ return true;
+ } catch (NamingException e) {
+ return false;
+ }
+ } finally {
+ try {
+ ctx.close();
+ } catch (NamingException e) {
+ log.warn("Cannot close LDAP query handle", e);
+ }
+ }
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java
deleted file mode 100644
index 7892ea1..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/Cache.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2009 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.cache;
-
-/**
- * A fast in-memory and/or on-disk based cache.
- *
- * @type <K> type of key used to lookup entries in the cache.
- * @type <V> type of value stored within each cache entry.
- */
-public interface Cache<K, V> {
- /** Get the element from the cache, or null if not stored in the cache. */
- public V get(K key);
-
- /** Put one element into the cache, replacing any existing value. */
- public void put(K key, V value);
-
- /** Remove any existing value from the cache, no-op if not present. */
- public void remove(K key);
-
- /** Remove all cached items. */
- public void removeAll();
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java
new file mode 100644
index 0000000..625bd14
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheBinding.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2009 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.cache;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.Weigher;
+import com.google.inject.TypeLiteral;
+
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nullable;
+
+/** Configure a cache declared within a {@link CacheModule} instance. */
+public interface CacheBinding<K, V> {
+ /** Set the total size of the cache. */
+ CacheBinding<K, V> maximumWeight(long weight);
+
+ /** Set the time an element lives before being expired. */
+ CacheBinding<K, V> expireAfterWrite(long duration, TimeUnit durationUnits);
+
+ /** Populate the cache with items from the CacheLoader. */
+ CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> clazz);
+
+ /** Algorithm to weigh an object with a method other than the unit weight 1. */
+ CacheBinding<K, V> weigher(Class<? extends Weigher<K, V>> clazz);
+
+ String name();
+ TypeLiteral<K> keyType();
+ TypeLiteral<V> valueType();
+ long maximumWeight();
+ @Nullable Long expireAfterWrite(TimeUnit unit);
+ @Nullable Weigher<K, V> weigher();
+ @Nullable CacheLoader<K, V> loader();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
index 7fb3b3b..c1e92da 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheModule.java
@@ -14,33 +14,41 @@
package com.google.gerrit.server.cache;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.cache.Weigher;
+import com.google.gerrit.extensions.annotations.Exports;
import com.google.inject.AbstractModule;
import com.google.inject.Key;
import com.google.inject.Provider;
import com.google.inject.Scopes;
import com.google.inject.TypeLiteral;
-import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.name.Names;
+import com.google.inject.util.Types;
import java.io.Serializable;
+import java.lang.reflect.Type;
/**
* Miniature DSL to support binding {@link Cache} instances in Guice.
*/
public abstract class CacheModule extends AbstractModule {
+ private static final TypeLiteral<Cache<?, ?>> ANY_CACHE =
+ new TypeLiteral<Cache<?, ?>>() {};
+
/**
- * Declare an unnamed in-memory cache.
+ * Declare a named in-memory cache.
*
* @param <K> type of key used to lookup entries.
* @param <V> type of value stored by the cache.
- * @param type type literal for the cache, this literal will be used to match
- * injection sites.
- * @return binding to describe the cache. Caller must set at least the name on
- * the returned binding.
+ * @return binding to describe the cache.
*/
- protected <K, V> UnnamedCacheBinding<K, V> core(
- final TypeLiteral<Cache<K, V>> type) {
- return core(Key.get(type));
+ protected <K, V> CacheBinding<K, V> cache(
+ String name,
+ Class<K> keyType,
+ Class<V> valType) {
+ return cache(name, TypeLiteral.get(keyType), TypeLiteral.get(valType));
}
/**
@@ -48,74 +56,127 @@
*
* @param <K> type of key used to lookup entries.
* @param <V> type of value stored by the cache.
- * @param type type literal for the cache, this literal will be used to match
- * injection sites. Injection sites are matched by this type literal
- * and with {@code @Named} annotations.
* @return binding to describe the cache.
*/
- protected <K, V> NamedCacheBinding<K, V> core(
- final TypeLiteral<Cache<K, V>> type, final String name) {
- return core(Key.get(type, Names.named(name))).name(name);
- }
-
- private <K, V> UnnamedCacheBinding<K, V> core(final Key<Cache<K, V>> key) {
- final boolean disk = false;
- final CacheProvider<K, V> b = new CacheProvider<K, V>(disk, this);
- bind(key).toProvider(b).in(Scopes.SINGLETON);
- return b;
+ protected <K, V> CacheBinding<K, V> cache(
+ String name,
+ Class<K> keyType,
+ TypeLiteral<V> valType) {
+ return cache(name, TypeLiteral.get(keyType), valType);
}
/**
- * Declare an unnamed in-memory/on-disk cache.
+ * Declare a named in-memory cache.
*
- * @param <K> type of key used to find entries, must be {@link Serializable}.
- * @param <V> type of value stored by the cache, must be {@link Serializable}.
- * @param type type literal for the cache, this literal will be used to match
- * injection sites. Injection sites are matched by this type literal
- * and with {@code @Named} annotations.
- * @return binding to describe the cache. Caller must set at least the name on
- * the returned binding.
+ * @param <K> type of key used to lookup entries.
+ * @param <V> type of value stored by the cache.
+ * @return binding to describe the cache.
*/
- protected <K extends Serializable, V extends Serializable> UnnamedCacheBinding<K, V> disk(
- final TypeLiteral<Cache<K, V>> type) {
- return disk(Key.get(type));
+ protected <K, V> CacheBinding<K, V> cache(
+ String name,
+ TypeLiteral<K> keyType,
+ TypeLiteral<V> valType) {
+ Type type = Types.newParameterizedType(
+ Cache.class,
+ keyType.getType(), valType.getType());
+
+ @SuppressWarnings("unchecked")
+ Key<Cache<K, V>> key = (Key<Cache<K, V>>) Key.get(type, Names.named(name));
+
+ CacheProvider<K, V> m =
+ new CacheProvider<K, V>(this, name, keyType, valType);
+ bind(key).toProvider(m).in(Scopes.SINGLETON);
+ bind(ANY_CACHE).annotatedWith(Exports.named(name)).to(key);
+ return m.maximumWeight(1024);
+ }
+
+ <K,V> Provider<CacheLoader<K,V>> bindCacheLoader(
+ CacheProvider<K, V> m,
+ Class<? extends CacheLoader<K,V>> impl) {
+ Type type = Types.newParameterizedType(
+ Cache.class,
+ m.keyType().getType(), m.valueType().getType());
+
+ Type loadingType = Types.newParameterizedType(
+ LoadingCache.class,
+ m.keyType().getType(), m.valueType().getType());
+
+ Type loaderType = Types.newParameterizedType(
+ CacheLoader.class,
+ m.keyType().getType(), m.valueType().getType());
+
+ @SuppressWarnings("unchecked")
+ Key<LoadingCache<K, V>> key =
+ (Key<LoadingCache<K, V>>) Key.get(type, Names.named(m.name));
+
+ @SuppressWarnings("unchecked")
+ Key<LoadingCache<K, V>> loadingKey =
+ (Key<LoadingCache<K, V>>) Key.get(loadingType, Names.named(m.name));
+
+ @SuppressWarnings("unchecked")
+ Key<CacheLoader<K, V>> loaderKey =
+ (Key<CacheLoader<K, V>>) Key.get(loaderType, Names.named(m.name));
+
+ bind(loaderKey).to(impl).in(Scopes.SINGLETON);
+ bind(loadingKey).to(key);
+ return getProvider(loaderKey);
+ }
+
+ <K,V> Provider<Weigher<K,V>> bindWeigher(
+ CacheProvider<K, V> m,
+ Class<? extends Weigher<K,V>> impl) {
+ Type weigherType = Types.newParameterizedType(
+ Weigher.class,
+ m.keyType().getType(), m.valueType().getType());
+
+ @SuppressWarnings("unchecked")
+ Key<Weigher<K, V>> key =
+ (Key<Weigher<K, V>>) Key.get(weigherType, Names.named(m.name));
+
+ bind(key).to(impl).in(Scopes.SINGLETON);
+ return getProvider(key);
}
/**
* Declare a named in-memory/on-disk cache.
*
- * @param <K> type of key used to find entries, must be {@link Serializable}.
- * @param <V> type of value stored by the cache, must be {@link Serializable}.
- * @param type type literal for the cache, this literal will be used to match
- * injection sites. Injection sites are matched by this type literal
- * and with {@code @Named} annotations.
+ * @param <K> type of key used to lookup entries.
+ * @param <V> type of value stored by the cache.
* @return binding to describe the cache.
*/
- protected <K extends Serializable, V extends Serializable> NamedCacheBinding<K, V> disk(
- final TypeLiteral<Cache<K, V>> type, final String name) {
- return disk(Key.get(type, Names.named(name))).name(name);
+ protected <K extends Serializable, V extends Serializable> CacheBinding<K, V> persist(
+ String name,
+ Class<K> keyType,
+ Class<V> valType) {
+ return persist(name, TypeLiteral.get(keyType), TypeLiteral.get(valType));
}
- private <K, V> UnnamedCacheBinding<K, V> disk(final Key<Cache<K, V>> key) {
- final boolean disk = true;
- final CacheProvider<K, V> b = new CacheProvider<K, V>(disk, this);
- bind(key).toProvider(b).in(Scopes.SINGLETON);
- return b;
+ /**
+ * Declare a named in-memory/on-disk cache.
+ *
+ * @param <K> type of key used to lookup entries.
+ * @param <V> type of value stored by the cache.
+ * @return binding to describe the cache.
+ */
+ protected <K extends Serializable, V extends Serializable> CacheBinding<K, V> persist(
+ String name,
+ Class<K> keyType,
+ TypeLiteral<V> valType) {
+ return persist(name, TypeLiteral.get(keyType), valType);
}
- <K, V> Provider<EntryCreator<K, V>> getEntryCreator(CacheProvider<K, V> cp,
- Class<? extends EntryCreator<K, V>> type) {
- Key<EntryCreator<K, V>> key = newKey();
- bind(key).to(type).in(Scopes.SINGLETON);
- return getProvider(key);
- }
-
- @SuppressWarnings("unchecked")
- private static <K, V> Key<EntryCreator<K, V>> newKey() {
- return (Key<EntryCreator<K, V>>) newKeyImpl();
- }
-
- private static Key<?> newKeyImpl() {
- return Key.get(EntryCreator.class, UniqueAnnotations.create());
+ /**
+ * Declare a named in-memory/on-disk cache.
+ *
+ * @param <K> type of key used to lookup entries.
+ * @param <V> type of value stored by the cache.
+ * @return binding to describe the cache.
+ */
+ protected <K extends Serializable, V extends Serializable> CacheBinding<K, V> persist(
+ String name,
+ TypeLiteral<K> keyType,
+ TypeLiteral<V> valType) {
+ return ((CacheProvider<K, V>) cache(name, keyType, valType))
+ .persist(true);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
index 1fa047b..1b8eea5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheProvider.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.
@@ -14,130 +14,156 @@
package com.google.gerrit.server.cache;
-import static com.google.gerrit.server.cache.EvictionPolicy.LFU;
-import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.SECONDS;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.Weigher;
+import com.google.gerrit.extensions.annotations.PluginName;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
+import com.google.inject.TypeLiteral;
import java.util.concurrent.TimeUnit;
-public final class CacheProvider<K, V> implements Provider<Cache<K, V>>,
- NamedCacheBinding<K, V>, UnnamedCacheBinding<K, V> {
+import javax.annotation.Nullable;
+
+class CacheProvider<K, V>
+ implements Provider<Cache<K, V>>,
+ CacheBinding<K, V> {
private final CacheModule module;
- private final boolean disk;
- private int memoryLimit;
- private int diskLimit;
- private long maxAge;
- private EvictionPolicy evictionPolicy;
- private String cacheName;
- private ProxyCache<K, V> cache;
- private Provider<EntryCreator<K, V>> entryCreator;
+ final String name;
+ private final TypeLiteral<K> keyType;
+ private final TypeLiteral<V> valType;
+ private boolean persist;
+ private long maximumWeight;
+ private Long expireAfterWrite;
+ private Provider<CacheLoader<K, V>> loader;
+ private Provider<Weigher<K, V>> weigher;
- CacheProvider(final boolean disk, CacheModule module) {
- this.disk = disk;
+ private String plugin;
+ private MemoryCacheFactory memoryCacheFactory;
+ private PersistentCacheFactory persistentCacheFactory;
+ private boolean frozen;
+
+ CacheProvider(CacheModule module,
+ String name,
+ TypeLiteral<K> keyType,
+ TypeLiteral<V> valType) {
this.module = module;
+ this.name = name;
+ this.keyType = keyType;
+ this.valType = valType;
+ }
- memoryLimit(1024);
- maxAge(90, DAYS);
- evictionPolicy(LFU);
-
- if (disk) {
- diskLimit(16384);
- }
+ @Inject(optional = true)
+ void setPluginName(@PluginName String pluginName) {
+ this.plugin = pluginName;
}
@Inject
- void setCachePool(final CachePool pool) {
- this.cache = pool.register(this);
+ void setMemoryCacheFactory(MemoryCacheFactory factory) {
+ this.memoryCacheFactory = factory;
}
- public void bind(Cache<K, V> impl) {
- if (cache == null) {
- throw new ProvisionException("Cache was never registered");
- }
- cache.bind(impl);
+ @Inject(optional = true)
+ void setPersistentCacheFactory(@Nullable PersistentCacheFactory factory) {
+ this.persistentCacheFactory = factory;
}
- public EntryCreator<K, V> getEntryCreator() {
- return entryCreator != null ? entryCreator.get() : null;
- }
-
- public String getName() {
- if (cacheName == null) {
- throw new ProvisionException("Cache has no name");
- }
- return cacheName;
- }
-
- public boolean disk() {
- return disk;
- }
-
- public int memoryLimit() {
- return memoryLimit;
- }
-
- public int diskLimit() {
- return diskLimit;
- }
-
- public long maxAge() {
- return maxAge;
- }
-
- public EvictionPolicy evictionPolicy() {
- return evictionPolicy;
- }
-
- public NamedCacheBinding<K, V> name(final String name) {
- if (cacheName != null) {
- throw new IllegalStateException("Cache name already set");
- }
- cacheName = name;
- return this;
- }
-
- public NamedCacheBinding<K, V> memoryLimit(final int objects) {
- memoryLimit = objects;
- return this;
- }
-
- public NamedCacheBinding<K, V> diskLimit(final int objects) {
- if (!disk) {
- // TODO This should really be a compile time type error, but I'm
- // too lazy to create the mess of permutations required to setup
- // type safe returns for bindings in our little DSL.
- //
- throw new IllegalStateException("Cache is not disk based");
- }
- diskLimit = objects;
- return this;
- }
-
- public NamedCacheBinding<K, V> maxAge(final long duration, final TimeUnit unit) {
- maxAge = SECONDS.convert(duration, unit);
+ CacheBinding<K, V> persist(boolean p) {
+ Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+ persist = p;
return this;
}
@Override
- public NamedCacheBinding<K, V> evictionPolicy(final EvictionPolicy policy) {
- evictionPolicy = policy;
+ public CacheBinding<K, V> maximumWeight(long weight) {
+ Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+ maximumWeight = weight;
return this;
}
- public NamedCacheBinding<K, V> populateWith(
- Class<? extends EntryCreator<K, V>> creator) {
- entryCreator = module.getEntryCreator(this, creator);
+ @Override
+ public CacheBinding<K, V> expireAfterWrite(long duration, TimeUnit unit) {
+ Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+ expireAfterWrite = SECONDS.convert(duration, unit);
return this;
}
- public Cache<K, V> get() {
- if (cache == null) {
- throw new ProvisionException("Cache \"" + cacheName + "\" not available");
+ @Override
+ public CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> impl) {
+ Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+ loader = module.bindCacheLoader(this, impl);
+ return this;
+ }
+
+ @Override
+ public CacheBinding<K, V> weigher(Class<? extends Weigher<K, V>> impl) {
+ Preconditions.checkState(!frozen, "binding frozen, cannot be modified");
+ weigher = module.bindWeigher(this, impl);
+ return this;
+ }
+
+ @Override
+ public String name() {
+ if (!Strings.isNullOrEmpty(plugin)) {
+ return plugin + "." + name;
}
- return cache;
+ return name;
+ }
+
+ @Override
+ public TypeLiteral<K> keyType() {
+ return keyType;
+ }
+
+ @Override
+ public TypeLiteral<V> valueType() {
+ return valType;
+ }
+
+ @Override
+ public long maximumWeight() {
+ return maximumWeight;
+ }
+
+ @Override
+ @Nullable
+ public Long expireAfterWrite(TimeUnit unit) {
+ return expireAfterWrite != null
+ ? unit.convert(expireAfterWrite, SECONDS)
+ : null;
+ }
+
+ @Override
+ @Nullable
+ public Weigher<K, V> weigher() {
+ return weigher != null ? weigher.get() : null;
+ }
+
+ @Override
+ @Nullable
+ public CacheLoader<K, V> loader() {
+ return loader != null ? loader.get() : null;
+ }
+
+ @Override
+ public Cache<K, V> get() {
+ frozen = true;
+
+ if (loader != null) {
+ CacheLoader<K, V> ldr = loader.get();
+ if (persist && persistentCacheFactory != null) {
+ return persistentCacheFactory.build(this, ldr);
+ }
+ return memoryCacheFactory.build(this, ldr);
+ } else if (persist && persistentCacheFactory != null) {
+ return persistentCacheFactory.build(this);
+ } else {
+ return memoryCacheFactory.build(this);
+ }
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java
deleted file mode 100644
index bafdc49..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2011 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.git;
-
-package com.google.gerrit.server.cache;
-
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * An infinitely sized cache backed by java.util.ConcurrentHashMap.
- * <p>
- * This cache type is only suitable for unit tests, as it has no upper limit on
- * number of items held in the cache. No upper limit can result in memory leaks
- * in production servers.
- */
-public class ConcurrentHashMapCache<K, V> implements Cache<K, V> {
- private final ConcurrentHashMap<K, V> map = new ConcurrentHashMap<K, V>();
-
- @Override
- public V get(K key) {
- return map.get(key);
- }
-
- @Override
- public void put(K key, V value) {
- map.put(key, value);
- }
-
- @Override
- public void remove(K key) {
- map.remove(key);
- }
-
- @Override
- public void removeAll() {
- map.clear();
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/EntryCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/EntryCreator.java
deleted file mode 100644
index af07e08..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/EntryCreator.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2009 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.cache;
-
-/**
- * Creates a cache entry on demand when its not found.
- *
- * @param <K> type of the cache's key.
- * @param <V> type of the cache's value element.
- */
-public abstract class EntryCreator<K, V> {
- /**
- * Invoked on a cache miss, to compute the cache entry.
- *
- * @param key entry whose content needs to be obtained.
- * @return new cache content. The caller will automatically put this object
- * into the cache.
- * @throws Exception the cache content cannot be computed. No entry will be
- * stored in the cache, and {@link #missing(Object)} will be invoked
- * instead. Future requests for the same key will retry this method.
- */
- public abstract V createEntry(K key) throws Exception;
-
- /** Invoked when {@link #createEntry(Object)} fails, by default return null. */
- public V missing(K key) {
- return null;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/MemoryCacheFactory.java
similarity index 61%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/cache/MemoryCacheFactory.java
index 43039e1..6b8b489 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/MemoryCacheFactory.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.
@@ -14,9 +14,14 @@
package com.google.gerrit.server.cache;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
-/** Configure a cache declared within a {@link CacheModule} instance. */
-public interface UnnamedCacheBinding<K, V> {
- /** Set the name of the cache. */
- public NamedCacheBinding<K, V> name(String cacheName);
+public interface MemoryCacheFactory {
+ <K, V> Cache<K, V> build(CacheBinding<K, V> def);
+
+ <K, V> LoadingCache<K, V> build(
+ CacheBinding<K, V> def,
+ CacheLoader<K, V> loader);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java
deleted file mode 100644
index 3394c71..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/NamedCacheBinding.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (C) 2009 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.cache;
-
-import java.util.concurrent.TimeUnit;
-
-/** Configure a cache declared within a {@link CacheModule} instance. */
-public interface NamedCacheBinding<K, V> {
- /** Set the number of objects to cache in memory. */
- public NamedCacheBinding<K, V> memoryLimit(int objects);
-
- /** Set the number of objects to cache in memory. */
- public NamedCacheBinding<K, V> diskLimit(int objects);
-
- /** Set the time an element lives before being expired. */
- public NamedCacheBinding<K, V> maxAge(long duration, TimeUnit durationUnits);
-
- /** Set the eviction policy for elements when the cache is full. */
- public NamedCacheBinding<K, V> evictionPolicy(EvictionPolicy policy);
-
- /** Populate the cache with items from the EntryCreator. */
- public NamedCacheBinding<K, V> populateWith(Class<? extends EntryCreator<K, V>> creator);
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
similarity index 61%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
index 43039e1..983e956 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.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.
@@ -14,9 +14,14 @@
package com.google.gerrit.server.cache;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
-/** Configure a cache declared within a {@link CacheModule} instance. */
-public interface UnnamedCacheBinding<K, V> {
- /** Set the name of the cache. */
- public NamedCacheBinding<K, V> name(String cacheName);
+public interface PersistentCacheFactory {
+ <K, V> Cache<K, V> build(CacheBinding<K, V> def);
+
+ <K, V> LoadingCache<K, V> build(
+ CacheBinding<K, V> def,
+ CacheLoader<K, V> loader);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyCache.java
deleted file mode 100644
index c1b0292..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyCache.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2010 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.cache;
-
-/** Proxy around a cache which has not yet been created. */
-public final class ProxyCache<K, V> implements Cache<K, V> {
- private volatile Cache<K, V> self;
-
- public void bind(Cache<K, V> self) {
- this.self = self;
- }
-
- public V get(K key) {
- return self.get(key);
- }
-
- public void put(K key, V value) {
- self.put(key, value);
- }
-
- public void remove(K key) {
- self.remove(key);
- }
-
- public void removeAll() {
- self.removeAll();
- }
-}
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/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
index 268e118..f466231 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
@@ -20,8 +20,8 @@
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.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
@@ -44,7 +44,7 @@
private final ChangeControl.Factory changeControlFactory;
private final ReviewDb db;
private final GitRepositoryManager gitManager;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final PatchSetInfoFactory patchSetInfoFactory;
private final PatchSet.Id patchSetId;
@@ -52,7 +52,7 @@
@Inject
DeleteDraftPatchSet(ChangeControl.Factory changeControlFactory,
ReviewDb db, GitRepositoryManager gitManager,
- ReplicationQueue replication, PatchSetInfoFactory patchSetInfoFactory,
+ GitReferenceUpdated replication, PatchSetInfoFactory patchSetInfoFactory,
@Assisted final PatchSet.Id patchSetId) {
this.changeControlFactory = changeControlFactory;
this.db = db;
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/changedetail/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java
index abd3582..9cff992 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java
@@ -80,7 +80,7 @@
throw new NoSuchChangeException(changeId);
}
- List<SubmitRecord> submitResult = control.canSubmit(db, patchSetId);
+ List<SubmitRecord> submitResult = control.canSubmit(db, patch);
if (submitResult.isEmpty()) {
throw new IllegalStateException(
"ChangeControl.canSubmit returned empty list");
@@ -113,6 +113,10 @@
errMsg.append("change " + changeId + ": needs " + lbl.label);
break;
+ case MAY:
+ // The MAY label didn't cause the NOT_READY status
+ break;
+
case IMPOSSIBLE:
if (errMsg.length() > 0) errMsg.append("; ");
errMsg.append("change " + changeId + ": needs " + lbl.label
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
index e76249a..6068c50 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -16,27 +16,10 @@
import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
-import com.google.common.base.Function;
-import com.google.common.base.Predicates;
-import com.google.common.collect.Iterables;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
-import com.google.gwtorm.server.SchemaFactory;
-
import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-
import java.lang.reflect.InvocationTargetException;
-import java.text.MessageFormat;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
-import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -303,83 +286,6 @@
}
}
- /**
- * Resolve groups from group names, via the database. Group names not found in
- * the database will be skipped.
- *
- * @param dbfactory database to resolve from.
- * @param groupNames group names to resolve.
- * @param log log for any warnings and errors.
- * @param groupNotFoundWarning formatted message to output to the log for each
- * group name which is not found in the database. <code>{0}</code> will
- * be replaced with the group name.
- * @return the actual groups resolved from the database. If no groups are
- * found, returns an empty {@code Set}, never {@code null}.
- */
- public static Set<AccountGroup.UUID> groupsFor(
- SchemaFactory<ReviewDb> dbfactory, String[] groupNames, Logger log,
- String groupNotFoundWarning) {
- final Set<AccountGroup.UUID> result = new HashSet<AccountGroup.UUID>();
- try {
- final ReviewDb db = dbfactory.open();
- try {
- List<AccountGroupName> groups = db.accountGroupNames().get(
- Iterables.transform(Arrays.asList(groupNames),
- new Function<String, AccountGroup.NameKey>() {
- @Override
- public AccountGroup.NameKey apply(String name) {
- return new AccountGroup.NameKey(name);
- }
- })).toList();
-
- Iterator<AccountGroup> ags = db.accountGroups().get(
- Iterables.transform(Iterables.filter(groups, Predicates.notNull()),
- new Function<AccountGroupName, AccountGroup.Id>() {
- @Override
- public AccountGroup.Id apply(AccountGroupName group) {
- return group.getId();
- }
- })).iterator();
-
- for (int i = 0; i < groupNames.length; i++) {
- if (groups.get(i) == null) {
- log.warn(MessageFormat.format(groupNotFoundWarning, groupNames[i]));
- continue;
- }
- AccountGroup ag = ags.next();
- if (ag == null) {
- log.warn(MessageFormat.format(groupNotFoundWarning, groupNames[i]));
- } else {
- result.add(ag.getGroupUUID());
- }
- }
- } finally {
- db.close();
- }
- } catch (OrmRuntimeException e) {
- log.error("Database error, cannot load groups", e);
- } catch (OrmException e) {
- log.error("Database error, cannot load groups", e);
- }
- return result;
- }
-
- /**
- * Resolve groups from group names, via the database. Group names not found in
- * the database will be skipped.
- *
- * @param dbfactory database to resolve from.
- * @param groupNames group names to resolve.
- * @param log log for any warnings and errors.
- * @return the actual groups resolved from the database. If no groups are
- * found, returns an empty {@code Set}, never {@code null}.
- */
- public static Set<AccountGroup.UUID> groupsFor(
- SchemaFactory<ReviewDb> dbfactory, String[] groupNames, Logger log) {
- return groupsFor(dbfactory, groupNames, log,
- "Group \"{0}\" not in database, skipping.");
- }
-
private static boolean match(final String a, final String... cases) {
for (final String b : cases) {
if (equalsIgnoreCase(a, b)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 99dd54a..9e3aeca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -16,14 +16,20 @@
import static com.google.inject.Scopes.SINGLETON;
+import com.google.common.cache.Cache;
import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.AuthType;
import com.google.gerrit.rules.PrologModule;
import com.google.gerrit.rules.RulesCache;
+import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.FileTypeRegistry;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.MimeUtilFileTypeRegistry;
-import com.google.gerrit.server.ReplicationUser;
import com.google.gerrit.server.account.AccountByEmailCacheImpl;
import com.google.gerrit.server.account.AccountCacheImpl;
import com.google.gerrit.server.account.AccountInfoCacheFactory;
@@ -32,19 +38,22 @@
import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.account.DefaultRealm;
import com.google.gerrit.server.account.EmailExpander;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupCacheImpl;
+import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.account.GroupIncludeCacheImpl;
import com.google.gerrit.server.account.GroupInfoCacheFactory;
-import com.google.gerrit.server.account.MaterializedGroupMembership;
+import com.google.gerrit.server.account.IncludingGroupMembership;
+import com.google.gerrit.server.account.InternalGroupBackend;
import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.account.UniversalGroupBackend;
import com.google.gerrit.server.auth.ldap.LdapModule;
import com.google.gerrit.server.events.EventFactory;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.ChangeMergeQueue;
import com.google.gerrit.server.git.GitModule;
import com.google.gerrit.server.git.MergeQueue;
-import com.google.gerrit.server.git.PushAllProjectsOp;
import com.google.gerrit.server.git.ReloadSubmitQueueOp;
-import com.google.gerrit.server.git.SecureCredentialsProvider;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.mail.FromAddressGenerator;
@@ -62,8 +71,10 @@
import com.google.gerrit.server.project.SectionSortCache;
import com.google.gerrit.server.tools.ToolsCatalog;
import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.server.workflow.FunctionState;
import com.google.inject.Inject;
+import com.google.inject.TypeLiteral;
import org.apache.velocity.runtime.RuntimeInstance;
import org.eclipse.jgit.lib.Config;
@@ -115,26 +126,31 @@
install(new AccessControlModule());
install(new GitModule());
install(new PrologModule());
+ install(ThreadLocalRequestContext.module());
factory(AccountInfoCacheFactory.Factory.class);
factory(CapabilityControl.Factory.class);
factory(GroupInfoCacheFactory.Factory.class);
+ factory(InternalUser.Factory.class);
factory(ProjectNode.Factory.class);
factory(ProjectState.Factory.class);
- factory(MaterializedGroupMembership.Factory.class);
bind(PermissionCollection.Factory.class);
bind(AccountVisibility.class)
.toProvider(AccountVisibilityProvider.class)
.in(SINGLETON);
+ bind(GroupControl.Factory.class).in(SINGLETON);
+ factory(IncludingGroupMembership.Factory.class);
+ bind(InternalGroupBackend.class).in(SINGLETON);
+ bind(GroupBackend.class).to(UniversalGroupBackend.class).in(SINGLETON);
+ DynamicSet.setOf(binder(), GroupBackend.class);
+ DynamicSet.bind(binder(), GroupBackend.class).to(InternalGroupBackend.class);
+
bind(FileTypeRegistry.class).to(MimeUtilFileTypeRegistry.class);
bind(ToolsCatalog.class);
bind(EventFactory.class);
bind(TransferConfig.class);
- factory(SecureCredentialsProvider.Factory.class);
- factory(PushAllProjectsOp.Factory.class);
-
bind(ChangeMergeQueue.class).in(SINGLETON);
bind(MergeQueue.class).to(ChangeMergeQueue.class).in(SINGLETON);
factory(ReloadSubmitQueueOp.Factory.class);
@@ -150,6 +166,12 @@
bind(ChangeControl.GenericFactory.class);
bind(ProjectControl.GenericFactory.class);
factory(FunctionState.Factory.class);
- factory(ReplicationUser.Factory.class);
+
+ bind(GitReferenceUpdated.class);
+ DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
+ DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
+ DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
+
+ bind(AnonymousUser.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index 71bcc6c..226f926 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -17,12 +17,11 @@
import static com.google.inject.Scopes.SINGLETON;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.RequestCleanup;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.account.GroupDetailFactory;
import com.google.gerrit.server.account.GroupMembers;
import com.google.gerrit.server.account.PerformCreateGroup;
@@ -34,6 +33,7 @@
import com.google.gerrit.server.changedetail.RestoreChange;
import com.google.gerrit.server.changedetail.Submit;
import com.google.gerrit.server.git.AsyncReceiveCommits;
+import com.google.gerrit.server.git.BanCommit;
import com.google.gerrit.server.git.CreateCodeReviewNotes;
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MetaDataUpdate;
@@ -73,11 +73,10 @@
bind(AccountResolver.class);
bind(ChangeQueryRewriter.class);
bind(ListProjects.class);
+ bind(ApprovalsUtil.class);
- bind(AnonymousUser.class).in(RequestScoped.class);
bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
bind(ChangeControl.Factory.class).in(SINGLETON);
- bind(GroupControl.Factory.class).in(SINGLETON);
bind(ProjectControl.Factory.class).in(SINGLETON);
bind(AccountControl.Factory.class).in(SINGLETON);
@@ -115,5 +114,6 @@
factory(CreateProject.Factory.class);
factory(Submit.Factory.class);
factory(SuggestParentCandidates.Factory.class);
+ factory(BanCommit.Factory.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
index 9992f18..8b517a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
@@ -15,8 +15,7 @@
package com.google.gerrit.server.config;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.Config;
@@ -25,9 +24,9 @@
public class GitReceivePackGroupsProvider extends GroupSetProvider {
@Inject
- public GitReceivePackGroupsProvider(@GerritServerConfig Config config,
- SchemaFactory<ReviewDb> db) {
- super(config, db, "receive", null, "allowGroup");
+ public GitReceivePackGroupsProvider(GroupBackend gb,
+ @GerritServerConfig Config config) {
+ super(gb, config, "receive", null, "allowGroup");
// If no group was set, default to "registered users"
//
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
index 76d8844..c519902 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
@@ -15,8 +15,7 @@
package com.google.gerrit.server.config;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.Config;
@@ -26,9 +25,9 @@
public class GitUploadPackGroupsProvider extends GroupSetProvider {
@Inject
- public GitUploadPackGroupsProvider(@GerritServerConfig Config config,
- SchemaFactory<ReviewDb> db) {
- super(config, db, "upload", null, "allowGroup");
+ public GitUploadPackGroupsProvider(GroupBackend gb,
+ @GerritServerConfig Config config) {
+ super(gb, config, "upload", null, "allowGroup");
// If no group was set, default to "registered users" and "anonymous"
//
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
index 15711af..5fa243b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -14,12 +14,11 @@
package com.google.gerrit.server.config;
-import static com.google.gerrit.server.config.ConfigUtil.groupsFor;
-import static java.util.Collections.unmodifiableSet;
-
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -37,10 +36,20 @@
protected Set<AccountGroup.UUID> groupIds;
@Inject
- protected GroupSetProvider(@GerritServerConfig Config config,
- SchemaFactory<ReviewDb> db, String section, String subsection, String name) {
+ protected GroupSetProvider(GroupBackend groupBackend,
+ @GerritServerConfig Config config, String section,
+ String subsection, String name) {
String[] groupNames = config.getStringList(section, subsection, name);
- groupIds = unmodifiableSet(groupsFor(db, groupNames, log));
+ ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
+ for (String n : groupNames) {
+ GroupReference g = GroupBackends.findBestSuggestion(groupBackend, n);
+ if (g == null) {
+ log.warn("Group \"{0}\" not in database, skipping.", n);
+ } else {
+ builder.add(g.getUUID());
+ }
+ }
+ groupIds = builder.build();
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
index 26c76c5..bb95dca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
@@ -14,14 +14,11 @@
package com.google.gerrit.server.config;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.git.PushAllProjectsOp;
import com.google.gerrit.server.git.ReloadSubmitQueueOp;
import com.google.inject.Inject;
-import org.eclipse.jgit.lib.Config;
-
import java.util.concurrent.TimeUnit;
/** Configuration for a master node in a cluster of servers. */
@@ -32,26 +29,15 @@
}
static class OnStart implements LifecycleListener {
- private final PushAllProjectsOp.Factory pushAll;
private final ReloadSubmitQueueOp.Factory submit;
- private final boolean replicateOnStartup;
@Inject
- OnStart(final PushAllProjectsOp.Factory pushAll,
- final ReloadSubmitQueueOp.Factory submit,
- final @GerritServerConfig Config cfg) {
- this.pushAll = pushAll;
+ OnStart(final ReloadSubmitQueueOp.Factory submit) {
this.submit = submit;
-
- replicateOnStartup = cfg.getBoolean("gerrit", "replicateOnStartup", true);
}
@Override
public void start() {
- if (replicateOnStartup) {
- pushAll.create(null).start(30, TimeUnit.SECONDS);
- }
-
submit.create().start(15, TimeUnit.SECONDS);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index b279086..6622b0f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -14,8 +14,7 @@
package com.google.gerrit.server.config;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.Config;
@@ -33,8 +32,8 @@
*/
public class ProjectOwnerGroupsProvider extends GroupSetProvider {
@Inject
- public ProjectOwnerGroupsProvider(
- @GerritServerConfig final Config config, final SchemaFactory<ReviewDb> db) {
- super(config, db, "repository", "*", "ownerGroup");
+ public ProjectOwnerGroupsProvider(GroupBackend gb,
+ @GerritServerConfig final Config config) {
+ super(gb, config, "repository", "*", "ownerGroup");
}
}
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..8d76e90 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,10 @@
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 data_dir;
public final File mail_dir;
public final File hooks_dir;
public final File static_dir;
@@ -38,7 +41,6 @@
public final File gerrit_config;
public final File secure_config;
- public final File replication_config;
public final File contact_information_pub;
public final File ssl_keystore;
@@ -62,6 +64,9 @@
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");
+ data_dir = new File(site_path, "data");
logs_dir = new File(site_path, "logs");
mail_dir = new File(etc_dir, "mail");
hooks_dir = new File(site_path, "hooks");
@@ -72,7 +77,6 @@
gerrit_config = new File(etc_dir, "gerrit.config");
secure_config = new File(etc_dir, "secure.config");
- replication_config = new File(etc_dir, "replication.config");
contact_information_pub = new File(etc_dir, "contact_information.pub");
ssl_keystore = new File(etc_dir, "keystore");
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..6bb7ba8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -0,0 +1,145 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.documentation;
+
+import static org.pegdown.Extensions.ALL;
+import static org.pegdown.Extensions.HARDWRAPS;
+
+import com.google.common.base.Strings;
+
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.pegdown.LinkRenderer;
+import org.pegdown.PegDownProcessor;
+import org.pegdown.ToHtmlSerializer;
+import org.pegdown.ast.HeaderNode;
+import org.pegdown.ast.Node;
+import org.pegdown.ast.RootNode;
+import org.pegdown.ast.TextNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class MarkdownFormatter {
+ private static final Logger log =
+ LoggerFactory.getLogger(MarkdownFormatter.class);
+
+ private static final String css;
+
+ static {
+ AtomicBoolean file = new AtomicBoolean();
+ String src;
+ try {
+ src = readPegdownCss(file);
+ } catch (IOException err) {
+ log.warn("Cannot load pegdown.css", err);
+ src = "";
+ }
+ css = file.get() ? null : src;
+ }
+
+ private static String readCSS() {
+ if (css != null) {
+ return css;
+ }
+ try {
+ return readPegdownCss(new AtomicBoolean());
+ } catch (IOException err) {
+ log.warn("Cannot load pegdown.css", err);
+ return "";
+ }
+ }
+
+ public byte[] markdownToDocHtml(String md, String charEnc)
+ throws UnsupportedEncodingException {
+ RootNode root = parseMarkdown(md);
+ String title = findTitle(root);
+
+ StringBuilder html = new StringBuilder();
+ html.append("<html>");
+ html.append("<head>");
+ if (!Strings.isNullOrEmpty(title)) {
+ html.append("<title>").append(title).append("</title>");
+ }
+ html.append("<style type=\"text/css\">\n")
+ .append(readCSS())
+ .append("\n</style>");
+ html.append("</head>");
+ html.append("<body>\n");
+ html.append(new ToHtmlSerializer(new LinkRenderer()).toHtml(root));
+ html.append("\n</body></html>");
+ return html.toString().getBytes(charEnc);
+ }
+
+ public String extractTitleFromMarkdown(byte[] data, String charEnc) {
+ String md = RawParseUtils.decode(Charset.forName(charEnc), data);
+ return findTitle(parseMarkdown(md));
+ }
+
+ private String findTitle(Node root) {
+ if (root instanceof HeaderNode) {
+ HeaderNode h = (HeaderNode) root;
+ if (h.getLevel() == 1
+ && h.getChildren() != null
+ && !h.getChildren().isEmpty()) {
+ StringBuilder b = new StringBuilder();
+ for (Node n : root.getChildren()) {
+ if (n instanceof TextNode) {
+ b.append(((TextNode) n).getText());
+ }
+ }
+ return b.toString();
+ }
+ }
+
+ for (Node n : root.getChildren()) {
+ String title = findTitle(n);
+ if (title != null) {
+ return title;
+ }
+ }
+ return null;
+ }
+
+ private RootNode parseMarkdown(String md) {
+ return new PegDownProcessor(ALL & ~(HARDWRAPS))
+ .parseMarkdown(md.toCharArray());
+ }
+
+ private static String readPegdownCss(AtomicBoolean file)
+ throws IOException {
+ String name = "pegdown.css";
+ URL url = MarkdownFormatter.class.getResource(name);
+ if (url == null) {
+ throw new FileNotFoundException("Resource " + name);
+ }
+ file.set("file".equals(url.getProtocol()));
+ InputStream in = url.openStream();
+ try {
+ TemporaryBuffer.Heap tmp = new TemporaryBuffer.Heap(128 * 1024);
+ tmp.copy(in);
+ return new String(tmp.toByteArray(), "UTF-8");
+ } finally {
+ in.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index 4d34b71..9fa582a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -32,6 +32,7 @@
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
@@ -39,6 +40,8 @@
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.ObjectId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
@@ -48,6 +51,7 @@
@Singleton
public class EventFactory {
+ private static final Logger log = LoggerFactory.getLogger(EventFactory.class);
private final AccountCache accountCache;
private final Provider<String> urlProvider;
private final ApprovalTypes approvalTypes;
@@ -229,16 +233,19 @@
public void addPatchSetFileNames(PatchSetAttribute patchSetAttribute,
Change change, PatchSet patchSet) {
- PatchList patchList = patchListCache.get(change, patchSet);
- for (PatchListEntry patch : patchList.getPatches()) {
- if (patchSetAttribute.files == null) {
- patchSetAttribute.files = new ArrayList<PatchAttribute>();
- }
+ try {
+ PatchList patchList = patchListCache.get(change, patchSet);
+ for (PatchListEntry patch : patchList.getPatches()) {
+ if (patchSetAttribute.files == null) {
+ patchSetAttribute.files = new ArrayList<PatchAttribute>();
+ }
- PatchAttribute p = new PatchAttribute();
- p.file = patch.getNewName();
- p.type = patch.getChangeType();
- patchSetAttribute.files.add(p);
+ PatchAttribute p = new PatchAttribute();
+ p.file = patch.getNewName();
+ p.type = patch.getChangeType();
+ patchSetAttribute.files.add(p);
+ }
+ } catch (PatchListNotAvailableException e) {
}
}
@@ -273,6 +280,20 @@
p.ref = patchSet.getRefName();
p.uploader = asAccountAttribute(patchSet.getUploader());
p.createdOn = patchSet.getCreatedOn().getTime() / 1000L;
+ try {
+ final ReviewDb db = schema.open();
+ try {
+ p.parents = new ArrayList<String>();
+ for (PatchSetAncestor a : db.patchSetAncestors().ancestorsOf(
+ patchSet.getId())) {
+ p.parents.add(a.getAncestorRevision().get());
+ }
+ } finally {
+ db.close();
+ }
+ } catch (OrmException e) {
+ log.error("Cannot load patch set data for " + patchSet.getId(), e);
+ }
return p;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java
index dca4438..f726ce3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java
@@ -17,13 +17,14 @@
import java.util.List;
public class PatchSetAttribute {
- public String number;
- public String revision;
- public String ref;
- public AccountAttribute uploader;
- public Long createdOn;
+ public String number;
+ public String revision;
+ public List<String> parents;
+ public String ref;
+ public AccountAttribute uploader;
+ public Long createdOn;
- public List<ApprovalAttribute> approvals;
- public List<PatchSetCommentAttribute> comments;
- public List<PatchAttribute> files;
+ public List<ApprovalAttribute> approvals;
+ public List<PatchSetCommentAttribute> comments;
+ public List<PatchAttribute> files;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
new file mode 100644
index 0000000..833b611
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.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.server.extensions.events;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+
+import java.util.Collections;
+import java.util.List;
+
+public class GitReferenceUpdated {
+ public static final GitReferenceUpdated DISABLED = new GitReferenceUpdated(
+ Collections.<GitReferenceUpdatedListener> emptyList());
+
+ private final Iterable<GitReferenceUpdatedListener> listeners;
+
+ @Inject
+ GitReferenceUpdated(DynamicSet<GitReferenceUpdatedListener> listeners) {
+ this.listeners = listeners;
+ }
+
+ GitReferenceUpdated(Iterable<GitReferenceUpdatedListener> listeners) {
+ this.listeners = listeners;
+ }
+
+ public void fire(Project.NameKey project, String ref) {
+ Event event = new Event(project, ref);
+ for (GitReferenceUpdatedListener l : listeners) {
+ l.onGitReferenceUpdated(event);
+ }
+ }
+
+ private static class Event implements GitReferenceUpdatedListener.Event {
+ private final String projectName;
+ private final String ref;
+
+ Event(Project.NameKey project, String ref) {
+ this.projectName = project.get();
+ this.ref = ref;
+ }
+
+ @Override
+ public String getProjectName() {
+ return projectName;
+ }
+
+ @Override
+ public List<GitReferenceUpdatedListener.Update> getUpdates() {
+ GitReferenceUpdatedListener.Update update =
+ new GitReferenceUpdatedListener.Update() {
+ public String getRefName() {
+ return ref;
+ }
+ };
+ return ImmutableList.of(update);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/AccountsSection.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/AccountsSection.java
new file mode 100644
index 0000000..7d868bf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/AccountsSection.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2010 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.git;
+
+import com.google.gerrit.common.data.PermissionRule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AccountsSection {
+ protected List<PermissionRule> sameGroupVisibility;
+
+ public List<PermissionRule> getSameGroupVisibility() {
+ if (sameGroupVisibility == null) {
+ sameGroupVisibility = new ArrayList<PermissionRule>();
+ }
+ return sameGroupVisibility;
+ }
+
+ public void setSameGroupVisibility(List<PermissionRule> sameGroupVisibility) {
+ this.sameGroupVisibility = sameGroupVisibility;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
new file mode 100644
index 0000000..4bfee9c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
@@ -0,0 +1,265 @@
+// 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.git;
+
+import static com.google.gerrit.server.git.GitRepositoryManager.REF_REJECT_COMMITS;
+
+import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.notes.NoteMapMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.List;
+import java.util.TimeZone;
+
+public class BanCommit {
+
+ private static final int MAX_LOCK_FAILURE_CALLS = 10;
+ private static final int SLEEP_ON_LOCK_FAILURE_MS = 25;
+
+ public interface Factory {
+ BanCommit create();
+ }
+
+ private final Provider<IdentifiedUser> currentUser;
+ private final GitRepositoryManager repoManager;
+ private final PersonIdent gerritIdent;
+
+ @Inject
+ BanCommit(final Provider<IdentifiedUser> currentUser,
+ final GitRepositoryManager repoManager,
+ @GerritPersonIdent final PersonIdent gerritIdent) {
+ this.currentUser = currentUser;
+ this.repoManager = repoManager;
+ this.gerritIdent = gerritIdent;
+ }
+
+ public BanCommitResult ban(final ProjectControl projectControl,
+ final List<ObjectId> commitsToBan, final String reason)
+ throws PermissionDeniedException, IOException,
+ InterruptedException, MergeException {
+ if (!projectControl.isOwner()) {
+ throw new PermissionDeniedException(
+ "No project owner: not permitted to ban commits");
+ }
+
+ final BanCommitResult result = new BanCommitResult();
+
+ final PersonIdent currentUserIdent = createPersonIdent();
+ final Repository repo =
+ repoManager.openRepository(projectControl.getProject().getNameKey());
+ try {
+ final RevWalk revWalk = new RevWalk(repo);
+ final ObjectInserter inserter = repo.newObjectInserter();
+ try {
+ NoteMap baseNoteMap = null;
+ RevCommit baseCommit = null;
+ final Ref notesBranch = repo.getRef(REF_REJECT_COMMITS);
+ if (notesBranch != null) {
+ baseCommit = revWalk.parseCommit(notesBranch.getObjectId());
+ baseNoteMap = NoteMap.read(revWalk.getObjectReader(), baseCommit);
+ }
+
+ final NoteMap ourNoteMap;
+ if (baseCommit != null) {
+ ourNoteMap = NoteMap.read(repo.newObjectReader(), baseCommit);
+ } else {
+ ourNoteMap = NoteMap.newEmptyMap();
+ }
+
+ for (final ObjectId commitToBan : commitsToBan) {
+ try {
+ revWalk.parseCommit(commitToBan);
+ } catch (MissingObjectException e) {
+ // ignore exception, also not existing commits can be banned
+ } catch (IncorrectObjectTypeException e) {
+ result.notACommit(commitToBan, e.getMessage());
+ continue;
+ }
+
+ final Note note = ourNoteMap.getNote(commitToBan);
+ if (note != null) {
+ result.commitAlreadyBanned(commitToBan);
+ continue;
+ }
+
+ final String noteContent = reason != null ? reason : "";
+ final ObjectId noteContentId =
+ inserter
+ .insert(Constants.OBJ_BLOB, noteContent.getBytes("UTF-8"));
+ ourNoteMap.set(commitToBan, noteContentId);
+ result.commitBanned(commitToBan);
+ }
+
+ if (result.getNewlyBannedCommits().isEmpty()) {
+ return result;
+ }
+
+ final ObjectId ourCommit =
+ commit(ourNoteMap, inserter, currentUserIdent, baseCommit, result,
+ reason);
+
+ updateRef(repo, revWalk, inserter, ourNoteMap, ourCommit, baseNoteMap,
+ baseCommit);
+ } finally {
+ revWalk.release();
+ inserter.release();
+ }
+ } finally {
+ repo.close();
+ }
+
+ return result;
+ }
+
+ private PersonIdent createPersonIdent() {
+ Date now = new Date();
+ TimeZone tz = gerritIdent.getTimeZone();
+ return currentUser.get().newCommitterIdent(now, tz);
+ }
+
+ private static ObjectId commit(final NoteMap noteMap,
+ final ObjectInserter inserter, final PersonIdent personIdent,
+ final ObjectId baseCommit, final BanCommitResult result,
+ final String reason) throws IOException {
+ final String commitMsg =
+ buildCommitMessage(result.getNewlyBannedCommits(), reason);
+ if (baseCommit != null) {
+ return createCommit(noteMap, inserter, personIdent, commitMsg, baseCommit);
+ } else {
+ return createCommit(noteMap, inserter, personIdent, commitMsg);
+ }
+ }
+
+ private static ObjectId createCommit(final NoteMap noteMap,
+ final ObjectInserter inserter, final PersonIdent personIdent,
+ final String message, final ObjectId... parents) throws IOException {
+ final CommitBuilder b = new CommitBuilder();
+ b.setTreeId(noteMap.writeTree(inserter));
+ b.setAuthor(personIdent);
+ b.setCommitter(personIdent);
+ if (parents.length > 0) {
+ b.setParentIds(parents);
+ }
+ b.setMessage(message);
+ final ObjectId commitId = inserter.insert(b);
+ inserter.flush();
+ return commitId;
+ }
+
+ private static String buildCommitMessage(final List<ObjectId> bannedCommits,
+ final String reason) {
+ final StringBuilder commitMsg = new StringBuilder();
+ commitMsg.append("Banning ");
+ commitMsg.append(bannedCommits.size());
+ commitMsg.append(" ");
+ commitMsg.append(bannedCommits.size() == 1 ? "commit" : "commits");
+ commitMsg.append("\n\n");
+ if (reason != null) {
+ commitMsg.append("Reason: ");
+ commitMsg.append(reason);
+ commitMsg.append("\n\n");
+ }
+ commitMsg.append("The following commits are banned:\n");
+ final StringBuilder commitList = new StringBuilder();
+ for (final ObjectId c : bannedCommits) {
+ if (commitList.length() > 0) {
+ commitList.append(",\n");
+ }
+ commitList.append(c.getName());
+ }
+ commitMsg.append(commitList);
+ return commitMsg.toString();
+ }
+
+ public void updateRef(final Repository repo, final RevWalk revWalk,
+ final ObjectInserter inserter, final NoteMap ourNoteMap,
+ final ObjectId oursCommit, final NoteMap baseNoteMap,
+ final ObjectId baseCommit) throws IOException, InterruptedException,
+ MissingObjectException, IncorrectObjectTypeException,
+ CorruptObjectException, MergeException {
+
+ int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
+ RefUpdate refUpdate = createRefUpdate(repo, oursCommit, baseCommit);
+
+ for (;;) {
+ final Result result = refUpdate.update();
+
+ if (result == Result.LOCK_FAILURE) {
+ if (--remainingLockFailureCalls > 0) {
+ Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
+ } else {
+ throw new MergeException("Failed to lock the ref: "
+ + REF_REJECT_COMMITS);
+ }
+
+ } else if (result == Result.REJECTED) {
+ final RevCommit theirsCommit =
+ revWalk.parseCommit(refUpdate.getOldObjectId());
+ final NoteMap theirNoteMap =
+ NoteMap.read(revWalk.getObjectReader(), theirsCommit);
+ final NoteMapMerger merger = new NoteMapMerger(repo);
+ final NoteMap merged =
+ merger.merge(baseNoteMap, ourNoteMap, theirNoteMap);
+ final ObjectId mergeCommit =
+ createCommit(merged, inserter, gerritIdent,
+ "Merged note commits\n", oursCommit, theirsCommit);
+ refUpdate = createRefUpdate(repo, mergeCommit, theirsCommit);
+ remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
+
+ } else if (result == Result.IO_FAILURE) {
+ throw new IOException(
+ "Couldn't create commit reject notes because of IO_FAILURE");
+ } else {
+ break;
+ }
+ }
+ }
+
+ private static RefUpdate createRefUpdate(final Repository repo,
+ final ObjectId newObjectId, final ObjectId expectedOldObjectId)
+ throws IOException {
+ RefUpdate refUpdate = repo.updateRef(REF_REJECT_COMMITS);
+ refUpdate.setNewObjectId(newObjectId);
+ if (expectedOldObjectId == null) {
+ refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
+ } else {
+ refUpdate.setExpectedOldObjectId(expectedOldObjectId);
+ }
+ return refUpdate;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java
new file mode 100644
index 0000000..1b48455
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java
@@ -0,0 +1,54 @@
+// 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.git;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class BanCommitResult {
+
+ private final List<ObjectId> newlyBannedCommits = new LinkedList<ObjectId>();
+ private final List<ObjectId> alreadyBannedCommits = new LinkedList<ObjectId>();
+ private final List<ObjectId> ignoredObjectIds = new LinkedList<ObjectId>();
+
+ public BanCommitResult() {
+ }
+
+ public void commitBanned(final ObjectId commitId) {
+ newlyBannedCommits.add(commitId);
+ }
+
+ public void commitAlreadyBanned(final ObjectId commitId) {
+ alreadyBannedCommits.add(commitId);
+ }
+
+ public void notACommit(final ObjectId id, final String message) {
+ ignoredObjectIds.add(id);
+ }
+
+ public List<ObjectId> getNewlyBannedCommits() {
+ return newlyBannedCommits;
+ }
+
+ public List<ObjectId> getAlreadyBannedCommits() {
+ return alreadyBannedCommits;
+ }
+
+ public List<ObjectId> getIgnoredObjectIds() {
+ return ignoredObjectIds;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
index 86e0740..6d1b155 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
@@ -20,16 +20,17 @@
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.config.GerritRequestModule;
import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.RequestScopePropagator;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
+import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.servlet.RequestScoped;
@@ -43,6 +44,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
@Singleton
@@ -57,6 +59,7 @@
private final WorkQueue workQueue;
private final Provider<MergeOp.Factory> bgFactory;
+ private final PerThreadRequestScope.Scoper threadScoper;
@Inject
ChangeMergeQueue(final WorkQueue wq, Injector parent) {
@@ -68,15 +71,9 @@
bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
bind(RequestScopePropagator.class)
.to(PerThreadRequestScope.Propagator.class);
+ bind(PerThreadRequestScope.Propagator.class);
install(new GerritRequestModule());
- bind(CurrentUser.class).to(IdentifiedUser.class);
- bind(IdentifiedUser.class).toProvider(new Provider<IdentifiedUser>() {
- @Override
- public IdentifiedUser get() {
- throw new OutOfScopeException("No user on merge thread");
- }
- });
bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
new Provider<SocketAddress>() {
@Override
@@ -91,8 +88,26 @@
}
});
}
+
+ @Provides
+ public PerThreadRequestScope.Scoper provideScoper(
+ final PerThreadRequestScope.Propagator propagator) {
+ final RequestContext requestContext = new RequestContext() {
+ @Override
+ public CurrentUser getCurrentUser() {
+ throw new OutOfScopeException("No user on merge thread");
+ }
+ };
+ return new PerThreadRequestScope.Scoper() {
+ @Override
+ public <T> Callable<T> scope(Callable<T> callable) {
+ return propagator.scope(requestContext, callable);
+ }
+ };
+ }
});
bgFactory = child.getProvider(MergeOp.Factory.class);
+ threadScoper = child.getInstance(PerThreadRequestScope.Scoper.class);
}
@Override
@@ -186,19 +201,15 @@
}
}
- private void mergeImpl(Branch.NameKey branch) {
+ private void mergeImpl(final Branch.NameKey branch) {
try {
- PerThreadRequestScope ctx = new PerThreadRequestScope();
- PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
- try {
- try {
+ threadScoper.scope(new Callable<Void>(){
+ @Override
+ public Void call() throws Exception {
bgFactory.get().create(branch).merge();
- } finally {
- ctx.cleanup.run();
+ return null;
}
- } finally {
- PerThreadRequestScope.set(old);
- }
+ }).call();
} catch (Throwable e) {
log.error("Merge attempt for " + branch + " failed", e);
} finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
index a2b0495..1bf157b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
@@ -59,10 +59,11 @@
* @return the cached Repository instance. Caller must call {@code close()}
* when done to decrement the resource handle.
* @throws RepositoryNotFoundException the name does not denote an existing
- * repository, or the name cannot be read as a repository.
+ * repository.
+ * @throws IOException the name cannot be read as a repository.
*/
public abstract Repository openRepository(Project.NameKey name)
- throws RepositoryNotFoundException;
+ throws RepositoryNotFoundException, IOException;
/**
* Create (and open) a repository by name.
@@ -73,9 +74,11 @@
* @throws RepositoryCaseMismatchException the name collides with an existing
* repository name, but only in case of a character within the name.
* @throws RepositoryNotFoundException the name is invalid.
+ * @throws IOException the repository cannot be created.
*/
public abstract Repository createRepository(Project.NameKey name)
- throws RepositoryCaseMismatchException, RepositoryNotFoundException;
+ throws RepositoryCaseMismatchException, RepositoryNotFoundException,
+ IOException;
/** @return set of all known projects, sorted by natural NameKey order. */
public abstract SortedSet<Project.NameKey> list();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 9fa45e1..cfb2d57 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.git;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -23,6 +23,8 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import com.jcraft.jsch.Session;
+
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ConfigConstants;
@@ -34,6 +36,9 @@
import org.eclipse.jgit.storage.file.LockFile;
import org.eclipse.jgit.storage.file.WindowCache;
import org.eclipse.jgit.storage.file.WindowCacheConfig;
+import org.eclipse.jgit.transport.JschConfigSessionFactory;
+import org.eclipse.jgit.transport.OpenSshConfig;
+import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
@@ -82,6 +87,15 @@
@Override
public void start() {
+ // Install our own factory which always runs in batch mode, as we
+ // have no UI available for interactive prompting.
+ SshSessionFactory.setInstance(new JschConfigSessionFactory() {
+ @Override
+ protected void configure(OpenSshConfig.Host hc, Session session) {
+ // Default configuration is batch mode.
+ }
+ });
+
final WindowCacheConfig c = new WindowCacheConfig();
c.fromConfig(cfg);
WindowCache.reconfigure(c);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
index 44becb5..1997c13 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
@@ -15,7 +15,7 @@
package com.google.gerrit.server.git;
/** Indicates the current branch's queue cannot be processed at this time. */
-class MergeException extends Exception {
+public class MergeException extends Exception {
private static final long serialVersionUID = 1L;
MergeException(final String msg) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 4773680..4db9409 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -37,6 +37,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.mail.MergeFailSender;
import com.google.gerrit.server.mail.MergedSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -134,7 +135,7 @@
private final SchemaFactory<ReviewDb> schemaFactory;
private final ProjectCache projectCache;
private final FunctionState.Factory functionState;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final MergedSender.Factory mergedSenderFactory;
private final MergeFailSender.Factory mergeFailSenderFactory;
private final Provider<String> urlProvider;
@@ -170,7 +171,7 @@
@Inject
MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf,
final ProjectCache pc, final FunctionState.Factory fs,
- final ReplicationQueue rq, final MergedSender.Factory msf,
+ final GitReferenceUpdated rq, final MergedSender.Factory msf,
final MergeFailSender.Factory mfsf,
@CanonicalWebUrl @Nullable final Provider<String> cwu,
final ApprovalTypes approvalTypes, final PatchSetInfoFactory psif,
@@ -319,6 +320,9 @@
} catch (RepositoryNotFoundException notGit) {
final String m = "Repository \"" + name.get() + "\" unknown.";
throw new MergeException(m, notGit);
+ } catch (IOException err) {
+ final String m = "Error opening repository \"" + name.get() + '"';
+ throw new MergeException(m, err);
}
rw = new RevWalk(repo) {
@@ -896,6 +900,7 @@
final ObjectId id = commit(m, mergeCommit);
final CodeReviewCommit newCommit = (CodeReviewCommit) rw.parseCommit(id);
+ final Change oldChange = n.change;
n.change =
db.changes().atomicUpdate(n.change.getId(),
@@ -925,6 +930,9 @@
}
});
+ this.submitted.remove(oldChange);
+ this.submitted.add(n.change);
+
if (approvalList != null) {
for (PatchSetApproval a : approvalList) {
db.patchSetApprovals().insert(
@@ -1026,8 +1034,7 @@
ps.getProject().getDescription());
}
- replication.scheduleUpdate(destBranch.getParentKey(), branchUpdate
- .getName());
+ replication.fire(destBranch.getParentKey(), branchUpdate.getName());
Account account = null;
final PatchSetApproval submitter = getSubmitter(db, mergeTip.patchsetId);
@@ -1122,7 +1129,7 @@
} catch (CodeReviewNoteCreationException e) {
log.error(e.getMessage());
}
- replication.scheduleUpdate(destBranch.getParentKey(),
+ replication.fire(destBranch.getParentKey(),
GitRepositoryManager.REFS_NOTES_REVIEW);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
index fbe9d16..86dc19c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -17,6 +17,7 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -25,6 +26,8 @@
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
+import java.io.IOException;
+
/** Helps with the updating of a {@link VersionedMetaData}. */
public class MetaDataUpdate {
public static class User {
@@ -49,7 +52,7 @@
}
public MetaDataUpdate create(Project.NameKey name)
- throws RepositoryNotFoundException {
+ throws RepositoryNotFoundException, IOException {
MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
md.getCommitBuilder().setAuthor(userIdent);
md.getCommitBuilder().setCommitter(serverIdent);
@@ -71,7 +74,7 @@
}
public MetaDataUpdate create(Project.NameKey name)
- throws RepositoryNotFoundException {
+ throws RepositoryNotFoundException, IOException {
MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
md.getCommitBuilder().setAuthor(serverIdent);
md.getCommitBuilder().setCommitter(serverIdent);
@@ -84,13 +87,13 @@
@Assisted Repository db);
}
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final Project.NameKey projectName;
private final Repository db;
private final CommitBuilder commit;
@Inject
- public MetaDataUpdate(ReplicationQueue replication,
+ public MetaDataUpdate(GitReferenceUpdated replication,
@Assisted Project.NameKey projectName, @Assisted Repository db) {
this.replication = replication;
this.projectName = projectName;
@@ -121,8 +124,6 @@
}
void replicate(String ref) {
- if (replication.isEnabled()) {
- replication.scheduleUpdate(projectName, ref);
- }
+ replication.fire(projectName, ref);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java
deleted file mode 100644
index c5ee262..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2011 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.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-
-/** A disabled {@link ReplicationQueue}. */
-public final class NoReplication implements ReplicationQueue {
- @Override
- public boolean isEnabled() {
- return false;
- }
-
- @Override
- public void scheduleUpdate(Project.NameKey project, String ref) {
- }
-
- @Override
- public void scheduleFullSync(Project.NameKey project, String urlMatch) {
- }
-
- @Override
- public void replicateNewProject(Project.NameKey project, String head) {
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
new file mode 100644
index 0000000..ba2833d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
@@ -0,0 +1,104 @@
+// 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.git;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.server.mail.Address;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+public class NotifyConfig implements Comparable<NotifyConfig> {
+ private String name;
+ private EnumSet<NotifyType> types = EnumSet.of(NotifyType.ALL);
+ private String filter;
+
+ private Set<GroupReference> groups = Sets.newHashSet();
+ private Set<Address> addresses = Sets.newHashSet();
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public boolean isNotify(NotifyType type) {
+ return types.contains(type) || types.contains(NotifyType.ALL);
+ }
+
+ public EnumSet<NotifyType> getNotify() {
+ return types;
+ }
+
+ public void setTypes(EnumSet<NotifyType> newTypes) {
+ types = EnumSet.copyOf(newTypes);
+ }
+
+ public String getFilter() {
+ return filter;
+ }
+
+ public void setFilter(String filter) {
+ if ("*".equals(filter)) {
+ this.filter = null;
+ } else {
+ this.filter = Strings.emptyToNull(filter);
+ }
+ }
+
+ public Set<GroupReference> getGroups() {
+ return groups;
+ }
+
+ public Set<Address> getAddresses() {
+ return addresses;
+ }
+
+ public void addEmail(GroupReference group) {
+ groups.add(group);
+ }
+
+ public void addEmail(Address address) {
+ addresses.add(address);
+ }
+
+ @Override
+ public int compareTo(NotifyConfig o) {
+ return name.compareTo(o.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof NotifyConfig) {
+ return compareTo((NotifyConfig) obj) == 0;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "NotifyConfig[" + name + " = " + addresses + " + " + groups + "]";
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
index 057e80d..8aea73a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
@@ -14,50 +14,86 @@
package com.google.gerrit.server.git;
-import com.google.gerrit.server.RequestCleanup;
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
+import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.Scope;
-import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.Callable;
-class PerThreadRequestScope {
- static class Propagator
- extends ThreadLocalRequestScopePropagator<PerThreadRequestScope> {
- Propagator() {
- super(REQUEST, current);
+public class PerThreadRequestScope {
+ public interface Scoper {
+ <T> Callable<T> scope(Callable<T> callable);
+ }
+
+ private static class Context {
+ private final Map<Key<?>, Object> map;
+
+ private Context() {
+ map = Maps.newHashMap();
}
- @Override
- protected PerThreadRequestScope continuingContext(
- PerThreadRequestScope ctx) {
- return new PerThreadRequestScope();
+ private <T> T get(Key<T> key, Provider<T> creator) {
+ @SuppressWarnings("unchecked")
+ T t = (T) map.get(key);
+ if (t == null) {
+ t = creator.get();
+ map.put(key, t);
+ }
+ return t;
}
}
- private static final ThreadLocal<PerThreadRequestScope> current =
- new ThreadLocal<PerThreadRequestScope>();
+ public static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
+ @Inject
+ Propagator(ThreadLocalRequestContext local) {
+ super(REQUEST, current, local);
+ }
- private static PerThreadRequestScope requireContext() {
- final PerThreadRequestScope ctx = current.get();
+ @Override
+ protected Context continuingContext(Context ctx) {
+ return new Context();
+ }
+
+ public <T> Callable<T> scope(RequestContext requestContext, Callable<T> callable) {
+ final Context ctx = new Context();
+ final Callable<T> wrapped = context(requestContext, cleanup(callable));
+ return new Callable<T>() {
+ @Override
+ public T call() throws Exception {
+ Context old = current.get();
+ current.set(ctx);
+ try {
+ return wrapped.call();
+ } finally {
+ current.set(old);
+ }
+ }
+ };
+ }
+ }
+
+ private static final ThreadLocal<Context> current = new ThreadLocal<Context>();
+
+ private static Context requireContext() {
+ final Context ctx = current.get();
if (ctx == null) {
throw new OutOfScopeException("Not in command/request");
}
return ctx;
}
- static PerThreadRequestScope set(PerThreadRequestScope ctx) {
- PerThreadRequestScope old = current.get();
- current.set(ctx);
- return old;
- }
-
- static final Scope REQUEST = new Scope() {
+ public static final Scope REQUEST = new Scope() {
+ @Override
public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
return new Provider<T>() {
+ @Override
public T get() {
return requireContext().get(key, creator);
}
@@ -74,26 +110,4 @@
return "PerThreadRequestScope.REQUEST";
}
};
-
- private static final Key<RequestCleanup> RC_KEY =
- Key.get(RequestCleanup.class);
-
- final RequestCleanup cleanup;
- private final Map<Key<?>, Object> map;
-
- PerThreadRequestScope() {
- cleanup = new RequestCleanup();
- map = new HashMap<Key<?>, Object>();
- map.put(RC_KEY, cleanup);
- }
-
- synchronized <T> T get(Key<T> key, Provider<T> creator) {
- @SuppressWarnings("unchecked")
- T t = (T) map.get(key);
- if (t == null) {
- t = creator.get();
- map.put(key, t);
- }
- return t;
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index d15095b..507f064 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -16,22 +16,32 @@
import static com.google.gerrit.common.data.Permission.isPermission;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.common.data.RefConfigSection;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.State;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.server.mail.Address;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.util.StringUtils;
import java.io.BufferedReader;
import java.io.IOException;
@@ -39,6 +49,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -56,6 +67,20 @@
private static final String KEY_INHERIT_FROM = "inheritFrom";
private static final String KEY_GROUP_PERMISSIONS = "exclusiveGroupPermissions";
+ private static final String ACCOUNTS = "accounts";
+ private static final String KEY_SAME_GROUP_VISIBILITY = "sameGroupVisibility";
+
+ private static final String CONTRIBUTOR_AGREEMENT = "contributor-agreement";
+ private static final String KEY_ACCEPTED = "accepted";
+ private static final String KEY_REQUIRE_CONTACT_INFORMATION = "requireContactInformation";
+ private static final String KEY_AUTO_VERIFY = "autoVerify";
+ private static final String KEY_AGREEMENT_URL = "agreementUrl";
+
+ private static final String NOTIFY = "notify";
+ private static final String KEY_EMAIL = "email";
+ private static final String KEY_FILTER = "filter";
+ private static final String KEY_TYPE = "type";
+
private static final String CAPABILITY = "capability";
private static final String RECEIVE = "receive";
@@ -76,8 +101,11 @@
private Project.NameKey projectName;
private Project project;
+ private AccountsSection accountsSection;
private Map<AccountGroup.UUID, GroupReference> groupsByUUID;
private Map<String, AccessSection> accessSections;
+ private Map<String, ContributorAgreement> contributorAgreements;
+ private Map<String, NotifyConfig> notifySections;
private List<ValidationError> validationErrors;
private ObjectId rulesId;
@@ -103,6 +131,10 @@
return project;
}
+ public AccountsSection getAccountsSection() {
+ return accountsSection;
+ }
+
public AccessSection getAccessSection(String name) {
return getAccessSection(name, false);
}
@@ -136,6 +168,42 @@
accessSections.put(section.getName(), section);
}
+ public ContributorAgreement getContributorAgreement(String name) {
+ return getContributorAgreement(name, false);
+ }
+
+ public ContributorAgreement getContributorAgreement(String name, boolean create) {
+ ContributorAgreement ca = contributorAgreements.get(name);
+ if (ca == null && create) {
+ ca = new ContributorAgreement(name);
+ contributorAgreements.put(name, ca);
+ }
+ return ca;
+ }
+
+ public Collection<ContributorAgreement> getContributorAgreements() {
+ return sort(contributorAgreements.values());
+ }
+
+ public void remove(ContributorAgreement section) {
+ if (section != null) {
+ accessSections.remove(section.getName());
+ }
+ }
+
+ public void replace(ContributorAgreement section) {
+ section.setAutoVerify(resolve(section.getAutoVerify()));
+ for (PermissionRule rule : section.getAccepted()) {
+ rule.setGroup(resolve(rule.getGroup()));
+ }
+
+ contributorAgreements.put(section.getName(), section);
+ }
+
+ public Collection<NotifyConfig> getNotifyConfigs() {
+ return notifySections.values();
+ }
+
public GroupReference resolve(AccountGroup group) {
return resolve(GroupReference.forGroup(group));
}
@@ -167,13 +235,13 @@
/**
* Check all GroupReferences use current group name, repairing stale ones.
*
- * @param groupCache cache to use when looking up group information by UUID.
+ * @param groupBackend cache to use when looking up group information by UUID.
* @return true if one or more group names was stale.
*/
- public boolean updateGroupNames(GroupCache groupCache) {
+ public boolean updateGroupNames(GroupBackend groupBackend) {
boolean dirty = false;
for (GroupReference ref : groupsByUUID.values()) {
- AccountGroup g = groupCache.get(ref.getUUID());
+ GroupDescription.Basic g = groupBackend.get(ref.getUUID());
if (g != null && !g.getName().equals(ref.getName())) {
dirty = true;
ref.setName(g.getName());
@@ -223,6 +291,116 @@
p.setUseContentMerge(getBoolean(rc, SUBMIT, KEY_MERGE_CONTENT, false));
p.setState(getEnum(rc, PROJECT, null, KEY_STATE, defaultStateValue));
+ loadAccountsSection(rc, groupsByName);
+ loadContributorAgreements(rc, groupsByName);
+ loadAccessSections(rc, groupsByName);
+ loadNotifySections(rc, groupsByName);
+ }
+
+ private void loadAccountsSection(
+ Config rc, Map<String, GroupReference> groupsByName) {
+ accountsSection = new AccountsSection();
+ accountsSection.setSameGroupVisibility(loadPermissionRules(
+ rc, ACCOUNTS, null, KEY_SAME_GROUP_VISIBILITY, groupsByName, false));
+ }
+
+ private void loadContributorAgreements(
+ Config rc, Map<String, GroupReference> groupsByName) {
+ contributorAgreements = new HashMap<String, ContributorAgreement>();
+ for (String name : rc.getSubsections(CONTRIBUTOR_AGREEMENT)) {
+ ContributorAgreement ca = getContributorAgreement(name, true);
+ ca.setDescription(rc.getString(CONTRIBUTOR_AGREEMENT, name, KEY_DESCRIPTION));
+ ca.setRequireContactInformation(
+ rc.getBoolean(CONTRIBUTOR_AGREEMENT, name, KEY_REQUIRE_CONTACT_INFORMATION, false));
+ ca.setAgreementUrl(rc.getString(CONTRIBUTOR_AGREEMENT, name, KEY_AGREEMENT_URL));
+ ca.setAccepted(loadPermissionRules(
+ rc, CONTRIBUTOR_AGREEMENT, name, KEY_ACCEPTED, groupsByName, false));
+
+ List<PermissionRule> rules = loadPermissionRules(
+ rc, CONTRIBUTOR_AGREEMENT, name, KEY_AUTO_VERIFY, groupsByName, false);
+ if (rules.isEmpty()) {
+ ca.setAutoVerify(null);
+ } else if (rules.size() > 1) {
+ error(new ValidationError(PROJECT_CONFIG, "Invalid rule in "
+ + CONTRIBUTOR_AGREEMENT
+ + "." + name
+ + "." + KEY_AUTO_VERIFY
+ + ": at most one group may be set"));
+ } else if (rules.get(0).getAction() != Action.ALLOW) {
+ error(new ValidationError(PROJECT_CONFIG, "Invalid rule in "
+ + CONTRIBUTOR_AGREEMENT
+ + "." + name
+ + "." + KEY_AUTO_VERIFY
+ + ": the group must be allowed"));
+ } else {
+ ca.setAutoVerify(rules.get(0).getGroup());
+ }
+ }
+ }
+
+ /**
+ * Parses the [notify] sections out of the configuration file.
+ *
+ * <pre>
+ * [notify "reviewers"]
+ * email = group Reviewers
+ * type = new_changes
+ *
+ * [notify "dev-team"]
+ * email = dev-team@example.com
+ * filter = branch:master
+ *
+ * [notify "qa"]
+ * email = qa@example.com
+ * filter = branch:\"^(maint|stable)-.*\"
+ * type = submitted_changes
+ * </pre>
+ */
+ private void loadNotifySections(
+ Config rc, Map<String, GroupReference> groupsByName) {
+ notifySections = Maps.newHashMap();
+ for (String sectionName : rc.getSubsections(NOTIFY)) {
+ NotifyConfig n = new NotifyConfig();
+ n.setName(sectionName);
+ n.setFilter(rc.getString(NOTIFY, sectionName, KEY_FILTER));
+
+ EnumSet<NotifyType> types = EnumSet.noneOf(NotifyType.class);
+ types.addAll(ConfigUtil.getEnumList(rc,
+ NOTIFY, sectionName, KEY_TYPE,
+ NotifyType.ALL));
+ n.setTypes(types);
+
+ for (String dst : rc.getStringList(NOTIFY, sectionName, KEY_EMAIL)) {
+ if (dst.startsWith("group ")) {
+ String groupName = dst.substring(6).trim();
+ GroupReference ref = groupsByName.get(groupName);
+ if (ref == null) {
+ ref = new GroupReference(null, groupName);
+ groupsByName.put(ref.getName(), ref);
+ }
+ if (ref.getUUID() != null) {
+ n.addEmail(ref);
+ } else {
+ error(new ValidationError(PROJECT_CONFIG,
+ "group \"" + ref.getName() + "\" not in " + GROUP_LIST));
+ }
+ } else if (dst.startsWith("user ")) {
+ error(new ValidationError(PROJECT_CONFIG, dst + " not supported"));
+ } else {
+ try {
+ n.addEmail(Address.parse(dst));
+ } catch (IllegalArgumentException err) {
+ error(new ValidationError(PROJECT_CONFIG,
+ "notify section \"" + sectionName + "\" has invalid email \"" + dst + "\""));
+ }
+ }
+ }
+ notifySections.put(sectionName, n);
+ }
+ }
+
+ private void loadAccessSections(
+ Config rc, Map<String, GroupReference> groupsByName) {
accessSections = new HashMap<String, AccessSection>();
for (String refName : rc.getSubsections(ACCESS)) {
if (RefConfigSection.isValid(refName)) {
@@ -260,6 +438,15 @@
}
}
+ private List<PermissionRule> loadPermissionRules(Config rc, String section,
+ String subsection, String varName,
+ Map<String, GroupReference> groupsByName,
+ boolean useRange) {
+ Permission perm = new Permission(varName);
+ loadPermissionRules(rc, section, subsection, varName, groupsByName, perm, useRange);
+ return perm.getRules();
+ }
+
private void loadPermissionRules(Config rc, String section,
String subsection, String varName,
Map<String, GroupReference> groupsByName, Permission perm,
@@ -349,6 +536,100 @@
set(rc, PROJECT, null, KEY_STATE, p.getState(), null);
Set<AccountGroup.UUID> keepGroups = new HashSet<AccountGroup.UUID>();
+ saveAccountsSection(rc, keepGroups);
+ saveContributorAgreements(rc, keepGroups);
+ saveAccessSections(rc, keepGroups);
+ saveNotifySections(rc, keepGroups);
+ groupsByUUID.keySet().retainAll(keepGroups);
+
+ saveConfig(PROJECT_CONFIG, rc);
+ saveGroupList();
+ }
+
+ private void saveAccountsSection(Config rc, Set<AccountGroup.UUID> keepGroups) {
+ if (accountsSection != null) {
+ rc.setStringList(ACCOUNTS, null, KEY_SAME_GROUP_VISIBILITY,
+ ruleToStringList(accountsSection.getSameGroupVisibility(), keepGroups));
+ }
+ }
+
+ private void saveContributorAgreements(
+ Config rc, Set<AccountGroup.UUID> keepGroups) {
+ for (ContributorAgreement ca : sort(contributorAgreements.values())) {
+ set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_DESCRIPTION, ca.getDescription());
+ set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_REQUIRE_CONTACT_INFORMATION, ca.isRequireContactInformation());
+ set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AGREEMENT_URL, ca.getAgreementUrl());
+
+ if (ca.getAutoVerify() != null) {
+ if (ca.getAutoVerify().getUUID() != null) {
+ keepGroups.add(ca.getAutoVerify().getUUID());
+ }
+ String autoVerify = new PermissionRule(ca.getAutoVerify()).asString(false);
+ set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AUTO_VERIFY, autoVerify);
+ } else {
+ rc.unset(CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AUTO_VERIFY);
+ }
+
+ rc.setStringList(CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_ACCEPTED,
+ ruleToStringList(ca.getAccepted(), keepGroups));
+ }
+ }
+
+ private void saveNotifySections(
+ Config rc, Set<AccountGroup.UUID> keepGroups) {
+ for (NotifyConfig nc : sort(notifySections.values())) {
+ List<String> email = Lists.newArrayList();
+ for (GroupReference gr : nc.getGroups()) {
+ if (gr.getUUID() != null) {
+ keepGroups.add(gr.getUUID());
+ }
+ email.add(new PermissionRule(gr).asString(false));
+ }
+ Collections.sort(email);
+
+ List<String> addrs = Lists.newArrayList();
+ for (Address addr : nc.getAddresses()) {
+ addrs.add(addr.toString());
+ }
+ Collections.sort(addrs);
+ email.addAll(addrs);
+
+ if (email.isEmpty()) {
+ rc.unset(NOTIFY, nc.getName(), KEY_EMAIL);
+ } else {
+ rc.setStringList(NOTIFY, nc.getName(), KEY_EMAIL, email);
+ }
+
+ if (nc.getNotify().equals(EnumSet.of(NotifyType.ALL))) {
+ rc.unset(NOTIFY, nc.getName(), KEY_TYPE);
+ } else {
+ List<String> types = Lists.newArrayListWithCapacity(4);
+ for (NotifyType t : NotifyType.values()) {
+ if (nc.isNotify(t)) {
+ types.add(StringUtils.toLowerCase(t.name()));
+ }
+ }
+ rc.setStringList(NOTIFY, nc.getName(), KEY_TYPE, types);
+ }
+
+ set(rc, NOTIFY, nc.getName(), KEY_FILTER, nc.getFilter());
+ }
+ }
+
+ private List<String> ruleToStringList(
+ List<PermissionRule> list, Set<AccountGroup.UUID> keepGroups) {
+ List<String> rules = new ArrayList<String>();
+ for (PermissionRule rule : sort(list)) {
+ if (rule.getGroup().getUUID() != null) {
+ keepGroups.add(rule.getGroup().getUUID());
+ }
+ rules.add(rule.asString(false));
+ }
+ return rules;
+ }
+
+ private void saveAccessSections(
+ Config rc, Set<AccountGroup.UUID> keepGroups) {
AccessSection capability = accessSections.get(AccessSection.GLOBAL_CAPABILITIES);
if (capability != null) {
Set<String> have = new HashSet<String>();
@@ -425,10 +706,6 @@
rc.unsetSection(ACCESS, name);
}
}
- groupsByUUID.keySet().retainAll(keepGroups);
-
- saveConfig(PROJECT_CONFIG, rc);
- saveGroupList();
}
private void saveGroupList() throws IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java
deleted file mode 100644
index 845d037..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (C) 2009 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.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.concurrent.TimeUnit;
-
-import javax.annotation.Nullable;
-
-public class PushAllProjectsOp extends DefaultQueueOp {
- public interface Factory {
- PushAllProjectsOp create(String urlMatch);
- }
-
- private static final Logger log =
- LoggerFactory.getLogger(PushAllProjectsOp.class);
-
- private final ProjectCache projectCache;
- private final ReplicationQueue replication;
- private final String urlMatch;
-
- @Inject
- public PushAllProjectsOp(final WorkQueue wq, final ProjectCache projectCache,
- final ReplicationQueue rq, @Assisted @Nullable final String urlMatch) {
- super(wq);
- this.projectCache = projectCache;
- this.replication = rq;
- this.urlMatch = urlMatch;
- }
-
- @Override
- public void start(final int delay, final TimeUnit unit) {
- if (replication.isEnabled()) {
- super.start(delay, unit);
- }
- }
-
- public void run() {
- try {
- for (final Project.NameKey nameKey : projectCache.all()) {
- replication.scheduleFullSync(nameKey, urlMatch);
- }
- } catch (RuntimeException e) {
- log.error("Cannot enumerate known projects", e);
- }
- }
-
- @Override
- public String toString() {
- String s = "Replicate All Projects";
- if (urlMatch != null) {
- s = s + " to " + urlMatch;
- }
- return s;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
deleted file mode 100644
index 0868749..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
+++ /dev/null
@@ -1,433 +0,0 @@
-// Copyright (C) 2009 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.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import com.jcraft.jsch.JSchException;
-
-import org.eclipse.jgit.errors.NoRemoteRepositoryException;
-import org.eclipse.jgit.errors.NotSupportedException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.errors.TransportException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.CredentialsProvider;
-import org.eclipse.jgit.transport.FetchConnection;
-import org.eclipse.jgit.transport.PushResult;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.RemoteRefUpdate;
-import org.eclipse.jgit.transport.Transport;
-import org.eclipse.jgit.transport.URIish;
-import org.slf4j.Logger;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * A push to remote operation started by {@link ReplicationQueue}.
- * <p>
- * Instance members are protected by the lock within PushQueue. Callers must
- * take that lock to ensure they are working with a current view of the object.
- */
-class PushOp implements ProjectRunnable {
- interface Factory {
- PushOp create(Project.NameKey d, URIish u);
- }
-
- private static final Logger log = PushReplication.log;
- static final String ALL_REFS = "..all..";
-
- private final GitRepositoryManager repoManager;
- private final SchemaFactory<ReviewDb> schema;
- private final PushReplication.ReplicationConfig pool;
- private final RemoteConfig config;
- private final CredentialsProvider credentialsProvider;
- private final TagCache tagCache;
-
- private final Set<String> delta = new HashSet<String>();
- private final Project.NameKey projectName;
- private final URIish uri;
- private boolean pushAllRefs;
-
- private Repository db;
-
- /**
- * It indicates if the current instance is in fact retrying to push.
- */
- private boolean retrying;
-
- private boolean canceled;
-
- @Inject
- PushOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> s,
- final PushReplication.ReplicationConfig p, final RemoteConfig c,
- final SecureCredentialsProvider.Factory cpFactory,
- final TagCache tc,
- @Assisted final Project.NameKey d, @Assisted final URIish u) {
- repoManager = grm;
- schema = s;
- pool = p;
- config = c;
- credentialsProvider = cpFactory.create(c.getName());
- tagCache = tc;
- projectName = d;
- uri = u;
- }
-
- public boolean isRetrying() {
- return retrying;
- }
-
- public void setToRetry() {
- retrying = true;
- }
-
- public void cancel() {
- canceled = true;
- }
-
- public boolean wasCanceled() {
- return canceled;
- }
-
- URIish getURI() {
- return uri;
- }
-
- void addRef(final String ref) {
- if (ALL_REFS.equals(ref)) {
- delta.clear();
- pushAllRefs = true;
- } else if (!pushAllRefs) {
- delta.add(ref);
- }
- }
-
- public Set<String> getRefs() {
- final Set<String> refs;
-
- if (pushAllRefs) {
- refs = new HashSet<String>(1);
- refs.add(ALL_REFS);
- } else {
- refs = delta;
- }
-
- return refs;
- }
-
- public void addRefs(Set<String> refs) {
- if (!pushAllRefs) {
- for (String ref : refs) {
- addRef(ref);
- }
- }
- }
-
- public void run() {
- PerThreadRequestScope ctx = new PerThreadRequestScope();
- PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
- try {
- runPushOperation();
- } finally {
- PerThreadRequestScope.set(old);
- }
- }
-
- private void runPushOperation() {
- // Lock the queue, and remove ourselves, so we can't be modified once
- // we start replication (instead a new instance, with the same URI, is
- // created and scheduled for a future point in time.)
- //
- pool.notifyStarting(this);
-
- // It should only verify if it was canceled after calling notifyStarting,
- // since the canceled flag would be set locking the queue.
- if (!canceled) {
- try {
- db = repoManager.openRepository(projectName);
- runImpl();
- } catch (RepositoryNotFoundException e) {
- log.error("Cannot replicate " + projectName + "; " + e.getMessage());
-
- } catch (NoRemoteRepositoryException e) {
- log.error("Cannot replicate to " + uri + "; repository not found");
-
- } catch (NotSupportedException e) {
- log.error("Cannot replicate to " + uri, e);
-
- } catch (TransportException e) {
- final Throwable cause = e.getCause();
- if (cause instanceof JSchException
- && cause.getMessage().startsWith("UnknownHostKey:")) {
- log.error("Cannot replicate to " + uri + ": " + cause.getMessage());
- } else {
- log.error("Cannot replicate to " + uri, e);
- }
-
- // The remote push operation should be retried.
- pool.reschedule(this);
- } catch (IOException e) {
- log.error("Cannot replicate to " + uri, e);
-
- } catch (RuntimeException e) {
- log.error("Unexpected error during replication to " + uri, e);
-
- } catch (Error e) {
- log.error("Unexpected error during replication to " + uri, e);
-
- } finally {
- if (db != null) {
- db.close();
- }
- }
- }
- }
-
- @Override
- public String toString() {
- return "push " + uri;
- }
-
- private void runImpl() throws IOException {
- final Transport tn = Transport.open(db, uri);
- final PushResult res;
- try {
- res = pushVia(tn);
- } finally {
- try {
- tn.close();
- } catch (Throwable e2) {
- log.warn("Unexpected error while closing " + uri, e2);
- }
- }
-
- for (final RemoteRefUpdate u : res.getRemoteUpdates()) {
- switch (u.getStatus()) {
- case OK:
- case UP_TO_DATE:
- case NON_EXISTING:
- break;
-
- case NOT_ATTEMPTED:
- case AWAITING_REPORT:
- case REJECTED_NODELETE:
- case REJECTED_NONFASTFORWARD:
- case REJECTED_REMOTE_CHANGED:
- log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
- + ": status " + u.getStatus().name());
- break;
-
- case REJECTED_OTHER_REASON:
- if ("non-fast-forward".equals(u.getMessage())) {
- log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
- + ", remote rejected non-fast-forward push."
- + " Check receive.denyNonFastForwards variable in config file"
- + " of destination repository.");
- } else {
- log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
- + ", reason: " + u.getMessage());
- }
- break;
- }
- }
- }
-
- private PushResult pushVia(final Transport tn) throws IOException,
- NotSupportedException, TransportException {
- tn.applyConfig(config);
- tn.setCredentialsProvider(credentialsProvider);
-
- final List<RemoteRefUpdate> todo = generateUpdates(tn);
- if (todo.isEmpty()) {
- // If we have no commands selected, we have nothing to do.
- // Calling JGit at this point would just redo the work we
- // already did, and come up with the same answer. Instead
- // send back an empty result.
- //
- return new PushResult();
- }
-
- return tn.push(NullProgressMonitor.INSTANCE, todo);
- }
-
- private List<RemoteRefUpdate> generateUpdates(final Transport tn)
- throws IOException {
- final ProjectControl pc;
- try {
- pc = pool.controlFor(projectName);
- } catch (NoSuchProjectException e) {
- return Collections.emptyList();
- }
-
- Map<String, Ref> local = db.getAllRefs();
- if (!pc.allRefsAreVisible()) {
- if (!pushAllRefs) {
- // If we aren't mirroring, reduce the space we need to filter
- // to only the references we will update during this operation.
- //
- Map<String, Ref> n = new HashMap<String, Ref>();
- for (String src : delta) {
- Ref r = local.get(src);
- if (r != null) {
- n.put(src, r);
- }
- }
- local = n;
- }
-
- final ReviewDb meta;
- try {
- meta = schema.open();
- } catch (OrmException e) {
- log.error("Cannot read database to replicate to " + projectName, e);
- return Collections.emptyList();
- }
- try {
- local = new VisibleRefFilter(tagCache, db, pc, meta, true).filter(local, true);
- } finally {
- meta.close();
- }
- }
-
- final boolean noPerms = !pool.isReplicatePermissions();
- final List<RemoteRefUpdate> cmds = new ArrayList<RemoteRefUpdate>();
- if (pushAllRefs) {
- final Map<String, Ref> remote = listRemote(tn);
-
- for (final Ref src : local.values()) {
- if (noPerms && GitRepositoryManager.REF_CONFIG.equals(src.getName())) {
- continue;
- }
-
- final RefSpec spec = matchSrc(src.getName());
- if (spec != null) {
- final Ref dst = remote.get(spec.getDestination());
- if (dst == null || !src.getObjectId().equals(dst.getObjectId())) {
- // Doesn't exist yet, or isn't the same value, request to push.
- //
- send(cmds, spec, src);
- }
- }
- }
-
- if (config.isMirror()) {
- for (final Ref ref : remote.values()) {
- if (!Constants.HEAD.equals(ref.getName())) {
- final RefSpec spec = matchDst(ref.getName());
- if (spec != null && !local.containsKey(spec.getSource())) {
- // No longer on local side, request removal.
- //
- delete(cmds, spec);
- }
- }
- }
- }
-
- } else {
- for (final String src : delta) {
- final RefSpec spec = matchSrc(src);
- if (spec != null) {
- // If the ref still exists locally, send it, otherwise delete it.
- //
- Ref srcRef = local.get(src);
- if (srcRef != null &&
- !(noPerms && GitRepositoryManager.REF_CONFIG.equals(src))) {
- send(cmds, spec, srcRef);
- } else if (config.isMirror()) {
- delete(cmds, spec);
- }
- }
- }
- }
-
- return cmds;
- }
-
- private Map<String, Ref> listRemote(final Transport tn)
- throws NotSupportedException, TransportException {
- final FetchConnection fc = tn.openFetch();
- try {
- return fc.getRefsMap();
- } finally {
- fc.close();
- }
- }
-
- private RefSpec matchSrc(final String ref) {
- for (final RefSpec s : config.getPushRefSpecs()) {
- if (s.matchSource(ref)) {
- return s.expandFromSource(ref);
- }
- }
- return null;
- }
-
- private RefSpec matchDst(final String ref) {
- for (final RefSpec s : config.getPushRefSpecs()) {
- if (s.matchDestination(ref)) {
- return s.expandFromDestination(ref);
- }
- }
- return null;
- }
-
- private void send(final List<RemoteRefUpdate> cmds, final RefSpec spec,
- final Ref src) throws IOException {
- final String dst = spec.getDestination();
- final boolean force = spec.isForceUpdate();
- cmds.add(new RemoteRefUpdate(db, src, dst, force, null, null));
- }
-
- private void delete(final List<RemoteRefUpdate> cmds, final RefSpec spec)
- throws IOException {
- final String dst = spec.getDestination();
- final boolean force = spec.isForceUpdate();
- cmds.add(new RemoteRefUpdate(db, (Ref) null, dst, force, null, null));
- }
-
- @Override
- public NameKey getProjectNameKey() {
- return projectName;
- }
-
- @Override
- public String getRemoteName() {
- return config.getName();
- }
-
- @Override
- public boolean hasCustomizedPrint() {
- return true;
- }
-}
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
deleted file mode 100644
index 5cf5f7a..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
+++ /dev/null
@@ -1,668 +0,0 @@
-// Copyright (C) 2009 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.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.ReplicationUser;
-import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.ListGroupMembership;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.FactoryModule;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.PerRequestProjectControlCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Singleton;
-import com.google.inject.servlet.RequestScoped;
-
-import com.jcraft.jsch.Session;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.storage.file.FileRepository;
-import org.eclipse.jgit.transport.JschConfigSessionFactory;
-import org.eclipse.jgit.transport.OpenSshConfig;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.RemoteSession;
-import org.eclipse.jgit.transport.SshSessionFactory;
-import org.eclipse.jgit.transport.URIish;
-import org.eclipse.jgit.util.FS;
-import org.eclipse.jgit.util.QuotedString;
-import org.eclipse.jgit.util.io.StreamCopyThread;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.URISyntaxException;
-import java.net.URLEncoder;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-
-/** Manages automatic replication to remote repositories. */
-@Singleton
-public class PushReplication implements ReplicationQueue {
- static final Logger log = LoggerFactory.getLogger(PushReplication.class);
-
- public static class Module extends AbstractModule {
- @Override
- protected void configure() {
- bind(ReplicationQueue.class).to(PushReplication.class);
- }
- }
-
- static {
- // Install our own factory which always runs in batch mode, as we
- // have no UI available for interactive prompting.
- //
- SshSessionFactory.setInstance(new JschConfigSessionFactory() {
- @Override
- protected void configure(OpenSshConfig.Host hc, Session session) {
- // Default configuration is batch mode.
- }
- });
- }
-
- private final Injector injector;
- private final WorkQueue workQueue;
- private final List<ReplicationConfig> configs;
- private final SchemaFactory<ReviewDb> database;
- private final ReplicationUser.Factory replicationUserFactory;
- private final GitRepositoryManager gitRepositoryManager;
-
- @Inject
- PushReplication(final Injector i, final WorkQueue wq, final SitePaths site,
- final ReplicationUser.Factory ruf, final SchemaFactory<ReviewDb> db,
- final GitRepositoryManager grm)
- throws ConfigInvalidException, IOException {
- injector = i;
- workQueue = wq;
- database = db;
- replicationUserFactory = ruf;
- gitRepositoryManager = grm;
- configs = allConfigs(site);
- }
-
- @Override
- public boolean isEnabled() {
- return configs.size() > 0;
- }
-
- @Override
- public void scheduleFullSync(final Project.NameKey project,
- final String urlMatch) {
- for (final ReplicationConfig cfg : configs) {
- for (final URIish uri : cfg.getURIs(project, urlMatch)) {
- cfg.schedule(project, PushOp.ALL_REFS, uri);
- }
- }
- }
-
- @Override
- public void scheduleUpdate(final Project.NameKey project, final String ref) {
- for (final ReplicationConfig cfg : configs) {
- if (cfg.wouldPushRef(ref)) {
- for (final URIish uri : cfg.getURIs(project, null)) {
- cfg.schedule(project, ref, uri);
- }
- }
- }
- }
-
- private static String replace(final String pat, final String key,
- final String val) {
- final int n = pat.indexOf("${" + key + "}");
-
- if (n != -1) {
- return pat.substring(0, n) + val + pat.substring(n + 3 + key.length());
- } else {
- return null;
- }
- }
-
- private List<ReplicationConfig> allConfigs(final SitePaths site)
- throws ConfigInvalidException, IOException {
- final FileBasedConfig cfg =
- new FileBasedConfig(site.replication_config, FS.DETECTED);
-
- if (!cfg.getFile().exists()) {
- log.warn("No " + cfg.getFile() + "; not replicating");
- return Collections.emptyList();
- }
- if (cfg.getFile().length() == 0) {
- log.info("Empty " + cfg.getFile() + "; not replicating");
- return Collections.emptyList();
- }
-
- try {
- cfg.load();
- } catch (ConfigInvalidException e) {
- throw new ConfigInvalidException("Config file " + cfg.getFile()
- + " is invalid: " + e.getMessage(), e);
- } catch (IOException e) {
- throw new IOException("Cannot read " + cfg.getFile() + ": "
- + e.getMessage(), e);
- }
-
- final List<ReplicationConfig> r = new ArrayList<ReplicationConfig>();
- for (final RemoteConfig c : allRemotes(cfg)) {
- if (c.getURIs().isEmpty()) {
- continue;
- }
-
- for (final URIish u : c.getURIs()) {
- if (u.getPath() == null || !u.getPath().contains("${name}")) {
- throw new ConfigInvalidException("remote." + c.getName() + ".url"
- + " \"" + u + "\" lacks ${name} placeholder in " + cfg.getFile());
- }
- }
-
- // In case if refspec destination for push is not set then we assume it is
- // equal to source
- for (RefSpec ref : c.getPushRefSpecs()) {
- if (ref.getDestination() == null) {
- ref.setDestination(ref.getSource());
- }
- }
-
-
- if (c.getPushRefSpecs().isEmpty()) {
- RefSpec spec = new RefSpec();
- spec = spec.setSourceDestination("refs/*", "refs/*");
- spec = spec.setForceUpdate(true);
- c.addPushRefSpec(spec);
- }
-
- r.add(new ReplicationConfig(injector, workQueue, c, cfg, database,
- replicationUserFactory, gitRepositoryManager));
- }
- return Collections.unmodifiableList(r);
- }
-
- private List<RemoteConfig> allRemotes(final FileBasedConfig cfg)
- throws ConfigInvalidException {
- List<String> names = new ArrayList<String>(cfg.getSubsections("remote"));
- Collections.sort(names);
-
- final List<RemoteConfig> result = new ArrayList<RemoteConfig>(names.size());
- for (final String name : names) {
- try {
- result.add(new RemoteConfig(cfg, name));
- } catch (URISyntaxException e) {
- throw new ConfigInvalidException("remote " + name
- + " has invalid URL in " + cfg.getFile());
- }
- }
- return result;
- }
-
- @Override
- public void replicateNewProject(Project.NameKey projectName, String head) {
- if (!isEnabled()) {
- return;
- }
-
- for (ReplicationConfig config : configs) {
- List<URIish> uriList = config.getURIs(projectName, "*");
- String[] adminUrls = config.getAdminUrls();
- boolean adminURLUsed = false;
-
- for (String url : adminUrls) {
- URIish adminURI = null;
- try {
- if (url != null && !url.isEmpty()) {
- adminURI = new URIish(url);
- }
- } catch (URISyntaxException e) {
- log.error("The URL '" + url + "' is invalid");
- }
-
- if (adminURI != null) {
- final String replacedPath =
- replace(adminURI.getPath(), "name", projectName.get());
- if (replacedPath != null) {
- adminURI = adminURI.setPath(replacedPath);
- if (usingSSH(adminURI)) {
- replicateProject(adminURI, head);
- adminURLUsed = true;
- } else {
- log.error("The adminURL '" + url
- + "' is non-SSH which is not allowed");
- }
- }
- }
- }
-
- if (!adminURLUsed) {
- for (URIish uri : uriList) {
- replicateProject(uri, head);
- }
- }
- }
- }
-
- private void replicateProject(final URIish replicateURI, final String head) {
- if (!replicateURI.isRemote()) {
- replicateProjectLocally(replicateURI, head);
- } else if (usingSSH(replicateURI)) {
- replicateProjectOverSsh(replicateURI, head);
- } else {
- log.warn("Cannot create new project on remote site since neither the "
- + "connection method is SSH nor the replication target is local: "
- + replicateURI.toString());
- return;
- }
- }
-
- private void replicateProjectLocally(final URIish replicateURI,
- final String head) {
- try {
- final Repository repo = new FileRepository(replicateURI.getPath());
- try {
- repo.create(true /* bare */);
-
- final RefUpdate u = repo.updateRef(Constants.HEAD);
- u.disableRefLog();
- u.link(head);
- } finally {
- repo.close();
- }
- } catch (IOException e) {
- log.error("Failed to replicate project locally: "
- + replicateURI.getPath());
- }
- }
-
- private void replicateProjectOverSsh(final URIish replicateURI,
- final String head) {
- SshSessionFactory sshFactory = SshSessionFactory.getInstance();
- RemoteSession sshSession;
- String projectPath = QuotedString.BOURNE.quote(replicateURI.getPath());
-
- OutputStream errStream = createErrStream();
- String cmd =
- "mkdir -p " + projectPath + "&& cd " + projectPath
- + "&& git init --bare" + "&& git symbolic-ref HEAD "
- + QuotedString.BOURNE.quote(head);
-
- try {
- sshSession = sshFactory.getSession(replicateURI, null, FS.DETECTED, 0);
- Process proc = sshSession.exec(cmd, 0);
- proc.getOutputStream().close();
- StreamCopyThread out = new StreamCopyThread(proc.getInputStream(), errStream);
- StreamCopyThread err = new StreamCopyThread(proc.getErrorStream(), errStream);
- out.start();
- err.start();
- try {
- proc.waitFor();
- out.halt();
- err.halt();
- } catch (InterruptedException interrupted) {
- // Don't wait, drop out immediately.
- }
- sshSession.disconnect();
- } catch (IOException e) {
- log.error("Communication error when trying to replicate to: "
- + replicateURI.toString() + "\n" + "Error reported: "
- + e.getMessage() + "\n" + "Error in communication: "
- + errStream.toString());
- }
- }
-
- private OutputStream createErrStream() {
- return new OutputStream() {
- private StringBuilder all = new StringBuilder();
- private StringBuilder sb = new StringBuilder();
-
- @Override
- public String toString() {
- String r = all.toString();
- while (r.endsWith("\n"))
- r = r.substring(0, r.length() - 1);
- return r;
- }
-
- @Override
- public synchronized void write(final int b) {
- if (b == '\r') {
- return;
- }
-
- sb.append((char) b);
-
- if (b == '\n') {
- all.append(sb);
- sb.setLength(0);
- }
- }
- };
- }
-
- private boolean usingSSH(final URIish uri) {
- final String scheme = uri.getScheme();
- if (!uri.isRemote()) return false;
- if (scheme != null && scheme.toLowerCase().contains("ssh")) return true;
- if (scheme == null && uri.getHost() != null && uri.getPath() != null)
- return true;
- return false;
- }
-
- static class ReplicationConfig {
- private final RemoteConfig remote;
- private final String[] adminUrls;
- private final int delay;
- private final int retryDelay;
- private final WorkQueue.Executor pool;
- private final Map<URIish, PushOp> pending = new HashMap<URIish, PushOp>();
- private final PushOp.Factory opFactory;
- private final ProjectControl.Factory projectControlFactory;
- private final GitRepositoryManager mgr;
- private final boolean replicatePermissions;
-
- ReplicationConfig(final Injector injector, final WorkQueue workQueue,
- final RemoteConfig rc, final Config cfg, SchemaFactory<ReviewDb> db,
- final ReplicationUser.Factory replicationUserFactory,
- final GitRepositoryManager gitRepositoryManager) {
-
- remote = rc;
- delay = Math.max(0, getInt(rc, cfg, "replicationdelay", 15));
- retryDelay = Math.max(0, getInt(rc, cfg, "replicationretry", 1));
-
- final int poolSize = Math.max(0, getInt(rc, cfg, "threads", 1));
- final String poolName = "ReplicateTo-" + rc.getName();
- pool = workQueue.createQueue(poolSize, poolName);
-
- String[] authGroupNames =
- cfg.getStringList("remote", rc.getName(), "authGroup");
- final GroupMembership authGroups;
- if (authGroupNames.length > 0) {
- authGroups = new ListGroupMembership(ConfigUtil.groupsFor(db, authGroupNames, //
- log, "Group \"{0}\" not in database, removing from authGroup"));
- } else {
- authGroups = ReplicationUser.EVERYTHING_VISIBLE;
- }
-
- adminUrls = cfg.getStringList("remote", rc.getName(), "adminUrl");
- replicatePermissions = cfg.getBoolean("remote", rc.getName(),
- "replicatePermissions", true);
- mgr = gitRepositoryManager;
-
- final ReplicationUser remoteUser =
- replicationUserFactory.create(authGroups);
-
- projectControlFactory =
- injector.createChildInjector(new AbstractModule() {
- @Override
- protected void configure() {
- bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
- bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
- bind(CurrentUser.class).toInstance(remoteUser);
- }
- }).getInstance(ProjectControl.Factory.class);
-
- opFactory = injector.createChildInjector(new FactoryModule() {
- @Override
- protected void configure() {
- bind(PushReplication.ReplicationConfig.class).toInstance(ReplicationConfig.this);
- bind(RemoteConfig.class).toInstance(remote);
- factory(PushOp.Factory.class);
- }
- }).getInstance(PushOp.Factory.class);
- }
-
- private int getInt(final RemoteConfig rc, final Config cfg,
- final String name, final int defValue) {
- return cfg.getInt("remote", rc.getName(), name, defValue);
- }
-
- void schedule(final Project.NameKey project, final String ref,
- final URIish uri) {
- PerThreadRequestScope ctx = new PerThreadRequestScope();
- PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
- try {
- try {
- if (!controlFor(project).isVisible()) {
- return;
- }
- } catch (NoSuchProjectException e1) {
- log.error("Internal error: project " + project
- + " not found during replication");
- return;
- }
- } finally {
- PerThreadRequestScope.set(old);
- }
-
- if (!replicatePermissions) {
- PushOp e;
- synchronized (pending) {
- e = pending.get(uri);
- }
- if (e == null) {
- Repository git;
- try {
- git = mgr.openRepository(project);
- } catch (RepositoryNotFoundException err) {
- log.error("Internal error: project " + project
- + " not found during replication", err);
- return;
- }
- try {
- Ref head = git.getRef(Constants.HEAD);
- if (head != null
- && head.isSymbolic()
- && GitRepositoryManager.REF_CONFIG.equals(head.getLeaf().getName())) {
- return;
- }
- } catch (IOException err) {
- log.error("Internal error: cannot check type of project " + project
- + " during replication", err);
- return;
- } finally {
- git.close();
- }
- }
- }
-
- synchronized (pending) {
- PushOp e = pending.get(uri);
- if (e == null) {
- e = opFactory.create(project, uri);
- pool.schedule(e, delay, TimeUnit.SECONDS);
- pending.put(uri, e);
- }
- e.addRef(ref);
- }
- }
-
- /**
- * It schedules again a PushOp instance.
- * <p>
- * It is assumed to be previously scheduled and found a
- * transport exception. It will schedule it as a push
- * operation to be retried after the minutes count
- * determined by class attribute retryDelay.
- * <p>
- * In case the PushOp instance to be scheduled has same
- * URI than one also pending for retry, it adds to the one
- * pending the refs list of the parameter instance.
- * <p>
- * In case the PushOp instance to be scheduled has same
- * URI than one pending, but not pending for retry, it
- * indicates the one pending should be canceled when it
- * starts executing, removes it from pending list, and
- * adds its refs to the parameter instance. The parameter
- * instance is scheduled for retry.
- * <p>
- * Notice all operations to indicate a PushOp should be
- * canceled, or it is retrying, or remove/add it from/to
- * pending Map should be protected by the lock on pending
- * Map class instance attribute.
- *
- * @param pushOp The PushOp instance to be scheduled.
- */
- void reschedule(final PushOp pushOp) {
- // It locks access to pending variable.
- synchronized (pending) {
- URIish uri = pushOp.getURI();
- PushOp pendingPushOp = pending.get(uri);
-
- if (pendingPushOp != null) {
- // There is one PushOp instance already pending to same URI.
-
- if (pendingPushOp.isRetrying()) {
- // The one pending is one already retrying, so it should
- // maintain it and add to it the refs of the one passed
- // as parameter to the method.
-
- // This scenario would happen if a PushOp has started running
- // and then before it failed due transport exception, another
- // one to same URI started. The first one would fail and would
- // be rescheduled, being present in pending list. When the
- // second one fails, it will also be rescheduled and then,
- // here, find out replication to its URI is already pending
- // for retry (blocking).
- pendingPushOp.addRefs(pushOp.getRefs());
-
- } else {
- // The one pending is one that is NOT retrying, it was just
- // scheduled believing no problem would happen. The one pending
- // should be canceled, and this is done by setting its canceled
- // flag, removing it from pending list, and adding its refs to
- // the pushOp instance that should then, later, in this method,
- // be scheduled for retry.
-
- // Notice that the PushOp found pending will start running and,
- // when notifying it is starting (with pending lock protection),
- // it will see it was canceled and then it will do nothing with
- // pending list and it will not execute its run implementation.
-
- pendingPushOp.cancel();
- pending.remove(uri);
-
- pushOp.addRefs(pendingPushOp.getRefs());
- }
- }
-
- if (pendingPushOp == null || !pendingPushOp.isRetrying()) {
- // The PushOp method param instance should be scheduled for retry.
- // Remember when retrying it should be used different delay.
-
- pushOp.setToRetry();
-
- pending.put(uri, pushOp);
- pool.schedule(pushOp, retryDelay, TimeUnit.MINUTES);
- }
- }
- }
-
- ProjectControl controlFor(final Project.NameKey project)
- throws NoSuchProjectException {
- return projectControlFactory.controlFor(project);
- }
-
- void notifyStarting(final PushOp op) {
- synchronized (pending) {
- if (!op.wasCanceled()) {
- pending.remove(op.getURI());
- }
- }
- }
-
- boolean wouldPushRef(final String ref) {
- if (!replicatePermissions && GitRepositoryManager.REF_CONFIG.equals(ref)) {
- return false;
- }
- for (final RefSpec s : remote.getPushRefSpecs()) {
- if (s.matchSource(ref)) {
- return true;
- }
- }
- return false;
- }
-
- boolean isReplicatePermissions() {
- return replicatePermissions;
- }
-
- List<URIish> getURIs(final Project.NameKey project, final String urlMatch) {
- final List<URIish> r = new ArrayList<URIish>(remote.getURIs().size());
- for (URIish uri : remote.getURIs()) {
- if (matches(uri, urlMatch)) {
- String name = project.get();
- if (needsUrlEncoding(uri)) {
- name = encode(name);
- }
- String replacedPath = replace(uri.getPath(), "name", name);
- if (replacedPath != null) {
- uri = uri.setPath(replacedPath);
- r.add(uri);
- }
- }
- }
- return r;
- }
-
- static boolean needsUrlEncoding(URIish uri) {
- return "http".equalsIgnoreCase(uri.getScheme())
- || "https".equalsIgnoreCase(uri.getScheme())
- || "amazon-s3".equalsIgnoreCase(uri.getScheme());
- }
-
- static String encode(String str) {
- try {
- // Some cleanup is required. The '/' character is always encoded as %2F
- // however remote servers will expect it to be not encoded as part of the
- // path used to the repository. Space is incorrectly encoded as '+' for this
- // context. In the path part of a URI space should be %20, but in form data
- // space is '+'. Our cleanup replace fixes these two issues.
- return URLEncoder.encode(str, "UTF-8")
- .replaceAll("%2[fF]", "/")
- .replace("+", "%20");
- } catch (UnsupportedEncodingException e) {
- throw new RuntimeException(e);
- }
- }
-
- String[] getAdminUrls() {
- return this.adminUrls;
- }
-
- private boolean matches(URIish uri, final String urlMatch) {
- if (urlMatch == null || urlMatch.equals("") || urlMatch.equals("*")) {
- return true;
- }
- return uri.toString().contains(urlMatch);
- }
- }
-}
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 37fbdb2..e9a35b8 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
@@ -21,15 +21,14 @@
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+import com.google.common.collect.LinkedListMultimap;
+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;
@@ -47,6 +46,7 @@
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.MultiProgressMonitor.Task;
import com.google.gerrit.server.mail.CreateChangeSender;
import com.google.gerrit.server.mail.MergedSender;
@@ -116,6 +116,34 @@
private static final FooterKey TESTED_BY = new FooterKey("Tested-by");
private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
+ private static final String COMMAND_REJECTION_MESSAGE_FOOTER =
+ "Please read the documentation and contact an administrator\n"
+ + "if you feel the configuration is incorrect";
+
+ private enum Error {
+ CONFIG_UPDATE("You are not allowed to perform this operation.\n"
+ + "Configuration changes can only be pushed by project owners\n"
+ + "who also have 'Push' rights on " + GitRepositoryManager.REF_CONFIG),
+ UPDATE("You are not allowed to perform this operation.\n"
+ + "To push into this reference you need 'Push' rights."),
+ DELETE("You need 'Push' rights with the 'Force Push'\n"
+ + "flag set to delete references."),
+ CODE_REVIEW("You need 'Push' rights to upload code review requests.\n"
+ + "Verify that you are pushing to the right branch."),
+ CREATE("You are not allowed to perform this operation.\n"
+ + "To create new references you need 'Create Reference' rights.");
+
+ private final String value;
+
+ Error(String value) {
+ this.value = value;
+ }
+
+ public String get() {
+ return value;
+ }
+ }
+
interface Factory {
ReceiveCommits create(ProjectControl projectControl, Repository repository);
}
@@ -178,14 +206,14 @@
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;
private final ReplacePatchSetSender.Factory replacePatchSetFactory;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final PatchSetInfoFactory patchSetInfoFactory;
private final ChangeHooks hooks;
+ private final ApprovalsUtil approvalsUtil;
private final GitRepositoryManager repoManager;
private final ProjectCache projectCache;
private final String canonicalWebUrl;
@@ -218,6 +246,7 @@
private final SubmoduleOp.Factory subOpFactory;
private final List<Message> messages = new ArrayList<Message>();
+ private ListMultimap<Error, String> errors = LinkedListMultimap.create();
private Task newProgress;
private Task replaceProgress;
private Task closeProgress;
@@ -225,14 +254,15 @@
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,
final ReplacePatchSetSender.Factory replacePatchSetFactory,
- final ReplicationQueue replication,
+ final GitReferenceUpdated replication,
final PatchSetInfoFactory patchSetInfoFactory,
final ChangeHooks hooks,
+ final ApprovalsUtil approvalsUtil,
final ProjectCache projectCache,
final GitRepositoryManager repoManager,
final TagCache tagCache,
@@ -247,7 +277,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;
@@ -255,6 +284,7 @@
this.replication = replication;
this.patchSetInfoFactory = patchSetInfoFactory;
this.hooks = hooks;
+ this.approvalsUtil = approvalsUtil;
this.projectCache = projectCache;
this.repoManager = repoManager;
this.canonicalWebUrl = canonicalWebUrl;
@@ -438,6 +468,14 @@
doReplaces();
replaceProgress.end();
+ if (!errors.isEmpty()) {
+ for (Error error : errors.keySet()) {
+ rp.sendMessage(buildError(error, errors.get(error)));
+ }
+ rp.sendMessage(String.format("User: %s", displayName(currentUser)));
+ rp.sendMessage(COMMAND_REJECTION_MESSAGE_FOOTER);
+ }
+
for (final ReceiveCommand c : commands) {
if (c.getResult() == OK) {
switch (c.getType()) {
@@ -475,7 +513,7 @@
// We only schedule direct refs updates for replication.
// Change refs are scheduled when they are created.
//
- replication.scheduleUpdate(project.getNameKey(), c.getRefName());
+ replication.fire(project.getNameKey(), c.getRefName());
Branch.NameKey destBranch = new Branch.NameKey(project.getNameKey(), c.getRefName());
hooks.doRefUpdatedHook(destBranch, c.getOldId(), c.getNewId(), currentUser.getAccount());
commandProgress.update(1);
@@ -502,6 +540,30 @@
}
}
+ private String buildError(Error error, List<String> branches) {
+ StringBuilder sb = new StringBuilder();
+ if (branches.size() == 1) {
+ sb.append("Branch ").append(branches.get(0)).append(":\n");
+ sb.append(error.get());
+ return sb.toString();
+ }
+ sb.append("Branches");
+ String delim = " ";
+ for (String branch : branches) {
+ sb.append(delim).append(branch);
+ delim = ", ";
+ }
+ return sb.append(":\n").append(error.get()).toString();
+ }
+
+ private static String displayName(IdentifiedUser user) {
+ String displayName = user.getUserName();
+ if (displayName == null) {
+ displayName = user.getAccount().getPreferredEmail();
+ }
+ return displayName;
+ }
+
private Account.Id toAccountId(final String nameOrEmail) throws OrmException,
NoSuchAccountException {
final Account a = accountResolver.findByNameOrEmail(nameOrEmail);
@@ -632,6 +694,7 @@
cmd.execute(rp);
}
} else {
+ errors.put(Error.CREATE, ctl.getRefName());
reject(cmd, "can not create new references");
}
}
@@ -648,6 +711,11 @@
cmd.execute(rp);
}
} else {
+ if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) {
+ errors.put(Error.CONFIG_UPDATE, GitRepositoryManager.REF_CONFIG);
+ } else {
+ errors.put(Error.UPDATE, ctl.getRefName());
+ }
reject(cmd, "can not update the reference as a fast forward");
}
}
@@ -678,7 +746,12 @@
cmd.execute(rp);
}
} else {
- reject(cmd, "can not delete references");
+ if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) {
+ reject(cmd, "cannot delete project configuration");
+ } else {
+ errors.put(Error.DELETE, ctl.getRefName());
+ reject(cmd, "can not delete references");
+ }
}
}
@@ -777,7 +850,8 @@
destBranchName.substring(0, split));
destBranchCtl = projectControl.controlForRef(destBranch);
if (!destBranchCtl.canUpload()) {
- reject(cmd, "can not upload a change to this reference");
+ errors.put(Error.CODE_REVIEW, cmd.getRefName());
+ reject(cmd, "can not upload review");
return;
}
@@ -1080,30 +1154,7 @@
ChangeUtil.updated(change);
db.changes().insert(Collections.singleton(change));
ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
-
- final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
- final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
- haveApprovals.add(me);
-
- if (allTypes.size() > 0) {
- final Account.Id authorId =
- info.getAuthor() != null ? info.getAuthor().getAccount() : null;
- final Account.Id committerId =
- info.getCommitter() != null ? info.getCommitter().getAccount() : null;
- final ApprovalCategory.Id catId =
- allTypes.get(allTypes.size() - 1).getCategory().getId();
- if (authorId != null && haveApprovals.add(authorId)) {
- insertDummyApproval(change, ps.getId(), authorId, catId, db);
- }
- if (committerId != null && haveApprovals.add(committerId)) {
- insertDummyApproval(change, ps.getId(), committerId, catId, db);
- }
- for (final Account.Id reviewer : reviewers) {
- if (haveApprovals.add(reviewer)) {
- insertDummyApproval(change, ps.getId(), reviewer, catId, db);
- }
- }
- }
+ approvalsUtil.addReviewers(change, ps, info, reviewers);
db.commit();
} finally {
db.rollback();
@@ -1116,7 +1167,7 @@
throw new IOException("Failed to create ref " + ps.getRefName() + " in "
+ repo.getDirectory() + ": " + ru.getResult());
}
- replication.scheduleUpdate(project.getNameKey(), ru.getName());
+ replication.fire(project.getNameKey(), ru.getName());
allNewChanges.add(change);
@@ -1343,68 +1394,22 @@
result.patchSet = ps;
result.info = info;
- final Account.Id authorId =
- result.info.getAuthor() != null ? result.info.getAuthor().getAccount()
- : null;
- final Account.Id committerId =
- result.info.getCommitter() != null ? result.info.getCommitter()
- .getAccount() : null;
+ List<PatchSetApproval> patchSetApprovals = approvalsUtil.copyVetosToLatestPatchSet(change);
- boolean haveAuthor = false;
- boolean haveCommitter = false;
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)
- && 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)));
- }
- }
-
- if (!haveAuthor && authorId != null && a.getAccountId().equals(authorId)) {
- haveAuthor = true;
- }
- if (!haveCommitter && committerId != null
- && a.getAccountId().equals(committerId)) {
- haveCommitter = true;
- }
}
- final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
- if (allTypes.size() > 0) {
- final ApprovalCategory.Id catId =
- allTypes.get(allTypes.size() - 1).getCategory().getId();
- if (authorId != null && haveApprovals.add(authorId)) {
- insertDummyApproval(result, authorId, catId, db);
- }
- if (committerId != null && haveApprovals.add(committerId)) {
- insertDummyApproval(result, committerId, catId, db);
- }
- for (final Account.Id reviewer : reviewers) {
- if (haveApprovals.add(reviewer)) {
- insertDummyApproval(result, reviewer, catId, db);
- }
- }
- }
+ approvalsUtil.addReviewers(change, ps, info, reviewers, haveApprovals);
msg =
new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
@@ -1464,7 +1469,7 @@
throw new IOException("Failed to create ref " + ps.getRefName() + " in "
+ repo.getDirectory() + ": " + ru.getResult());
}
- replication.scheduleUpdate(project.getNameKey(), ru.getName());
+ replication.fire(project.getNameKey(), ru.getName());
hooks.doPatchsetCreatedHook(result.change, ps, db);
request.cmd.setResult(OK);
@@ -1534,23 +1539,6 @@
}
}
- private void insertDummyApproval(final ReplaceResult result,
- final Account.Id forAccount, final ApprovalCategory.Id catId,
- final ReviewDb db) throws OrmException {
- insertDummyApproval(result.change, result.patchSet.getId(), forAccount,
- catId, db);
- }
-
- private void insertDummyApproval(final Change change, final PatchSet.Id psId,
- final Account.Id forAccount, final ApprovalCategory.Id catId,
- final ReviewDb db) throws OrmException {
- final PatchSetApproval ca =
- new PatchSetApproval(new PatchSetApproval.Key(psId, forAccount, catId),
- (short) 0);
- ca.cache(change);
- db.patchSetApprovals().insert(Collections.singleton(ca));
- }
-
private Ref findMergedInto(final String first, final RevCommit commit) {
try {
final Map<String, Ref> all = repo.getAllRefs();
@@ -1977,7 +1965,7 @@
change.setStatus(Change.Status.MERGED);
ChangeUtil.updated(change);
- ApprovalsUtil.syncChangeStatus(db, change);
+ approvalsUtil.syncChangeStatus(change);
final StringBuilder msgBuf = new StringBuilder();
msgBuf.append("Change has been successfully pushed");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
index e87fe2b..ec8c080 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
@@ -16,7 +16,7 @@
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.transport.AdvertiseRefsHook;
-import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.BaseReceivePack;
import org.eclipse.jgit.transport.UploadPack;
import java.util.HashMap;
@@ -31,7 +31,7 @@
}
@Override
- public void advertiseRefs(ReceivePack rp) {
+ public void advertiseRefs(BaseReceivePack rp) {
Map<String, Ref> oldRefs = rp.getAdvertisedRefs();
if (oldRefs == null) {
oldRefs = rp.getRepository().getAllRefs();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java
deleted file mode 100644
index f7bb9f8..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (C) 2009 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.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-
-/** Manages replication to other nodes. */
-public interface ReplicationQueue {
- /** Is replication to one or more other destinations configured? */
- boolean isEnabled();
-
- /**
- * Schedule a full replication for a single project.
- * <p>
- * All remote URLs are checked to verify the are current with regards to the
- * local project state. If not, they are updated by pushing new refs, updating
- * existing ones which don't match, and deleting stale refs which have been
- * removed from the local repository.
- *
- * @param project identity of the project to replicate.
- * @param urlMatch substring that must appear in a URI to support replication.
- */
- void scheduleFullSync(Project.NameKey project, String urlMatch);
-
- /**
- * Schedule update of a single ref.
- * <p>
- * This method automatically tries to batch together multiple requests in the
- * same project, to take advantage of Git's native ability to update multiple
- * refs during a single push operation.
- *
- * @param project identity of the project to replicate.
- * @param ref unique name of the ref; must start with {@code refs/}.
- */
- void scheduleUpdate(Project.NameKey project, String ref);
-
- /**
- * Create new empty project at the remote sites.
- * <p>
- * When a new project has been created locally call this method to make sure
- * that the project will be created at the remote sites as well.
- *
- * @param project of the project to be created.
- * @param head name HEAD should point at (must be {@code refs/heads/...}).
- */
- void replicateNewProject(Project.NameKey project, String head);
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
index 64ba6e8..98ddf80 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.git;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java
deleted file mode 100644
index d51936a..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java
+++ /dev/null
@@ -1,92 +0,0 @@
-// Copyright (C) 2011 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.git;
-
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.UnsupportedCredentialItem;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.transport.CredentialItem;
-import org.eclipse.jgit.transport.CredentialsProvider;
-import org.eclipse.jgit.transport.URIish;
-
-/** Looks up a remote's password in secure.config. */
-public class SecureCredentialsProvider extends CredentialsProvider {
- public interface Factory {
- SecureCredentialsProvider create(String remoteName);
- }
-
- private final String cfgUser;
- private final String cfgPass;
-
- @Inject
- SecureCredentialsProvider(@GerritServerConfig Config cfg,
- @Assisted String remoteName) {
- cfgUser = cfg.getString("remote", remoteName, "username");
- cfgPass = cfg.getString("remote", remoteName, "password");
- }
-
- @Override
- public boolean isInteractive() {
- return false;
- }
-
- @Override
- public boolean supports(CredentialItem... items) {
- for (CredentialItem i : items) {
- if (i instanceof CredentialItem.Username) {
- continue;
- } else if (i instanceof CredentialItem.Password) {
- continue;
- } else {
- return false;
- }
- }
- return true;
- }
-
- @Override
- public boolean get(URIish uri, CredentialItem... items)
- throws UnsupportedCredentialItem {
- String username = uri.getUser();
- if (username == null) {
- username = cfgUser;
- }
- if (username == null) {
- return false;
- }
-
- String password = uri.getPass();
- if (password == null) {
- password = cfgPass;
- }
- if (password == null) {
- return false;
- }
-
- for (CredentialItem i : items) {
- if (i instanceof CredentialItem.Username) {
- ((CredentialItem.Username) i).setValue(username);
- } else if (i instanceof CredentialItem.Password) {
- ((CredentialItem.Password) i).setValue(password.toCharArray());
- } else {
- throw new UnsupportedCredentialItem(uri, i.getPromptText());
- }
- }
- return true;
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 706ba7d..0ca8793 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -21,6 +21,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.util.SubmoduleSectionParser;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
@@ -84,7 +85,7 @@
private final Map<Change.Id, CodeReviewCommit> commits;
private final PersonIdent myIdent;
private final GitRepositoryManager repoManager;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated replication;
private final SchemaFactory<ReviewDb> schemaFactory;
private final Set<Branch.NameKey> updatedSubscribers;
@@ -96,7 +97,7 @@
@Assisted Project destProject, @Assisted List<Change> submitted,
@Assisted final Map<Change.Id, CodeReviewCommit> commits,
@GerritPersonIdent final PersonIdent myIdent,
- GitRepositoryManager repoManager, ReplicationQueue replication) {
+ GitRepositoryManager repoManager, GitReferenceUpdated replication) {
this.destBranch = destBranch;
this.mergeTip = mergeTip;
this.rw = rw;
@@ -201,6 +202,9 @@
if (!subscribers.isEmpty()) {
String msgbuf = msg;
if (msgbuf == null) {
+ // Initialize the message buffer
+ msgbuf = "";
+
// The first updatedBranch on a cascade event of automatic
// updates of repos is added to updatedSubscribers set so
// if we face a situation having
@@ -331,7 +335,7 @@
switch (rfu.update()) {
case NEW:
case FAST_FORWARD:
- replication.scheduleUpdate(subscriber.getParentKey(), rfu.getName());
+ replication.fire(subscriber.getParentKey(), rfu.getName());
// TODO since this is performed "in the background" no mail will be
// sent to inform users about the updated branch
break;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
index ac4882f..3c64229 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
@@ -14,13 +14,12 @@
package com.google.gerrit.server.git;
+import com.google.common.cache.Cache;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import org.eclipse.jgit.lib.ObjectId;
@@ -38,19 +37,17 @@
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<EntryKey, EntryVal>> type =
- new TypeLiteral<Cache<EntryKey, EntryVal>>() {};
- disk(type, CACHE_NAME);
+ persist(CACHE_NAME, String.class, EntryVal.class);
bind(TagCache.class);
}
};
}
- private final Cache<EntryKey, EntryVal> cache;
+ private final Cache<String, EntryVal> cache;
private final Object createLock = new Object();
@Inject
- TagCache(@Named(CACHE_NAME) Cache<EntryKey, EntryVal> cache) {
+ TagCache(@Named(CACHE_NAME) Cache<String, EntryVal> cache) {
this.cache = cache;
}
@@ -74,67 +71,43 @@
// never fail with an exception. Some of these references can be null
// (e.g. not all projects are cached, or the cache is not current).
//
- EntryVal val = cache.get(new EntryKey(name));
+ EntryVal val = cache.getIfPresent(name.get());
if (val != null) {
TagSetHolder holder = val.holder;
if (holder != null) {
TagSet tags = holder.getTagSet();
if (tags != null) {
- tags.updateFastForward(refName, oldValue, newValue);
+ if (tags.updateFastForward(refName, oldValue, newValue)) {
+ cache.put(name.get(), val);
+ }
}
}
}
}
TagSetHolder get(Project.NameKey name) {
- EntryKey key = new EntryKey(name);
- EntryVal val = cache.get(key);
+ EntryVal val = cache.getIfPresent(name.get());
if (val == null) {
synchronized (createLock) {
- val = cache.get(key);
+ val = cache.getIfPresent(name.get());
if (val == null) {
val = new EntryVal();
val.holder = new TagSetHolder(name);
- cache.put(key, val);
+ cache.put(name.get(), val);
}
}
}
return val.holder;
}
- static class EntryKey implements Serializable {
- static final long serialVersionUID = 1L;
-
- private transient String name;
-
- EntryKey(Project.NameKey name) {
- this.name = name.get();
- }
-
- @Override
- public int hashCode() {
- return name.hashCode();
- }
-
- @Override
- public boolean equals(Object o) {
- if (o instanceof EntryKey) {
- return name.equals(((EntryKey) o).name);
- }
- return false;
- }
-
- private void readObject(ObjectInputStream in) throws IOException {
- name = in.readUTF();
- }
-
- private void writeObject(ObjectOutputStream out) throws IOException {
- out.writeUTF(name);
- }
+ void put(Project.NameKey name, TagSetHolder tags) {
+ EntryVal val = new EntryVal();
+ val.holder = tags;
+ cache.put(name.get(), val);
}
static class EntryVal implements Serializable {
- static final long serialVersionUID = EntryKey.serialVersionUID;
+ static final long serialVersionUID = 1L;
transient TagSetHolder holder;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
index 6cf873d..7d95db2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagMatcher.java
@@ -30,15 +30,22 @@
final List<Ref> newRefs = new ArrayList<Ref>();
final List<LostRef> lostRefs = new ArrayList<LostRef>();
final TagSetHolder holder;
+ final TagCache cache;
final Repository db;
final Collection<Ref> include;
TagSet tags;
- boolean updated;
+ final boolean updated;
private boolean rebuiltForNewTags;
- TagMatcher(TagSetHolder holder, Repository db, Collection<Ref> include,
- TagSet tags, boolean updated) {
+ TagMatcher(
+ TagSetHolder holder,
+ TagCache cache,
+ Repository db,
+ Collection<Ref> include,
+ TagSet tags,
+ boolean updated) {
this.holder = holder;
+ this.cache = cache;
this.db = db;
this.include = include;
this.tags = tags;
@@ -63,7 +70,7 @@
}
rebuiltForNewTags = true;
- holder.rebuildForNewTags(this);
+ holder.rebuildForNewTags(cache, this);
return isReachable(tagRef);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
index 8830580..c57942c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSet.java
@@ -58,7 +58,7 @@
return tags.get(id);
}
- void updateFastForward(String refName, ObjectId oldValue,
+ boolean updateFastForward(String refName, ObjectId oldValue,
ObjectId newValue) {
CachedRef ref = refs.get(refName);
if (ref != null) {
@@ -68,9 +68,10 @@
//
ObjectId cur = ref.get();
if (cur.equals(oldValue)) {
- ref.compareAndSet(cur, newValue);
+ return ref.compareAndSet(cur, newValue);
}
}
+ return false;
}
void prepare(TagMatcher m) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
index 91c8a5c..d5120e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
@@ -42,51 +42,52 @@
this.tags = tags;
}
- TagMatcher matcher(Repository db, Collection<Ref> include) {
+ TagMatcher matcher(TagCache cache, Repository db, Collection<Ref> include) {
TagSet tags = this.tags;
if (tags == null) {
- tags = build(db);
+ tags = build(cache, db);
}
- TagMatcher m = new TagMatcher(this, db, include, tags, false);
+ TagMatcher m = new TagMatcher(this, cache, db, include, tags, false);
tags.prepare(m);
if (!m.newRefs.isEmpty() || !m.lostRefs.isEmpty()) {
- tags = rebuild(db, tags, m);
+ tags = rebuild(cache, db, tags, m);
- m = new TagMatcher(this, db, include, tags, true);
+ m = new TagMatcher(this, cache, db, include, tags, true);
tags.prepare(m);
}
return m;
}
- void rebuildForNewTags(TagMatcher m) {
- m.tags = rebuild(m.db, m.tags, null);
-
+ void rebuildForNewTags(TagCache cache, TagMatcher m) {
+ m.tags = rebuild(cache, m.db, m.tags, null);
m.mask.clear();
m.newRefs.clear();
m.lostRefs.clear();
m.tags.prepare(m);
}
- private TagSet build(Repository db) {
+ private TagSet build(TagCache cache, Repository db) {
synchronized (buildLock) {
TagSet tags = this.tags;
if (tags == null) {
tags = new TagSet(projectName);
tags.build(db, null, null);
this.tags = tags;
+ cache.put(projectName, this);
}
return tags;
}
}
- private TagSet rebuild(Repository db, TagSet old, TagMatcher m) {
+ private TagSet rebuild(TagCache cache, Repository db, TagSet old, TagMatcher m) {
synchronized (buildLock) {
TagSet cur = this.tags;
if (cur == old) {
cur = new TagSet(projectName);
cur.build(db, old, m);
this.tags = cur;
+ cache.put(projectName, this);
}
return cur;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
index c34cc54..085424e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.git;
+import com.google.common.base.Objects;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -23,7 +24,7 @@
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.UnmergedPathException;
+import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
@@ -150,72 +151,130 @@
* executed as requested.
*/
public boolean commit(MetaDataUpdate update) throws IOException {
- final Repository db = update.getRepository();
- final CommitBuilder commit = update.getCommitBuilder();
-
- reader = db.newObjectReader();
- inserter = db.newObjectInserter();
+ BatchMetaDataUpdate batch = openUpdate(update);
try {
- final RevWalk rw = new RevWalk(reader);
- final RevTree src = revision != null ? rw.parseTree(revision) : null;
- final ObjectId res = writeTree(src, commit);
-
- if (res.equals(src)) {
- // If there are no changes to the content, don't create the commit.
- return true;
- }
-
- commit.setTreeId(res);
- if (revision != null) {
- commit.setParentId(revision);
- }
-
- RefUpdate ru = db.updateRef(getRefName());
- if (revision != null) {
- ru.setExpectedOldObjectId(revision);
- } else {
- ru.setExpectedOldObjectId(ObjectId.zeroId());
- }
- ru.setNewObjectId(inserter.insert(commit));
- ru.disableRefLog();
- inserter.flush();
-
- switch (ru.update(rw)) {
- case NEW:
- case FAST_FORWARD:
- revision = rw.parseCommit(ru.getNewObjectId());
- update.replicate(ru.getName());
- return true;
-
- case LOCK_FAILURE:
- return false;
-
- default:
- throw new IOException("Cannot update " + ru.getName() + " in "
- + db.getDirectory() + ": " + ru.getResult());
- }
- } catch (ConfigInvalidException e) {
- throw new IOException("Cannot update " + getRefName() + " in "
- + db.getDirectory() + ": " + e.getMessage(), e);
+ batch.write(update.getCommitBuilder());
+ return batch.commit();
} finally {
- inserter.release();
- inserter = null;
-
- reader.release();
- reader = null;
+ batch.close();
}
}
- private ObjectId writeTree(RevTree srcTree, CommitBuilder commit)
- throws IOException, MissingObjectException, IncorrectObjectTypeException,
- UnmergedPathException, ConfigInvalidException {
- try {
- newTree = readTree(srcTree);
- onSave(commit);
- return newTree.writeTree(inserter);
- } finally {
- newTree = null;
- }
+ public interface BatchMetaDataUpdate {
+ void write(CommitBuilder commit) throws IOException;
+ void write(VersionedMetaData config, CommitBuilder commit) throws IOException;
+ boolean commit() throws IOException;
+ boolean commitAt(ObjectId revision) throws IOException;
+ void close();
+ }
+
+ public BatchMetaDataUpdate openUpdate(final MetaDataUpdate update) throws IOException {
+ final Repository db = update.getRepository();
+
+ reader = db.newObjectReader();
+ inserter = db.newObjectInserter();
+ final RevWalk rw = new RevWalk(reader);
+ final RevTree tree = revision != null ? rw.parseTree(revision) : null;
+ newTree = readTree(tree);
+ return new BatchMetaDataUpdate() {
+ AnyObjectId src = revision;
+ AnyObjectId srcTree = tree;
+
+ @Override
+ public void write(CommitBuilder commit) throws IOException {
+ write(VersionedMetaData.this, commit);
+ }
+
+ private void doSave(VersionedMetaData config, CommitBuilder commit) throws IOException {
+ DirCache nt = config.newTree;
+ ObjectReader r = config.reader;
+ ObjectInserter i = config.inserter;
+ try {
+ config.newTree = newTree;
+ config.reader = reader;
+ config.inserter = inserter;
+ config.onSave(commit);
+ } catch (ConfigInvalidException e) {
+ throw new IOException("Cannot update " + getRefName() + " in "
+ + db.getDirectory() + ": " + e.getMessage(), e);
+ } finally {
+ config.newTree = nt;
+ config.reader = r;
+ config.inserter = i;
+ }
+ }
+
+ @Override
+ public void write(VersionedMetaData config, CommitBuilder commit) throws IOException {
+ doSave(config, commit);
+
+ final ObjectId res = newTree.writeTree(inserter);
+ if (res.equals(srcTree)) {
+ // If there are no changes to the content, don't create the commit.
+ return;
+ }
+
+ commit.setTreeId(res);
+ if (src != null) {
+ commit.addParentId(src);
+ }
+
+ src = inserter.insert(commit);
+ srcTree = res;
+ }
+
+ @Override
+ public boolean commit() throws IOException {
+ return commitAt(revision);
+ }
+
+ @Override
+ public boolean commitAt(ObjectId expected) throws IOException {
+ if (Objects.equal(src, expected)) {
+ return true;
+ }
+
+ RefUpdate ru = db.updateRef(getRefName());
+ if (expected != null) {
+ ru.setExpectedOldObjectId(expected);
+ } else {
+ ru.setExpectedOldObjectId(ObjectId.zeroId());
+ }
+ ru.setNewObjectId(src);
+ ru.disableRefLog();
+ inserter.flush();
+
+ switch (ru.update(rw)) {
+ case NEW:
+ case FAST_FORWARD:
+ revision = rw.parseCommit(ru.getNewObjectId());
+ update.replicate(ru.getName());
+ return true;
+
+ case LOCK_FAILURE:
+ return false;
+
+ default:
+ throw new IOException("Cannot update " + ru.getName() + " in "
+ + db.getDirectory() + ": " + ru.getResult());
+ }
+ }
+
+ @Override
+ public void close() {
+ newTree = null;
+
+ if (inserter != null) {
+ inserter.release();
+ inserter = null;
+ }
+
+ if (reader != null) {
+ reader.release();
+ reader = null;
+ }
+ }
+ };
}
private DirCache readTree(RevTree tree) throws IOException,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
index fc47f10..bf0d75d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.git;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
@@ -60,6 +62,13 @@
}
public Map<String, Ref> filter(Map<String, Ref> refs, boolean filterTagsSeperately) {
+ if (projectCtl.allRefsAreVisibleExcept(
+ ImmutableSet.of(GitRepositoryManager.REF_CONFIG))) {
+ Map<String, Ref> r = Maps.newHashMap(refs);
+ r.remove(GitRepositoryManager.REF_CONFIG);
+ return r;
+ }
+
final Set<Change.Id> visibleChanges = visibleChanges();
final Map<String, Ref> result = new HashMap<String, Ref>();
final List<Ref> deferredTags = new ArrayList<Ref>();
@@ -92,8 +101,10 @@
// to identify what tags we can actually reach, and what we cannot.
//
if (!deferredTags.isEmpty() && (!result.isEmpty() || filterTagsSeperately)) {
- TagMatcher tags = tagCache.get(projectName).
- matcher(db, filterTagsSeperately ? filter(db.getAllRefs()).values() : result.values());
+ TagMatcher tags = tagCache.get(projectName).matcher(
+ tagCache,
+ db,
+ filterTagsSeperately ? filter(db.getAllRefs()).values() : result.values());
for (Ref tag : deferredTags) {
if (tags.isReachable(tag)) {
result.put(tag.getName(), tag);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
index 987ab7c..bb11e62 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.git;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gerrit.server.util.IdGenerator;
@@ -172,6 +172,10 @@
);
}
+ public void unregisterWorkQueue() {
+ queues.remove(this);
+ }
+
@Override
protected <V> RunnableScheduledFuture<V> decorateTask(
final Runnable runnable, RunnableScheduledFuture<V> r) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
index 7fabcfe1..0eb3dfe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -38,7 +39,7 @@
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
+ bccWatches(NotifyType.ALL_COMMENTS);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
index 624e626..4e9ed2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -55,6 +55,19 @@
}
@Override
+ public int hashCode() {
+ return email.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof Address) {
+ return email.equals(((Address) other).email);
+ }
+ return false;
+ }
+
+ @Override
public String toString() {
try {
return toHeaderString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index ff0bb578..31c8bd5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -14,39 +14,53 @@
package com.google.gerrit.server.mail;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.StarredChange;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.NotifyConfig;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.SingleGroupUser;
import com.google.gwtorm.server.OrmException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.text.MessageFormat;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
-import java.util.List;
+import java.util.Queue;
import java.util.Set;
import java.util.TreeSet;
/** Sends an email to one or more interested parties. */
public abstract class ChangeEmail extends OutgoingEmail {
+ private static final Logger log = LoggerFactory.getLogger(ChangeEmail.class);
+
protected final Change change;
protected PatchSet patchSet;
protected PatchSetInfo patchSetInfo;
@@ -224,42 +238,46 @@
/** Create the change message and the affected file list. */
public String getChangeDetail() {
- StringBuilder detail = new StringBuilder();
+ try {
+ StringBuilder detail = new StringBuilder();
- if (patchSetInfo != null) {
- detail.append(patchSetInfo.getMessage().trim() + "\n");
- } else {
- detail.append(change.getSubject().trim() + "\n");
- }
-
- if (patchSet != null) {
- detail.append("---\n");
- PatchList patchList = getPatchList();
- for (PatchListEntry p : patchList.getPatches()) {
- if (Patch.COMMIT_MSG.equals(p.getNewName())) {
- continue;
- }
- detail.append(p.getChangeType().getCode() + " " + p.getNewName() + "\n");
+ if (patchSetInfo != null) {
+ detail.append(patchSetInfo.getMessage().trim() + "\n");
+ } else {
+ detail.append(change.getSubject().trim() + "\n");
}
- detail.append(MessageFormat.format("" //
- + "{0,choice,0#0 files|1#1 file|1<{0} files} changed, " //
- + "{1,choice,0#0 insertions|1#1 insertion|1<{1} insertions}(+), " //
- + "{2,choice,0#0 deletions|1#1 deletion|1<{2} deletions}(-)" //
- + "\n", patchList.getPatches().size() - 1, //
- patchList.getInsertions(), //
- patchList.getDeletions()));
- detail.append("\n");
+
+ if (patchSet != null) {
+ detail.append("---\n");
+ PatchList patchList = getPatchList();
+ for (PatchListEntry p : patchList.getPatches()) {
+ if (Patch.COMMIT_MSG.equals(p.getNewName())) {
+ continue;
+ }
+ detail.append(p.getChangeType().getCode() + " " + p.getNewName() + "\n");
+ }
+ detail.append(MessageFormat.format("" //
+ + "{0,choice,0#0 files|1#1 file|1<{0} files} changed, " //
+ + "{1,choice,0#0 insertions|1#1 insertion|1<{1} insertions}(+), " //
+ + "{2,choice,0#0 deletions|1#1 deletion|1<{2} deletions}(-)" //
+ + "\n", patchList.getPatches().size() - 1, //
+ patchList.getInsertions(), //
+ patchList.getDeletions()));
+ detail.append("\n");
+ }
+ return detail.toString();
+ } catch (Exception err) {
+ log.warn("Cannot format change detail", err);
+ return "";
}
- return detail.toString();
}
-
/** Get the patch list corresponding to this patch set. */
- protected PatchList getPatchList() {
+ protected PatchList getPatchList() throws PatchListNotAvailableException {
if (patchSet != null) {
return args.patchListCache.get(change, patchSet);
}
- return null;
+ throw new PatchListNotAvailableException("no patchSet specified");
}
/** Get the project entity the change is in; null if its been deleted. */
@@ -295,53 +313,149 @@
// Just don't BCC everyone. Better to send a partial message to those
// we already have queued up then to fail deliver entirely to people
// who have a lower interest in the change.
+ log.warn("Cannot BCC users that starred updated change", err);
}
}
- /** BCC any user who has set "notify all comments" on this project. */
- protected void bccWatchesNotifyAllComments() {
+ /** BCC users and groups that want notification of events. */
+ protected void bccWatches(NotifyType type) {
try {
- // BCC anyone else who has interest in this project's changes
- //
- for (final AccountProjectWatch w : getWatches()) {
- if (w.isNotify(NotifyType.ALL_COMMENTS)) {
- add(RecipientType.BCC, w.getAccountId());
- }
+ Watchers matching = getWatches(type);
+ for (Account.Id user : matching.accounts) {
+ add(RecipientType.BCC, user);
+ }
+ for (Address addr : matching.emails) {
+ add(RecipientType.BCC, addr);
}
} catch (OrmException err) {
// Just don't CC everyone. Better to send a partial message to those
// we already have queued up then to fail deliver entirely to people
// who have a lower interest in the change.
+ log.warn("Cannot BCC watchers for " + type, err);
}
}
/** Returns all watches that are relevant */
- protected final List<AccountProjectWatch> getWatches() throws OrmException {
+ protected final Watchers getWatches(NotifyType type) throws OrmException {
+ Watchers matching = new Watchers();
if (changeData == null) {
- return Collections.emptyList();
+ return matching;
}
- List<AccountProjectWatch> matching = new ArrayList<AccountProjectWatch>();
Set<Account.Id> projectWatchers = new HashSet<Account.Id>();
for (AccountProjectWatch w : args.db.get().accountProjectWatches()
.byProject(change.getProject())) {
projectWatchers.add(w.getAccountId());
- add(matching, w);
- }
-
- for (AccountProjectWatch w : args.db.get().accountProjectWatches()
- .byProject(args.allProjectsName)) {
- if (!projectWatchers.contains(w.getAccountId())) {
+ if (w.isNotify(type)) {
add(matching, w);
}
}
- return Collections.unmodifiableList(matching);
+ for (AccountProjectWatch w : args.db.get().accountProjectWatches()
+ .byProject(args.allProjectsName)) {
+ if (!projectWatchers.contains(w.getAccountId()) && w.isNotify(type)) {
+ add(matching, w);
+ }
+ }
+
+ ProjectState state = projectState;
+ while (state != null) {
+ for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
+ if (nc.isNotify(type)) {
+ try {
+ add(matching, nc, state.getProject().getNameKey());
+ } catch (QueryParseException e) {
+ log.warn(String.format(
+ "Project %s has invalid notify %s filter \"%s\": %s",
+ state.getProject().getName(), nc.getName(),
+ nc.getFilter(), e.getMessage()));
+ }
+ }
+ }
+ state = state.getParentState();
+ }
+
+ return matching;
+ }
+
+ protected static class Watchers {
+ protected final Set<Account.Id> accounts = Sets.newHashSet();
+ protected final Set<Address> emails = Sets.newHashSet();
}
@SuppressWarnings("unchecked")
- private void add(List<AccountProjectWatch> matching, AccountProjectWatch w)
+ private void add(Watchers matching, NotifyConfig nc, Project.NameKey project)
+ throws OrmException, QueryParseException {
+ for (GroupReference ref : nc.getGroups()) {
+ AccountGroup group =
+ GroupDescriptions.toAccountGroup(args.groupBackend.get(ref.getUUID()));
+ if (group == null) {
+ log.warn(String.format(
+ "Project %s has invalid group %s in notify section %s",
+ project.get(), ref.getName(), nc.getName()));
+ continue;
+ }
+
+ if (group.getType() != AccountGroup.Type.INTERNAL) {
+ log.warn(String.format(
+ "Project %s cannot use group %s of type %s in notify section %s",
+ project.get(), ref.getName(), group.getType(), nc.getName()));
+ continue;
+ }
+
+ ChangeQueryBuilder qb = args.queryBuilder.create(new SingleGroupUser(
+ args.capabilityControlFactory,
+ ref.getUUID()));
+ qb.setAllowFile(true);
+ Predicate<ChangeData> p = qb.is_visible();
+ if (nc.getFilter() != null) {
+ p = Predicate.and(qb.parse(nc.getFilter()), p);
+ p = args.queryRewriter.get().rewrite(p);
+ }
+ if (p.match(changeData)) {
+ recursivelyAddAllAccounts(matching, group);
+ }
+ }
+
+ if (!nc.getAddresses().isEmpty()) {
+ if (nc.getFilter() != null) {
+ ChangeQueryBuilder qb = args.queryBuilder.create(args.anonymousUser);
+ qb.setAllowFile(true);
+ Predicate<ChangeData> p = qb.parse(nc.getFilter());
+ p = args.queryRewriter.get().rewrite(p);
+ if (p.match(changeData)) {
+ matching.emails.addAll(nc.getAddresses());
+ }
+ } else {
+ matching.emails.addAll(nc.getAddresses());
+ }
+ }
+ }
+
+ private void recursivelyAddAllAccounts(Watchers matching, AccountGroup group)
+ throws OrmException {
+ Set<AccountGroup.Id> seen = Sets.newHashSet();
+ Queue<AccountGroup.Id> scan = Lists.newLinkedList();
+ scan.add(group.getId());
+ seen.add(group.getId());
+ while (!scan.isEmpty()) {
+ AccountGroup.Id next = scan.remove();
+ for (AccountGroupMember m : args.db.get().accountGroupMembers()
+ .byGroup(next)) {
+ matching.accounts.add(m.getAccountId());
+ }
+ for (AccountGroupInclude m : args.db.get().accountGroupIncludes()
+ .byGroup(next)) {
+ if (seen.add(m.getIncludeId())) {
+ scan.add(m.getIncludeId());
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void add(Watchers matching, AccountProjectWatch w)
throws OrmException {
IdentifiedUser user =
args.identifiedUserFactory.create(args.db, w.getAccountId());
@@ -353,13 +467,13 @@
p = Predicate.and(qb.parse(w.getFilter()), p);
p = args.queryRewriter.get().rewrite(p);
if (p.match(changeData)) {
- matching.add(w);
+ matching.accounts.add(w.getAccountId());
}
} catch (QueryParseException e) {
// Ignore broken filter expressions.
}
} else if (p.match(changeData)) {
- matching.add(w);
+ matching.accounts.add(w.getAccountId());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index f054ee8..6a85ea3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -17,13 +17,14 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.patch.PatchFile;
import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Repository;
import java.io.IOException;
@@ -68,7 +69,7 @@
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
+ bccWatches(NotifyType.ALL_COMMENTS);
}
@Override
@@ -81,7 +82,14 @@
final Repository repo = getRepository();
try {
- final PatchList patchList = repo != null ? getPatchList() : null;
+ PatchList patchList = null;
+ if (repo != null) {
+ try {
+ patchList = getPatchList();
+ } catch (PatchListNotAvailableException e) {
+ patchList = null;
+ }
+ }
Patch.Key currentFileKey = null;
PatchFile currentFileData = null;
@@ -139,7 +147,7 @@
private Repository getRepository() {
try {
return args.server.openRepository(projectState.getProject().getNameKey());
- } catch (RepositoryNotFoundException e) {
+ } catch (IOException e) {
return null;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index c6f716d..9b82c71 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -15,74 +15,65 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import java.util.HashSet;
-import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/** Notify interested parties of a brand new change. */
public class CreateChangeSender extends NewChangeSender {
+ private static final Logger log =
+ LoggerFactory.getLogger(CreateChangeSender.class);
+
public static interface Factory {
public CreateChangeSender create(Change change);
}
- private final GroupCache groupCache;
-
@Inject
public CreateChangeSender(EmailArguments ea,
@AnonymousCowardName String anonymousCowardName, SshInfo sshInfo,
- GroupCache groupCache, @Assisted Change c) {
+ @Assisted Change c) {
super(ea, anonymousCowardName, sshInfo, c);
- this.groupCache = groupCache;
}
@Override
protected void init() throws EmailException {
super.init();
- bccWatchers();
- }
-
- private void bccWatchers() {
try {
+ // BCC anyone who has interest in this project's changes
// Try to mark interested owners with a TO and not a BCC line.
//
- final Set<Account.Id> owners = new HashSet<Account.Id>();
- for (AccountGroup.UUID uuid : getProjectOwners()) {
- AccountGroup group = groupCache.get(uuid);
- if (group != null) {
- for (AccountGroupMember m : args.db.get().accountGroupMembers()
- .byGroup(group.getId())) {
- owners.add(m.getAccountId());
- }
+ Watchers matching = getWatches(NotifyType.NEW_CHANGES);
+ for (Account.Id user : matching.accounts) {
+ if (isOwnerOfProjectOrBranch(user)) {
+ add(RecipientType.TO, user);
+ } else {
+ add(RecipientType.BCC, user);
}
}
-
- // BCC anyone who has interest in this project's changes
- //
- for (final AccountProjectWatch w : getWatches()) {
- if (w.isNotify(NotifyType.NEW_CHANGES)) {
- if (owners.contains(w.getAccountId())) {
- add(RecipientType.TO, w.getAccountId());
- } else {
- add(RecipientType.BCC, w.getAccountId());
- }
- }
+ for (Address addr : matching.emails) {
+ add(RecipientType.BCC, addr);
}
} catch (OrmException err) {
// Just don't CC everyone. Better to send a partial message to those
// we already have queued up then to fail deliver entirely to people
// who have a lower interest in the change.
+ log.warn("Cannot BCC watchers for new change", err);
}
}
+
+ private boolean isOwnerOfProjectOrBranch(Account.Id user) {
+ return projectState != null
+ && change != null
+ && projectState.controlFor(args.identifiedUserFactory.create(user))
+ .controlForRef(change.getDest())
+ .isOwner();
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index 68f78d0..e6c96a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -15,10 +15,12 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.IdentifiedUser.GenericFactory;
import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -37,13 +39,15 @@
class EmailArguments {
final GitRepositoryManager server;
final ProjectCache projectCache;
- final GroupCache groupCache;
+ final GroupBackend groupBackend;
final AccountCache accountCache;
final PatchListCache patchListCache;
final FromAddressGenerator fromAddressGenerator;
final EmailSender emailSender;
final PatchSetInfoFactory patchSetInfoFactory;
final IdentifiedUser.GenericFactory identifiedUserFactory;
+ final CapabilityControl.Factory capabilityControlFactory;
+ final AnonymousUser anonymousUser;
final Provider<String> urlProvider;
final AllProjectsName allProjectsName;
@@ -54,10 +58,12 @@
@Inject
EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
- GroupCache groupCache, AccountCache accountCache,
+ GroupBackend groupBackend, AccountCache accountCache,
PatchListCache patchListCache, FromAddressGenerator fromAddressGenerator,
EmailSender emailSender, PatchSetInfoFactory patchSetInfoFactory,
GenericFactory identifiedUserFactory,
+ CapabilityControl.Factory capabilityControlFactory,
+ AnonymousUser anonymousUser,
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
AllProjectsName allProjectsName,
ChangeQueryBuilder.Factory queryBuilder,
@@ -65,13 +71,15 @@
RuntimeInstance velocityRuntime) {
this.server = server;
this.projectCache = projectCache;
- this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
this.accountCache = accountCache;
this.patchListCache = patchListCache;
this.fromAddressGenerator = fromAddressGenerator;
this.emailSender = emailSender;
this.patchSetInfoFactory = patchSetInfoFactory;
this.identifiedUserFactory = identifiedUserFactory;
+ this.capabilityControlFactory = capabilityControlFactory;
+ this.anonymousUser = anonymousUser;
this.urlProvider = urlProvider;
this.allProjectsName = allProjectsName;
this.queryBuilder = queryBuilder;
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/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index 3590b8a..70b2d7f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -17,7 +17,6 @@
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
import com.google.gerrit.reviewdb.client.Change;
@@ -53,8 +52,8 @@
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
- bccWatchesNotifySubmittedChanges();
+ bccWatches(NotifyType.ALL_COMMENTS);
+ bccWatches(NotifyType.SUBMITTED_CHANGES);
}
@Override
@@ -140,20 +139,4 @@
}
m.put(ca.getCategoryId(), ca);
}
-
- private void bccWatchesNotifySubmittedChanges() {
- try {
- // BCC anyone else who has interest in this project's changes
- //
- for (final AccountProjectWatch w : getWatches()) {
- if (w.isNotify(NotifyType.SUBMITTED_CHANGES)) {
- add(RecipientType.BCC, w.getAccountId());
- }
- }
- } catch (OrmException err) {
- // Just don't CC everyone. Better to send a partial message to those
- // we already have queued up then to fail deliver entirely to people
- // who have a lower interest in the change.
- }
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index de8628f..caa441d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.mail;
+import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.UserIdentity;
import com.google.gerrit.server.account.AccountState;
@@ -34,14 +35,13 @@
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
-import java.util.List;
import java.util.Map;
+import java.util.Set;
/** Sends an email to one or more interested parties. */
public abstract class OutgoingEmail {
@@ -53,7 +53,7 @@
protected String messageClass;
private final HashSet<Account.Id> rcptTo = new HashSet<Account.Id>();
private final Map<String, EmailHeader> headers;
- private final List<Address> smtpRcptTo = new ArrayList<Address>();
+ private final Set<Address> smtpRcptTo = Sets.newHashSet();
private Address smtpFromAddress;
private StringBuilder body;
protected VelocityContext velocityContext;
@@ -282,7 +282,7 @@
return false;
}
- if (rcptTo.size() == 1 && rcptTo.contains(fromId)) {
+ if (smtpRcptTo.size() == 1 && rcptTo.size() == 1 && rcptTo.contains(fromId)) {
// If the only recipient is also the sender, don't bother.
//
return false;
@@ -324,14 +324,15 @@
protected void add(final RecipientType rt, final Address addr) {
if (addr != null && addr.email != null && addr.email.length() > 0) {
if (args.emailSender.canEmail(addr.email)) {
- smtpRcptTo.add(addr);
- switch (rt) {
- case TO:
- ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
- break;
- case CC:
- ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
- break;
+ if (smtpRcptTo.add(addr)) {
+ switch (rt) {
+ case TO:
+ ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
+ break;
+ case CC:
+ ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
+ break;
+ }
}
} else {
log.warn("Not emailing " + addr.email + " (prohibited by allowrcpt)");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
index c9afdde..946c29f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -38,7 +39,7 @@
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
+ bccWatches(NotifyType.ALL_COMMENTS);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
index 964bfed..033bd56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -37,7 +38,7 @@
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
+ bccWatches(NotifyType.ALL_COMMENTS);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
index c5c5925..08af5e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
@@ -70,11 +70,11 @@
return edits;
}
- ObjectId getBlobA() {
+ public ObjectId getBlobA() {
return aId;
}
- ObjectId getBlobB() {
+ public ObjectId getBlobB() {
return bId;
}
@@ -114,6 +114,9 @@
public String toString() {
StringBuilder n = new StringBuilder();
n.append("IntraLineDiffKey[");
+ if (projectKey != null) {
+ n.append(projectKey.get()).append(" ");
+ }
n.append(aId.name());
n.append("..");
n.append(bId.name());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
index 358d3ba..5b65920 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -15,7 +15,7 @@
package com.google.gerrit.server.patch;
-import com.google.gerrit.server.cache.EntryCreator;
+import com.google.common.cache.CacheLoader;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
@@ -35,9 +35,8 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
-class IntraLineLoader extends EntryCreator<IntraLineDiffKey, IntraLineDiff> {
- private static final Logger log = LoggerFactory
- .getLogger(IntraLineLoader.class);
+class IntraLineLoader extends CacheLoader<IntraLineDiffKey, IntraLineDiff> {
+ static final Logger log = LoggerFactory.getLogger(IntraLineLoader.class);
private static final Pattern BLANK_LINE_RE = Pattern
.compile("^[ \\t]*(|[{}]|/\\*\\*?|\\*)[ \\t]*$");
@@ -62,7 +61,7 @@
}
@Override
- public IntraLineDiff createEntry(IntraLineDiffKey key) throws Exception {
+ public IntraLineDiff load(IntraLineDiffKey key) throws Exception {
Worker w = workerPool.poll();
if (w == null) {
w = new Worker();
@@ -119,7 +118,7 @@
throws Exception {
if (!input.offer(new Input(key))) {
log.error("Cannot enqueue task to thread " + thread.getName());
- return null;
+ return Result.TIMEOUT;
}
Result r = result.poll(timeoutMillis, TimeUnit.MILLISECONDS);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWeigher.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWeigher.java
new file mode 100644
index 0000000..f6cff15
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWeigher.java
@@ -0,0 +1,28 @@
+// 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.patch;
+
+import com.google.common.cache.Weigher;
+
+/** Approximates memory usage for IntralineDiff in bytes of memory used. */
+public class IntraLineWeigher implements
+ Weigher<IntraLineDiffKey, IntraLineDiff> {
+ @Override
+ public int weigh(IntraLineDiffKey key, IntraLineDiff value) {
+ return 16 + 8*8 + 2*36 // Size of IntraLineDiffKey, 64 bit JVM
+ + 16 + 2*8 + 16+8+4+20 // Size of IntraLineDiff, 64 bit JVM
+ + (8 + 16 + 4*4) * value.getEdits().size();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
index 8a61d30..fe77f5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -19,9 +19,10 @@
/** Provides a cached list of {@link PatchListEntry}. */
public interface PatchListCache {
- public PatchList get(PatchListKey key);
+ public PatchList get(PatchListKey key) throws PatchListNotAvailableException;
- public PatchList get(Change change, PatchSet patchSet);
+ public PatchList get(Change change, PatchSet patchSet)
+ throws PatchListNotAvailableException;
public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 26dbe2d..967e6a7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -15,24 +15,23 @@
package com.google.gerrit.server.patch;
-
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EvictionPolicy;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
+import java.util.concurrent.ExecutionException;
+
/** Provides a cached list of {@link PatchListEntry}. */
@Singleton
public class PatchListCacheImpl implements PatchListCache {
@@ -43,21 +42,15 @@
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<PatchListKey, PatchList>> fileType =
- new TypeLiteral<Cache<PatchListKey, PatchList>>() {};
- disk(fileType, FILE_NAME) //
- .memoryLimit(128) // very large items, cache only a few
- .evictionPolicy(EvictionPolicy.LRU) // prefer most recent
- .populateWith(PatchListLoader.class) //
- ;
+ persist(FILE_NAME, PatchListKey.class, PatchList.class)
+ .maximumWeight(10 << 20)
+ .loader(PatchListLoader.class)
+ .weigher(PatchListWeigher.class);
- final TypeLiteral<Cache<IntraLineDiffKey, IntraLineDiff>> intraType =
- new TypeLiteral<Cache<IntraLineDiffKey, IntraLineDiff>>() {};
- disk(intraType, INTRA_NAME) //
- .memoryLimit(128) // very large items, cache only a few
- .evictionPolicy(EvictionPolicy.LRU) // prefer most recent
- .populateWith(IntraLineLoader.class) //
- ;
+ persist(INTRA_NAME, IntraLineDiffKey.class, IntraLineDiff.class)
+ .maximumWeight(10 << 20)
+ .loader(IntraLineLoader.class)
+ .weigher(IntraLineWeigher.class);
bind(PatchListCacheImpl.class);
bind(PatchListCache.class).to(PatchListCacheImpl.class);
@@ -65,14 +58,14 @@
};
}
- private final Cache<PatchListKey, PatchList> fileCache;
- private final Cache<IntraLineDiffKey, IntraLineDiff> intraCache;
+ private final LoadingCache<PatchListKey, PatchList> fileCache;
+ private final LoadingCache<IntraLineDiffKey, IntraLineDiff> intraCache;
private final boolean computeIntraline;
@Inject
PatchListCacheImpl(
- @Named(FILE_NAME) final Cache<PatchListKey, PatchList> fileCache,
- @Named(INTRA_NAME) final Cache<IntraLineDiffKey, IntraLineDiff> intraCache,
+ @Named(FILE_NAME) LoadingCache<PatchListKey, PatchList> fileCache,
+ @Named(INTRA_NAME) LoadingCache<IntraLineDiffKey, IntraLineDiff> intraCache,
@GerritServerConfig Config cfg) {
this.fileCache = fileCache;
this.intraCache = intraCache;
@@ -82,11 +75,19 @@
cfg.getBoolean("cache", "diff", "intraline", true));
}
- public PatchList get(final PatchListKey key) {
- return fileCache.get(key);
+ @Override
+ public PatchList get(PatchListKey key) throws PatchListNotAvailableException {
+ try {
+ return fileCache.get(key);
+ } catch (ExecutionException e) {
+ PatchListLoader.log.warn("Error computing " + key, e);
+ throw new PatchListNotAvailableException(e.getCause());
+ }
}
- public PatchList get(final Change change, final PatchSet patchSet) {
+ @Override
+ public PatchList get(final Change change, final PatchSet patchSet)
+ throws PatchListNotAvailableException {
final Project.NameKey projectKey = change.getProject();
final ObjectId a = null;
final ObjectId b = ObjectId.fromString(patchSet.getRevision().get());
@@ -97,11 +98,12 @@
@Override
public IntraLineDiff getIntraLineDiff(IntraLineDiffKey key) {
if (computeIntraline) {
- IntraLineDiff d = intraCache.get(key);
- if (d == null) {
- d = new IntraLineDiff(IntraLineDiff.Status.ERROR);
+ try {
+ return intraCache.get(key);
+ } catch (ExecutionException e) {
+ IntraLineLoader.log.warn("Error computing " + key, e);
+ return new IntraLineDiff(IntraLineDiff.Status.ERROR);
}
- return d;
} else {
return new IntraLineDiff(IntraLineDiff.Status.DISABLED);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
index 33ed54e..ff9e6cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -122,6 +122,22 @@
this.deletions = deletions;
}
+ int weigh() {
+ int size = 16 + 6*8 + 2*4 + 20 + 16+8+4+20;
+ size += stringSize(oldName);
+ size += stringSize(newName);
+ size += header.length;
+ size += (8 + 16 + 4*4) * edits.size();
+ return size;
+ }
+
+ private static int stringSize(String str) {
+ if (str != null) {
+ return 16 + 3*4 + 16 + str.length() * 2;
+ }
+ return 0;
+ }
+
public ChangeType getChangeType() {
return changeType;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 5bba42b..d6e84bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -15,9 +15,9 @@
package com.google.gerrit.server.patch;
-import com.google.gerrit.reviewdb.client.Patch;
+import com.google.common.cache.CacheLoader;
import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
@@ -54,6 +54,8 @@
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.io.DisabledOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
@@ -62,7 +64,9 @@
import java.util.List;
import java.util.Map;
-class PatchListLoader extends EntryCreator<PatchListKey, PatchList> {
+class PatchListLoader extends CacheLoader<PatchListKey, PatchList> {
+ static final Logger log = LoggerFactory.getLogger(PatchListLoader.class);
+
private final GitRepositoryManager repoManager;
@Inject
@@ -71,7 +75,7 @@
}
@Override
- public PatchList createEntry(final PatchListKey key) throws Exception {
+ public PatchList load(final PatchListKey key) throws Exception {
final Repository repo = repoManager.openRepository(key.projectKey);
try {
return readPatchList(key, repo);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java
new file mode 100644
index 0000000..2ccc9f1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.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.patch;
+
+public class PatchListNotAvailableException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public PatchListNotAvailableException(String message) {
+ super(message);
+ }
+
+ public PatchListNotAvailableException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java
new file mode 100644
index 0000000..d715246
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java
@@ -0,0 +1,30 @@
+// 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.patch;
+
+import com.google.common.cache.Weigher;
+
+/** Approximates memory usage for PatchList in bytes of memory used. */
+public class PatchListWeigher implements Weigher<PatchListKey, PatchList> {
+ @Override
+ public int weigh(PatchListKey key, PatchList value) {
+ int size = 16 + 4*8 + 2*36 // Size of PatchListKey, 64 bit JVM
+ + 16 + 3*8 + 3*4 + 20; // Size of PatchList, 64 bit JVM
+ for (PatchListEntry e : value.getPatches()) {
+ size += e.weigh();
+ }
+ return size;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index f59dcc3..8165bf2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -18,7 +18,6 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.client.UserIdentity;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -68,31 +67,39 @@
}
public PatchSetInfo get(ReviewDb db, PatchSet.Id patchSetId)
- throws PatchSetInfoNotAvailableException {
- Repository repo = null;
+ throws PatchSetInfoNotAvailableException {
try {
final PatchSet patchSet = db.patchSets().get(patchSetId);
final Change change = db.changes().get(patchSet.getId().getParentKey());
- final Project.NameKey projectKey = change.getProject();
- repo = repoManager.openRepository(projectKey);
+ return get(change, patchSet);
+ } catch (OrmException e) {
+ throw new PatchSetInfoNotAvailableException(e);
+ }
+ }
+
+ public PatchSetInfo get(Change change, PatchSet patchSet)
+ throws PatchSetInfoNotAvailableException {
+ Repository repo;
+ try {
+ repo = repoManager.openRepository(change.getProject());
+ } catch (IOException e) {
+ throw new PatchSetInfoNotAvailableException(e);
+ }
+ try {
final RevWalk rw = new RevWalk(repo);
try {
final RevCommit src =
rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
- PatchSetInfo info = get(src, patchSetId);
+ PatchSetInfo info = get(src, patchSet.getId());
info.setParents(toParentInfos(src.getParents(), rw));
return info;
} finally {
rw.release();
}
- } catch (OrmException e) {
- throw new PatchSetInfoNotAvailableException(e);
} catch (IOException e) {
throw new PatchSetInfoNotAvailableException(e);
} finally {
- if (repo != null) {
- repo.close();
- }
+ repo.close();
}
}
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..e8af060
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -0,0 +1,393 @@
+// 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.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+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 Multimap<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 = LinkedListMultimap.create();
+
+ 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.entries()) {
+ @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..7018a3b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
@@ -0,0 +1,47 @@
+// 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());
+ } else {
+ PluginLoader.log.info("Cleaned plugin " + tmpFile.getName());
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
new file mode 100644
index 0000000..f34826d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.File;
+
+/**
+ * Copies critical objects from the {@code dbInjector} into a plugin.
+ * <p>
+ * Most explicit bindings are copied automatically from the cfgInjector and
+ * sysInjector to be made available to a plugin's private world. This module is
+ * necessary to get things bound in the dbInjector that are not otherwise easily
+ * available, but that a plugin author might expect to exist.
+ */
+@Singleton
+class CopyConfigModule extends AbstractModule {
+ @Inject
+ @SitePath
+ private File sitePath;
+
+ @Provides
+ @SitePath
+ File getSitePath() {
+ return sitePath;
+ }
+
+ @Inject
+ private SitePaths sitePaths;
+
+ @Provides
+ SitePaths getSitePaths() {
+ return sitePaths;
+ }
+
+ @Inject
+ private TrackingFooters trackingFooters;
+
+ @Provides
+ TrackingFooters getTrackingFooters() {
+ return trackingFooters;
+ }
+
+ @Inject
+ @GerritServerConfig
+ private Config gerritServerConfig;
+
+ @Provides
+ @GerritServerConfig
+ Config getGerritServerConfig() {
+ return gerritServerConfig;
+ }
+
+ @Inject
+ private SchemaFactory<ReviewDb> schemaFactory;
+
+ @Provides
+ SchemaFactory<ReviewDb> getSchemaFactory() {
+ return schemaFactory;
+ }
+
+ @Inject
+ private GitRepositoryManager gitRepositoryManager;
+
+ @Provides
+ GitRepositoryManager getGitRepositoryManager() {
+ return gitRepositoryManager;
+ }
+
+ @Inject
+ CopyConfigModule() {
+ }
+
+ @Override
+ protected void configure() {
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java
new file mode 100644
index 0000000..31be10c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+public class InvalidPluginException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidPluginException(String message) {
+ super(message);
+ }
+
+ public InvalidPluginException(String message, Throwable why) {
+ super(message, why);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
new file mode 100644
index 0000000..92e3b1d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.inject.Module;
+
+public interface ModuleGenerator {
+ void setPluginName(String name);
+
+ void export(Export export, Class<?> type) throws InvalidPluginException;
+
+ Module create() throws InvalidPluginException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
new file mode 100644
index 0000000..a3c363b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -0,0 +1,317 @@
+// 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.PluginData;
+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.extensions.systemstatus.ServerInformation;
+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 com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+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 {
+ public static enum ApiType {
+ EXTENSION, PLUGIN;
+ }
+
+ /** Unique key that changes whenever a plugin reloads. */
+ public static final class CacheKey {
+ private final String name;
+
+ CacheKey(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ int id = System.identityHashCode(this);
+ return String.format("Plugin[%s@%x]", name, id);
+ }
+ }
+
+ 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);
+ }
+
+ static ApiType getApiType(Manifest manifest) throws InvalidPluginException {
+ Attributes main = manifest.getMainAttributes();
+ String v = main.getValue("Gerrit-ApiType");
+ if (Strings.isNullOrEmpty(v)
+ || ApiType.EXTENSION.name().equalsIgnoreCase(v)) {
+ return ApiType.EXTENSION;
+ } else if (ApiType.PLUGIN.name().equalsIgnoreCase(v)) {
+ return ApiType.PLUGIN;
+ } else {
+ throw new InvalidPluginException("Invalid Gerrit-ApiType: " + v);
+ }
+ }
+
+ private final CacheKey cacheKey;
+ private final String name;
+ private final File srcJar;
+ private final FileSnapshot snapshot;
+ private final JarFile jarFile;
+ private final Manifest manifest;
+ private final File dataDir;
+ private final ApiType apiType;
+ 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,
+ File dataDir,
+ ApiType apiType,
+ ClassLoader classLoader,
+ @Nullable Class<? extends Module> sysModule,
+ @Nullable Class<? extends Module> sshModule,
+ @Nullable Class<? extends Module> httpModule) {
+ this.cacheKey = new CacheKey(name);
+ this.name = name;
+ this.srcJar = srcJar;
+ this.snapshot = snapshot;
+ this.jarFile = jarFile;
+ this.manifest = manifest;
+ this.dataDir = dataDir;
+ this.apiType = apiType;
+ this.classLoader = classLoader;
+ this.sysModule = sysModule;
+ this.sshModule = sshModule;
+ this.httpModule = httpModule;
+ }
+
+ File getSrcJar() {
+ return srcJar;
+ }
+
+ public CacheKey getCacheKey() {
+ return cacheKey;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Nullable
+ public String getVersion() {
+ Attributes main = manifest.getMainAttributes();
+ return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+ }
+
+ public ApiType getApiType() {
+ return apiType;
+ }
+
+ 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()) {
+ List<Module> modules = Lists.newLinkedList();
+ if (apiType == ApiType.PLUGIN) {
+ modules.add(env.getSshModule());
+ }
+ if (sshModule != null) {
+ modules.add(sysInjector.getInstance(sshModule));
+ sshInjector = sysInjector.createChildInjector(modules);
+ manager.add(sshInjector);
+ } else if (auto != null && auto.sshModule != null) {
+ modules.add(auto.sshModule);
+ sshInjector = sysInjector.createChildInjector(modules);
+ manager.add(sshInjector);
+ }
+ }
+
+ if (env.hasHttpModule()) {
+ List<Module> modules = Lists.newLinkedList();
+ if (apiType == ApiType.PLUGIN) {
+ modules.add(env.getHttpModule());
+ }
+ if (httpModule != null) {
+ modules.add(sysInjector.getInstance(httpModule));
+ httpInjector = sysInjector.createChildInjector(modules);
+ manager.add(httpInjector);
+ } else if (auto != null && auto.httpModule != null) {
+ modules.add(auto.httpModule);
+ httpInjector = sysInjector.createChildInjector(modules);
+ manager.add(httpInjector);
+ }
+ }
+
+ manager.start();
+ }
+
+ private Injector newRootInjector(final PluginGuiceEnvironment env) {
+ List<Module> modules = Lists.newArrayListWithCapacity(4);
+ modules.add(env.getSysModule());
+ if (apiType == ApiType.PLUGIN) {
+ modules.add(env.getSysModule());
+ } else {
+ modules.add(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(ServerInformation.class).toInstance(env.getServerInformation());
+ }
+ });
+ }
+ modules.add(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(String.class)
+ .annotatedWith(PluginName.class)
+ .toInstance(name);
+
+ bind(File.class)
+ .annotatedWith(PluginData.class)
+ .toProvider(new Provider<File>() {
+ private volatile boolean ready;
+
+ @Override
+ public File get() {
+ if (!ready) {
+ synchronized (dataDir) {
+ if (!dataDir.exists() && !dataDir.mkdirs()) {
+ throw new ProvisionException(String.format(
+ "Cannot create %s for plugin %s",
+ dataDir.getAbsolutePath(), name));
+ }
+ ready = true;
+ }
+ }
+ return dataDir;
+ }
+ });
+ }
+ });
+ return Guice.createInjector(modules);
+ }
+
+ 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(RegistrationHandle handle) {
+ if (handle instanceof ReloadableRegistrationHandle) {
+ if (reloadableHandles == null) {
+ reloadableHandles = Lists.newArrayList();
+ }
+ reloadableHandles.add((ReloadableRegistrationHandle<?>) handle);
+ }
+ manager.add(handle);
+ }
+
+ 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/PluginCleanerTask.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
new file mode 100644
index 0000000..7081d70
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
@@ -0,0 +1,100 @@
+// 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.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+class PluginCleanerTask implements Runnable {
+ private final WorkQueue workQueue;
+ private final PluginLoader loader;
+ private volatile int pending;
+ private Future<?> self;
+ private int attempts;
+ private long start;
+
+ @Inject
+ PluginCleanerTask(WorkQueue workQueue, PluginLoader loader) {
+ this.workQueue = workQueue;
+ this.loader = loader;
+ }
+
+ @Override
+ public void run() {
+ try {
+ for (int t = 0; t < 2 * (attempts + 1); t++) {
+ System.gc();
+ Thread.sleep(50);
+ }
+ } catch (InterruptedException e) {
+ }
+
+ int left = loader.processPendingCleanups();
+ synchronized (this) {
+ pending = left;
+ self = null;
+
+ if (0 < left) {
+ long waiting = System.currentTimeMillis() - start;
+ PluginLoader.log.warn(String.format(
+ "%d plugins still waiting to be reclaimed after %d minutes",
+ pending,
+ TimeUnit.MILLISECONDS.toMinutes(waiting)));
+ attempts = Math.min(attempts + 1, 15);
+ ensureScheduled();
+ } else {
+ attempts = 0;
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ int p = pending;
+ if (0 < p) {
+ return String.format("Plugin Cleaner (waiting for %d plugins)", p);
+ }
+ return "Plugin Cleaner";
+ }
+
+ synchronized void clean(int expect) {
+ if (self == null && pending == 0) {
+ start = System.currentTimeMillis();
+ }
+ pending = expect;
+ ensureScheduled();
+ }
+
+ private void ensureScheduled() {
+ if (self == null && 0 < pending) {
+ if (attempts == 1) {
+ self = workQueue.getDefaultQueue().schedule(
+ this,
+ 30,
+ TimeUnit.SECONDS);
+ } else {
+ self = workQueue.getDefaultQueue().schedule(
+ this,
+ attempts + 1,
+ TimeUnit.MINUTES);
+ }
+ }
+ }
+}
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..f16131c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -0,0 +1,488 @@
+// 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.extensions.registration.PrivateInternals_DynamicTypes.dynamicMapsOf;
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicSetsOf;
+
+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.events.LifecycleListener;
+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.PrivateInternals_DynamicTypes;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+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 ServerInformation srvInfo;
+ 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,
+ ServerInformation srvInfo,
+ CopyConfigModule ccm) {
+ this.sysInjector = sysInjector;
+ this.srvInfo = srvInfo;
+ 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);
+ }
+
+ ServerInformation getServerInformation() {
+ return srvInfo;
+ }
+
+ 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) {
+ for (RegistrationHandle h : PrivateInternals_DynamicTypes
+ .attachSets(src, sets)) {
+ plugin.add(h);
+ }
+ }
+
+ private void attachMap(Map<TypeLiteral<?>, DynamicMap<?>> maps,
+ @Nullable Injector src,
+ Plugin plugin) {
+ for (RegistrationHandle h : PrivateInternals_DynamicTypes
+ .attachMaps(src, plugin.getName(), maps)) {
+ plugin.add(h);
+ }
+ }
+
+ 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();
+ if (key.getAnnotation() == null) {
+ continue;
+ }
+
+ @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()));
+ }
+ }
+ }
+ }
+
+ /** 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();
+ if (key.getAnnotation() == null) {
+ continue;
+ }
+
+ @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()));
+ }
+ }
+ }
+ }
+
+ private static <T> void replace(Plugin newPlugin,
+ ReloadableRegistrationHandle<T> h, Binding<T> b) {
+ RegistrationHandle n = h.replace(b.getKey(), b.getProvider());
+ 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 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())
+ && e.getKey().getAnnotation() != null) {
+ // A type used in DynamicSet or DynamicMap that has an annotation
+ // must be picked up by the set/map itself. A type used in either
+ // but without an annotation may be magic glue implementing F and
+ // using DynamicSet<F> or DynamicMap<F> internally. That should be
+ // exported to plugins.
+ continue;
+ } else if (shouldCopy(e.getKey())) {
+ bindings.put(e.getKey(), e.getValue());
+ }
+ }
+ bindings.remove(Key.get(Injector.class));
+ bindings.remove(Key.get(java.util.logging.Logger.class));
+
+ return new AbstractModule() {
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void configure() {
+ for (Map.Entry<Key<?>, Binding<?>> e : bindings.entrySet()) {
+ Key<Object> k = (Key<Object>) e.getKey();
+ Binding<Object> b = (Binding<Object>) e.getValue();
+ bind(k).toProvider(b.getProvider());
+ }
+ }
+ };
+ }
+
+ private static boolean shouldCopy(Key<?> key) {
+ Class<?> type = key.getTypeLiteral().getRawType();
+ if (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/cache/EvictionPolicy.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.java
similarity index 65%
rename from gerrit-server/src/main/java/com/google/gerrit/server/cache/EvictionPolicy.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.java
index cff4f11..77fa702 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/EvictionPolicy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.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.
@@ -12,13 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.plugins;
-/** How entries should be evicted from the cache. */
-public enum EvictionPolicy {
- /** Least recently used is evicted first. */
- LRU,
+public class PluginInstallException extends Exception {
+ private static final long serialVersionUID = 1L;
- /** Least frequently used is evicted first. */
- LFU;
+ 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..67d715f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -0,0 +1,442 @@
+// 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.Joiner;
+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.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+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.Provider;
+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 dataDir;
+ private final File tmpDir;
+ private final PluginGuiceEnvironment env;
+ private final ServerInformationImpl srvInfoImpl;
+ private final ConcurrentMap<String, Plugin> running;
+ private final Map<String, FileSnapshot> broken;
+ private final ReferenceQueue<ClassLoader> cleanupQueue;
+ private final ConcurrentMap<CleanupHandle, Boolean> cleanupHandles;
+ private final Provider<PluginCleanerTask> cleaner;
+ private final PluginScannerThread scanner;
+
+ @Inject
+ public PluginLoader(SitePaths sitePaths,
+ PluginGuiceEnvironment pe,
+ ServerInformationImpl sii,
+ Provider<PluginCleanerTask> pct,
+ @GerritServerConfig Config cfg) {
+ pluginsDir = sitePaths.plugins_dir;
+ dataDir = sitePaths.data_dir;
+ tmpDir = sitePaths.tmp_dir;
+ env = pe;
+ srvInfoImpl = sii;
+ running = Maps.newConcurrentMap();
+ broken = Maps.newHashMap();
+ cleanupQueue = new ReferenceQueue<ClassLoader>();
+ cleanupHandles = Maps.newConcurrentMap();
+ cleaner = pct;
+
+ 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 Iterable<Plugin> getPlugins() {
+ return 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);
+ 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));
+ }
+ } catch (PluginInstallException e) {
+ jar.delete();
+ throw e;
+ }
+
+ cleanInBackground();
+ }
+ }
+
+ 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) {
+ 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);
+ }
+ cleanInBackground();
+ }
+ }
+
+ @Override
+ public synchronized void start() {
+ log.info("Loading plugins from " + pluginsDir.getAbsolutePath());
+ srvInfoImpl.state = ServerInformation.State.STARTUP;
+ rescan();
+ srvInfoImpl.state = ServerInformation.State.RUNNING;
+ if (scanner != null) {
+ scanner.start();
+ }
+ }
+
+ @Override
+ public void stop() {
+ if (scanner != null) {
+ scanner.end();
+ }
+ srvInfoImpl.state = ServerInformation.State.SHUTDOWN;
+ synchronized (this) {
+ for (Plugin p : running.values()) {
+ p.stop();
+ }
+ running.clear();
+ broken.clear();
+ if (cleanupHandles.size() > running.size()) {
+ System.gc();
+ processPendingCleanups();
+ }
+ }
+ }
+
+ public void reload(List<String> names)
+ throws InvalidPluginException, PluginInstallException {
+ synchronized (this) {
+ List<Plugin> reload = Lists.newArrayListWithCapacity(names.size());
+ List<String> bad = Lists.newArrayListWithExpectedSize(4);
+ for (String name : names) {
+ Plugin active = running.get(name);
+ if (active != null) {
+ reload.add(active);
+ } else {
+ bad.add(name);
+ }
+ }
+ if (!bad.isEmpty()) {
+ throw new InvalidPluginException(String.format(
+ "Plugin(s) \"%s\" not running",
+ Joiner.on("\", \"").join(bad)));
+ }
+
+ for (Plugin active : reload) {
+ String name = active.getName();
+ try {
+ log.info(String.format("Reloading plugin %s", name));
+ runPlugin(name, active.getSrcJar(), active);
+ } catch (PluginInstallException e) {
+ log.warn(String.format("Cannot reload plugin %s", name), e.getCause());
+ throw e;
+ }
+ }
+
+ cleanInBackground();
+ }
+ }
+
+ public synchronized void rescan() {
+ List<File> jars = scanJarsInPluginsDirectory();
+ stopRemovedPlugins(jars);
+
+ for (File jar : jars) {
+ String name = nameOf(jar);
+ FileSnapshot brokenTime = broken.get(name);
+ if (brokenTime != null && !brokenTime.isModified(jar)) {
+ continue;
+ }
+
+ Plugin active = running.get(name);
+ if (active != null && !active.isModified(jar)) {
+ continue;
+ }
+
+ if (active != null) {
+ log.info(String.format("Reloading plugin %s", name));
+ }
+
+ try {
+ runPlugin(name, jar, active);
+ if (active == null) {
+ log.info(String.format("Loaded plugin %s", name));
+ }
+ } catch (PluginInstallException e) {
+ log.warn(String.format("Cannot load plugin %s", name), e.getCause());
+ }
+ }
+
+ cleanInBackground();
+ }
+
+ 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 void stopRemovedPlugins(List<File> jars) {
+ Set<String> unload = Sets.newHashSet(running.keySet());
+ for (File jar : jars) {
+ unload.remove(nameOf(jar));
+ }
+ for (String name : unload){
+ log.info(String.format("Unloading plugin %s", name));
+ running.remove(name).stop();
+ }
+ }
+
+ synchronized int processPendingCleanups() {
+ CleanupHandle h;
+ while ((h = (CleanupHandle) cleanupQueue.poll()) != null) {
+ h.cleanup();
+ cleanupHandles.remove(h);
+ }
+ return Math.max(0, cleanupHandles.size() - running.size());
+ }
+
+ private void cleanInBackground() {
+ int cnt = Math.max(0, cleanupHandles.size() - running.size());
+ if (0 < cnt) {
+ cleaner.get().clean(cnt);
+ }
+ }
+
+ 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, InvalidPluginException {
+ 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();
+ Plugin.ApiType type = Plugin.getApiType(manifest);
+ Attributes main = manifest.getMainAttributes();
+ String sysName = main.getValue("Gerrit-Module");
+ String sshName = main.getValue("Gerrit-SshModule");
+ String httpName = main.getValue("Gerrit-HttpModule");
+
+ if (!Strings.isNullOrEmpty(sshName) && type != Plugin.ApiType.PLUGIN) {
+ throw new InvalidPluginException(String.format(
+ "Using Gerrit-SshModule requires Gerrit-ApiType: %s",
+ Plugin.ApiType.PLUGIN));
+ }
+
+ URL[] urls = {tmp.toURI().toURL()};
+ ClassLoader parentLoader = parentFor(type);
+ 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,
+ new File(dataDir, name), type, pluginLoader,
+ sysModule, sshModule, httpModule);
+ } finally {
+ if (!keep) {
+ jarFile.close();
+ }
+ }
+ }
+
+ private static ClassLoader parentFor(Plugin.ApiType type)
+ throws InvalidPluginException {
+ switch (type) {
+ case EXTENSION:
+ return PluginName.class.getClassLoader();
+ case PLUGIN:
+ return PluginLoader.class.getClassLoader();
+ default:
+ throw new InvalidPluginException("Unsupported ApiType " + type);
+ }
+ }
+
+ 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..ab7dc3c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
@@ -0,0 +1,33 @@
+// 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.systemstatus.ServerInformation;
+import com.google.gerrit.lifecycle.LifecycleModule;
+
+public class PluginModule extends LifecycleModule {
+ @Override
+ protected void configure() {
+ bind(ServerInformationImpl.class);
+ bind(ServerInformation.class).to(ServerInformationImpl.class);
+
+ bind(PluginCleanerTask.class);
+ 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..a484c5d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+class PluginScannerThread extends Thread {
+ private final CountDownLatch done = new CountDownLatch(1);
+ private final PluginLoader loader;
+ private final long checkFrequencyMillis;
+
+ PluginScannerThread(PluginLoader loader, long checkFrequencyMillis) {
+ this.loader = loader;
+ this.checkFrequencyMillis = checkFrequencyMillis;
+ setDaemon(true);
+ setName("PluginScanner");
+ }
+
+ @Override
+ public void run() {
+ for (;;) {
+ try {
+ if (done.await(checkFrequencyMillis, TimeUnit.MILLISECONDS)) {
+ return;
+ }
+ } catch (InterruptedException e) {
+ }
+ loader.rescan();
+ }
+ }
+
+ void end() {
+ done.countDown();
+ try {
+ join();
+ } catch (InterruptedException e) {
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java
similarity index 67%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java
index 3370b08..72a499e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 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.
@@ -12,8 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.plugins;
-public interface CachePool {
- public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+/** 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/ServerInformationImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerInformationImpl.java
new file mode 100644
index 0000000..c4a8900
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerInformationImpl.java
@@ -0,0 +1,28 @@
+// 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.systemstatus.ServerInformation;
+import com.google.inject.Singleton;
+
+@Singleton
+class ServerInformationImpl implements ServerInformation {
+ volatile State state = State.STARTUP;
+
+ @Override
+ public State getState() {
+ return state;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java
similarity index 69%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java
index 3370b08..aaad370 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 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.
@@ -12,8 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.plugins;
-public interface CachePool {
- public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+/** 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 d17762e..64f7d97 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
@@ -26,10 +26,11 @@
import com.google.gerrit.rules.StoredValues;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import com.google.inject.util.Providers;
import com.googlecode.prolog_cafe.compiler.CompileException;
import com.googlecode.prolog_cafe.lang.IntegerTerm;
@@ -49,6 +50,8 @@
import java.util.List;
import java.util.Set;
+import javax.annotation.Nullable;
+
/** Access control management for a user accessing a single change. */
public class ChangeControl {
@@ -161,7 +164,7 @@
/** Can this user see this change? */
public boolean isVisible(ReviewDb db) throws OrmException {
- if (change.getStatus() == Change.Status.DRAFT && !isDraftVisible(db)) {
+ if (change.getStatus() == Change.Status.DRAFT && !isDraftVisible(db, null)) {
return false;
}
return isRefVisible();
@@ -174,7 +177,7 @@
/** Can this user see the given patchset? */
public boolean isPatchVisible(PatchSet ps, ReviewDb db) throws OrmException {
- if (ps.isDraft() && !isDraftVisible(db)) {
+ if (ps.isDraft() && !isDraftVisible(db, null)) {
return false;
}
return isVisible(db);
@@ -236,10 +239,20 @@
/** Is this user a reviewer for the change? */
public boolean isReviewer(ReviewDb db) throws OrmException {
+ return isReviewer(db, null);
+ }
+
+ /** Is this user a reviewer for the change? */
+ public boolean isReviewer(ReviewDb db, @Nullable ChangeData cd)
+ throws OrmException {
if (getCurrentUser() instanceof IdentifiedUser) {
final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
- ResultSet<PatchSetApproval> results =
- db.patchSetApprovals().byChange(change.getId());
+ Iterable<PatchSetApproval> results;
+ if (cd != null) {
+ results = cd.currentApprovals(Providers.of(db));
+ } else {
+ results = db.patchSetApprovals().byChange(change.getId());
+ }
for (PatchSetApproval approval : results) {
if (user.getAccountId().equals(approval.getAccountId())) {
return true;
@@ -279,34 +292,43 @@
return false;
}
- public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet.Id patchSetId) {
- if (change.getStatus().isClosed()) {
+ public List<SubmitRecord> getSubmitRecords(ReviewDb db, PatchSet patchSet) {
+ return canSubmit(db, patchSet, null, false, true);
+ }
+
+ public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet) {
+ return canSubmit(db, patchSet, null, false, false);
+ }
+
+ public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet,
+ @Nullable ChangeData cd, boolean fastEvalLabels, boolean allowClosed) {
+ if (!allowClosed && change.getStatus().isClosed()) {
SubmitRecord rec = new SubmitRecord();
rec.status = SubmitRecord.Status.CLOSED;
return Collections.singletonList(rec);
}
- if (!patchSetId.equals(change.currentPatchSetId())) {
- return ruleError("Patch set " + patchSetId + " is not current");
+ if (!patchSet.getId().equals(change.currentPatchSetId())) {
+ return ruleError("Patch set " + patchSet.getPatchSetId() + " is not current");
}
try {
- if (change.getStatus() == Change.Status.DRAFT){
- if (!isVisible(db)) {
- return ruleError("Patch set " + patchSetId + " not found");
+ if (change.getStatus() == Change.Status.DRAFT) {
+ if (!isDraftVisible(db, cd)) {
+ return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
} else {
return ruleError("Cannot submit draft changes");
}
}
- if (isDraftPatchSet(patchSetId, db)) {
- if (!isVisible(db)) {
- return ruleError("Patch set " + patchSetId + " not found");
+ if (patchSet.isDraft()) {
+ if (!isDraftVisible(db, cd)) {
+ return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
} else {
return ruleError("Cannot submit draft patch sets");
}
}
} catch (OrmException err) {
- return logRuleError("Cannot read patch set " + patchSetId, err);
+ return logRuleError("Cannot read patch set " + patchSet.getId(), err);
}
List<Term> results = new ArrayList<Term>();
@@ -324,7 +346,8 @@
try {
env.set(StoredValues.REVIEW_DB, db);
env.set(StoredValues.CHANGE, change);
- env.set(StoredValues.PATCH_SET_ID, patchSetId);
+ env.set(StoredValues.CHANGE_DATA, cd);
+ env.set(StoredValues.PATCH_SET, patchSet);
env.set(StoredValues.CHANGE_CONTROL, this);
submitRule = env.once(
@@ -335,6 +358,10 @@
+ getProject().getName());
}
+ if (fastEvalLabels) {
+ env.once("gerrit", "assume_range_from_label");
+ }
+
try {
for (Term[] template : env.all(
"gerrit", "can_submit",
@@ -373,6 +400,10 @@
parentEnv.once("gerrit", "locate_submit_filter", new VariableTerm());
if (filterRule != null) {
try {
+ if (fastEvalLabels) {
+ env.once("gerrit", "assume_range_from_label");
+ }
+
Term resultsTerm = toListTerm(results);
results.clear();
Term[] template = parentEnv.once(
@@ -468,6 +499,9 @@
} else if ("need".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.NEED;
+ } else if ("may".equals(status.name())) {
+ lbl.status = SubmitRecord.Label.Status.MAY;
+
} else if ("impossible".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
@@ -516,16 +550,9 @@
}
}
- private boolean isDraftVisible(ReviewDb db) throws OrmException {
- return isOwner() || isReviewer(db);
- }
-
- private boolean isDraftPatchSet(PatchSet.Id id, ReviewDb db) throws OrmException {
- PatchSet ps = db.patchSets().get(id);
- if (ps == null) {
- throw new OrmException("Patch set " + id + " not found");
- }
- return ps.isDraft();
+ private boolean isDraftVisible(ReviewDb db, ChangeData cd)
+ throws OrmException {
+ return isOwner() || isReviewer(db, cd);
}
private static boolean isUser(Term who) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 65bdc1e..879f772 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -15,21 +15,24 @@
package com.google.gerrit.server.project;
import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.errors.ProjectCreationFailedException;
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.config.ProjectOwnerGroups;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.ReplicationQueue;
import com.google.gerrit.server.git.RepositoryCaseMismatchException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -49,6 +52,8 @@
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.Set;
@@ -64,28 +69,32 @@
private final Set<AccountGroup.UUID> projectOwnerGroups;
private final IdentifiedUser currentUser;
private final GitRepositoryManager repoManager;
- private final ReplicationQueue replication;
+ private final GitReferenceUpdated referenceUpdated;
+ private final DynamicSet<NewProjectCreatedListener> createdListener;
private final PersonIdent serverIdent;
private CreateProjectArgs createProjectArgs;
private ProjectCache projectCache;
- private GroupCache groupCache;
+ private GroupBackend groupBackend;
private MetaDataUpdate.User metaDataUpdateFactory;
@Inject
CreateProject(@ProjectOwnerGroups Set<AccountGroup.UUID> pOwnerGroups,
IdentifiedUser identifiedUser, GitRepositoryManager gitRepoManager,
- ReplicationQueue replicateq, ReviewDb db,
- @GerritPersonIdent PersonIdent personIdent, final GroupCache groupCache,
- final MetaDataUpdate.User metaDataUpdateFactory,
+ GitReferenceUpdated referenceUpdated,
+ DynamicSet<NewProjectCreatedListener> createdListener,
+ ReviewDb db,
+ @GerritPersonIdent PersonIdent personIdent, GroupBackend groupBackend,
+ MetaDataUpdate.User metaDataUpdateFactory,
@Assisted CreateProjectArgs createPArgs, ProjectCache pCache) {
this.projectOwnerGroups = pOwnerGroups;
this.currentUser = identifiedUser;
this.repoManager = gitRepoManager;
- this.replication = replicateq;
+ this.referenceUpdated = referenceUpdated;
+ this.createdListener = createdListener;
this.serverIdent = personIdent;
this.createProjectArgs = createPArgs;
this.projectCache = pCache;
- this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
this.metaDataUpdateFactory = metaDataUpdateFactory;
}
@@ -95,10 +104,23 @@
try {
final String head =
createProjectArgs.permissionsOnly ? GitRepositoryManager.REF_CONFIG
- : createProjectArgs.branch;
+ : createProjectArgs.branch.get(0);
final Repository repo = repoManager.createRepository(nameKey);
try {
- replication.replicateNewProject(nameKey, head);
+ NewProjectCreatedListener.Event event = new NewProjectCreatedListener.Event() {
+ @Override
+ public String getProjectName() {
+ return nameKey.get();
+ }
+
+ @Override
+ public String getHeadName() {
+ return head;
+ }
+ };
+ for (NewProjectCreatedListener l : createdListener) {
+ l.onNewProjectCreated(event);
+ }
final RefUpdate u = repo.updateRef(Constants.HEAD);
u.disableRefLog();
@@ -108,7 +130,7 @@
if (!createProjectArgs.permissionsOnly
&& createProjectArgs.createEmptyCommit) {
- createEmptyCommit(repo, nameKey, createProjectArgs.branch);
+ createEmptyCommits(repo, nameKey, createProjectArgs.branch);
}
} finally {
repo.close();
@@ -130,10 +152,10 @@
} finally {
repo.close();
}
- } catch (RepositoryNotFoundException doesNotExist) {
+ } catch (IOException ioErr) {
final String msg = "Cannot create " + nameKey;
log.error(msg, err);
- throw new ProjectCreationFailedException(msg, err);
+ throw new ProjectCreationFailedException(msg, ioErr);
}
} catch (Exception e) {
final String msg = "Cannot create " + nameKey;
@@ -166,9 +188,9 @@
final AccessSection all =
config.getAccessSection(AccessSection.ALL, true);
for (AccountGroup.UUID ownerId : createProjectArgs.ownerIds) {
- AccountGroup accountGroup = groupCache.get(ownerId);
- if (accountGroup != null) {
- GroupReference group = config.resolve(accountGroup);
+ GroupDescription.Basic g = groupBackend.get(ownerId);
+ if (g != null) {
+ GroupReference group = config.resolve(GroupReference.forGroup(g));
all.getPermission(Permission.OWNER, true).add(
new PermissionRule(group));
}
@@ -186,7 +208,7 @@
projectCache.onCreateProject(createProjectArgs.getProject());
repoManager.setProjectDescription(createProjectArgs.getProject(),
createProjectArgs.projectDescription);
- replication.scheduleUpdate(createProjectArgs.getProject(),
+ referenceUpdated.fire(createProjectArgs.getProject(),
GitRepositoryManager.REF_CONFIG);
}
@@ -216,20 +238,32 @@
new ArrayList<AccountGroup.UUID>(projectOwnerGroups);
}
- while (createProjectArgs.branch.startsWith("/")) {
- createProjectArgs.branch = createProjectArgs.branch.substring(1);
+ List<String> transformedBranches = new ArrayList<String>();
+ if (createProjectArgs.branch == null ||
+ createProjectArgs.branch.isEmpty()) {
+ createProjectArgs.branch = Collections.singletonList(Constants.MASTER);
}
- if (!createProjectArgs.branch.startsWith(Constants.R_HEADS)) {
- createProjectArgs.branch = Constants.R_HEADS + createProjectArgs.branch;
+ for (String branch : createProjectArgs.branch) {
+ while (branch.startsWith("/")) {
+ branch = branch.substring(1);
+ }
+ if (!branch.startsWith(Constants.R_HEADS)) {
+ branch = Constants.R_HEADS + branch;
+ }
+ if (!Repository.isValidRefName(branch)) {
+ throw new ProjectCreationFailedException(String.format(
+ "Branch \"%s\" is not a valid name.", branch));
+ }
+ if (!transformedBranches.contains(branch)) {
+ transformedBranches.add(branch);
+ }
}
- if (!Repository.isValidRefName(createProjectArgs.branch)) {
- throw new ProjectCreationFailedException(String.format(
- "Branch \"%s\" is not a valid name.", createProjectArgs.branch));
- }
+ createProjectArgs.branch = transformedBranches;
}
- private void createEmptyCommit(final Repository repo,
- final Project.NameKey project, final String ref) throws IOException {
+ private void createEmptyCommits(final Repository repo,
+ final Project.NameKey project, final List<String> refs)
+ throws IOException {
ObjectInserter oi = repo.newObjectInserter();
try {
CommitBuilder cb = new CommitBuilder();
@@ -241,15 +275,18 @@
ObjectId id = oi.insert(cb);
oi.flush();
- RefUpdate ru = repo.updateRef(Constants.HEAD);
- ru.setNewObjectId(id);
- final Result result = ru.update();
- switch (result) {
- case NEW:
- replication.scheduleUpdate(project, ref);
- break;
- default: {
- throw new IOException(result.name());
+ for (String ref : refs) {
+ RefUpdate ru = repo.updateRef(ref);
+ ru.setNewObjectId(id);
+ final Result result = ru.update();
+ switch (result) {
+ case NEW:
+ referenceUpdated.fire(project, ref);
+ break;
+ default: {
+ throw new IOException(String.format(
+ "Failed to create ref \"%s\": %s", ref, result.name()));
+ }
}
}
} catch (IOException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
index 98adf85..2dee4f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -30,7 +30,7 @@
public boolean contributorAgreements;
public boolean signedOffBy;
public boolean permissionsOnly;
- public String branch;
+ public List<String> branch;
public boolean contentMerge;
public boolean changeIdRequired;
public boolean createEmptyCommit;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index 95a8b77..716a5a8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -14,10 +14,14 @@
package com.google.gerrit.server.project;
+import com.google.common.collect.Maps;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.util.TreeFormatter;
+import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -36,6 +40,7 @@
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
@@ -75,6 +80,9 @@
private final GitRepositoryManager repoManager;
private final ProjectNode.Factory projectNodeFactory;
+ @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+ private OutputFormat format = OutputFormat.TEXT;
+
@Option(name = "--show-branch", aliases = {"-b"}, multiValued = true,
usage = "displays the sha of each project in the specified branch")
private List<String> showBranch;
@@ -93,6 +101,11 @@
@Option(name = "--all", usage = "display all projects that are accessible by the calling user")
private boolean all;
+ @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of projects to list")
+ private int limit;
+
+ private String matchPrefix;
+
@Inject
protected ListProjects(CurrentUser currentUser, ProjectCache projectCache,
GitRepositoryManager repoManager,
@@ -115,6 +128,20 @@
return showDescription;
}
+ public OutputFormat getFormat() {
+ return format;
+ }
+
+ public ListProjects setFormat(OutputFormat fmt) {
+ this.format = fmt;
+ return this;
+ }
+
+ public ListProjects setMatchPrefix(String prefix) {
+ this.matchPrefix = prefix;
+ return this;
+ }
+
public void display(OutputStream out) {
final PrintWriter stdout;
try {
@@ -124,10 +151,14 @@
throw new RuntimeException("JVM lacks UTF-8 encoding", e);
}
+ int found = 0;
+ Map<String, ProjectInfo> output = Maps.newTreeMap();
+ Map<String, String> hiddenNames = Maps.newHashMap();
+
final TreeMap<Project.NameKey, ProjectNode> treeMap =
new TreeMap<Project.NameKey, ProjectNode>();
try {
- for (final Project.NameKey projectName : projectCache.all()) {
+ for (final Project.NameKey projectName : scan()) {
final ProjectState e = projectCache.get(projectName);
if (e == null) {
// If we can't get it from the cache, pretend its not present.
@@ -137,18 +168,39 @@
final ProjectControl pctl = e.controlFor(currentUser);
final boolean isVisible = pctl.isVisible() || (all && pctl.isOwner());
- if (showTree) {
+ if (showTree && !format.isJson()) {
treeMap.put(projectName,
projectNodeFactory.create(pctl.getProject(), isVisible));
continue;
}
- if (!isVisible) {
+ if (!isVisible && !(showTree && pctl.isOwner())) {
// Require the project itself to be visible to the user.
//
continue;
}
+ ProjectInfo info = new ProjectInfo();
+ info.name = projectName.get();
+ if (showTree && format.isJson()) {
+ ProjectState parent = e.getParentState();
+ if (parent != null) {
+ ProjectControl parentCtrl = parent.controlFor(currentUser);
+ if (parentCtrl.isVisible() || parentCtrl.isOwner()) {
+ info.parent = parent.getProject().getName();
+ } else {
+ info.parent = hiddenNames.get(parent.getProject().getName());
+ if (info.parent == null) {
+ info.parent = "?-" + (hiddenNames.size() + 1);
+ hiddenNames.put(parent.getProject().getName(), info.parent);
+ }
+ }
+ }
+ }
+ if (showDescription && !e.getProject().getDescription().isEmpty()) {
+ info.description = e.getProject().getDescription();
+ }
+
try {
if (showBranch != null) {
Repository git = repoManager.openRepository(projectName);
@@ -162,20 +214,19 @@
continue;
}
- for (Ref ref : refs) {
- if (ref == null) {
- // Print stub (forty '-' symbols)
- stdout.print("----------------------------------------");
- } else {
- stdout.print(ref.getObjectId().name());
+ for (int i = 0; i < showBranch.size(); i++) {
+ Ref ref = refs.get(i);
+ if (ref != null && ref.getObjectId() != null) {
+ if (info.branches == null) {
+ info.branches = Maps.newLinkedHashMap();
+ }
+ info.branches.put(showBranch.get(i), ref.getObjectId().name());
}
- stdout.print(' ');
}
} finally {
git.close();
}
-
- } else if (type != FilterType.ALL) {
+ } else if (!showTree && type != FilterType.ALL) {
Repository git = repoManager.openRepository(projectName);
try {
if (!type.matches(git)) {
@@ -194,18 +245,40 @@
continue;
}
- stdout.print(projectName.get());
-
- String desc;
- if (showDescription && !(desc = e.getProject().getDescription()).isEmpty()) {
- // We still want to list every project as one-liners, hence escaping \n.
- stdout.print(" - " + desc.replace("\n", "\\n"));
+ if (limit > 0 && ++found > limit) {
+ break;
}
- stdout.print("\n");
+ if (format.isJson()) {
+ output.put(info.name, info);
+ continue;
+ }
+
+ if (showBranch != null) {
+ for (String name : showBranch) {
+ String ref = info.branches != null ? info.branches.get(name) : null;
+ if (ref == null) {
+ // Print stub (forty '-' symbols)
+ ref = "----------------------------------------";
+ }
+ stdout.print(ref);
+ stdout.print(' ');
+ }
+ }
+ stdout.print(info.name);
+
+ if (info.description != null) {
+ // We still want to list every project as one-liners, hence escaping \n.
+ stdout.print(" - " + info.description.replace("\n", "\\n"));
+ }
+ stdout.print('\n');
}
- if (showTree && treeMap.size() > 0) {
+ if (format.isJson()) {
+ format.newGson().toJson(
+ output, new TypeToken<Map<String, ProjectInfo>>() {}.getType(), stdout);
+ stdout.print('\n');
+ } else if (showTree && treeMap.size() > 0) {
printProjectTree(stdout, treeMap);
}
} finally {
@@ -213,6 +286,14 @@
}
}
+ private Iterable<NameKey> scan() {
+ if (matchPrefix != null) {
+ return projectCache.byName(matchPrefix);
+ } else {
+ return projectCache.all();
+ }
+ }
+
private void printProjectTree(final PrintWriter stdout,
final TreeMap<Project.NameKey, ProjectNode> treeMap) {
final SortedSet<ProjectNode> sortedNodes = new TreeSet<ProjectNode>();
@@ -270,4 +351,11 @@
}
return false;
}
+
+ private static class ProjectInfo {
+ transient String name;
+ String parent;
+ String description;
+ Map<String, String> branches;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
index 70f4013..7a111311 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
@@ -32,6 +32,12 @@
/** Invalidate the cached information about the given project. */
public void evict(Project p);
+ /**
+ * Remove information about the given project from the cache. It will no
+ * longer be returned from {@link #all()}.
+ */
+ void remove(Project p);
+
/** @return sorted iteration of projects. */
public abstract Iterable<Project.NameKey> all();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 51d8fb2..cb18398 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -14,10 +14,11 @@
package com.google.gerrit.server.project;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ProjectConfig;
@@ -27,20 +28,24 @@
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.SortedSet;
-import java.util.TreeSet;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/** Cache of project information, including access rights. */
@Singleton
public class ProjectCacheImpl implements ProjectCache {
+ private static final Logger log = LoggerFactory
+ .getLogger(ProjectCacheImpl.class);
+
private static final String CACHE_NAME = "projects";
private static final String CACHE_LIST = "project_list";
@@ -48,13 +53,14 @@
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<Project.NameKey, ProjectState>> nameType =
- new TypeLiteral<Cache<Project.NameKey, ProjectState>>() {};
- core(nameType, CACHE_NAME).populateWith(Loader.class);
+ cache(CACHE_NAME, String.class, ProjectState.class)
+ .loader(Loader.class);
- final TypeLiteral<Cache<ListKey, SortedSet<Project.NameKey>>> listType =
- new TypeLiteral<Cache<ListKey, SortedSet<Project.NameKey>>>() {};
- core(listType, CACHE_LIST).populateWith(Lister.class);
+ cache(CACHE_LIST,
+ ListKey.class,
+ new TypeLiteral<SortedSet<Project.NameKey>>() {})
+ .maximumWeight(1)
+ .loader(Lister.class);
bind(ProjectCacheImpl.class);
bind(ProjectCache.class).to(ProjectCacheImpl.class);
@@ -63,16 +69,16 @@
}
private final AllProjectsName allProjectsName;
- private final Cache<Project.NameKey, ProjectState> byName;
- private final Cache<ListKey,SortedSet<Project.NameKey>> list;
+ private final LoadingCache<String, ProjectState> byName;
+ private final LoadingCache<ListKey, SortedSet<Project.NameKey>> list;
private final Lock listLock;
private final ProjectCacheClock clock;
@Inject
ProjectCacheImpl(
final AllProjectsName allProjectsName,
- @Named(CACHE_NAME) final Cache<Project.NameKey, ProjectState> byName,
- @Named(CACHE_LIST) final Cache<ListKey, SortedSet<Project.NameKey>> list,
+ @Named(CACHE_NAME) LoadingCache<String, ProjectState> byName,
+ @Named(CACHE_LIST) LoadingCache<ListKey, SortedSet<Project.NameKey>> list,
ProjectCacheClock clock) {
this.allProjectsName = allProjectsName;
this.byName = byName;
@@ -99,29 +105,53 @@
* @return the cached data; null if no such project exists.
*/
public ProjectState get(final Project.NameKey projectName) {
- ProjectState state = byName.get(projectName);
- if (state != null && state.needsRefresh(clock.read())) {
- byName.remove(projectName);
- state = byName.get(projectName);
+ if (projectName == null) {
+ return null;
}
- return state;
+ try {
+ ProjectState state = byName.get(projectName.get());
+ if (state != null && state.needsRefresh(clock.read())) {
+ byName.invalidate(projectName.get());
+ state = byName.get(projectName.get());
+ }
+ return state;
+ } catch (ExecutionException e) {
+ log.warn(String.format("Cannot read project %s", projectName.get()), e);
+ return null;
+ }
}
/** Invalidate the cached information about the given project. */
public void evict(final Project p) {
if (p != null) {
- byName.remove(p.getNameKey());
+ byName.invalidate(p.getNameKey().get());
}
}
@Override
+ public void remove(final Project p) {
+ listLock.lock();
+ try {
+ SortedSet<Project.NameKey> n = Sets.newTreeSet(list.get(ListKey.ALL));
+ n.remove(p.getNameKey());
+ list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
+ } catch (ExecutionException e) {
+ log.warn("Cannot list avaliable projects", e);
+ } finally {
+ listLock.unlock();
+ }
+ evict(p);
+ }
+
+ @Override
public void onCreateProject(Project.NameKey newProjectName) {
listLock.lock();
try {
- SortedSet<Project.NameKey> n = list.get(ListKey.ALL);
- n = new TreeSet<Project.NameKey>(n);
+ SortedSet<Project.NameKey> n = Sets.newTreeSet(list.get(ListKey.ALL));
n.add(newProjectName);
list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
+ } catch (ExecutionException e) {
+ log.warn("Cannot list avaliable projects", e);
} finally {
listLock.unlock();
}
@@ -129,18 +159,28 @@
@Override
public Iterable<Project.NameKey> all() {
- return list.get(ListKey.ALL);
+ try {
+ return list.get(ListKey.ALL);
+ } catch (ExecutionException e) {
+ log.warn("Cannot list available projects", e);
+ return Collections.emptyList();
+ }
}
@Override
public Iterable<Project.NameKey> byName(final String pfx) {
+ final Iterable<Project.NameKey> src;
+ try {
+ src = list.get(ListKey.ALL).tailSet(new Project.NameKey(pfx));
+ } catch (ExecutionException e) {
+ return Collections.emptyList();
+ }
return new Iterable<Project.NameKey>() {
@Override
public Iterator<Project.NameKey> iterator() {
return new Iterator<Project.NameKey>() {
+ private Iterator<Project.NameKey> itr = src.iterator();
private Project.NameKey next;
- private Iterator<Project.NameKey> itr =
- list.get(ListKey.ALL).tailSet(new Project.NameKey(pfx)).iterator();
@Override
public boolean hasNext() {
@@ -182,7 +222,7 @@
};
}
- static class Loader extends EntryCreator<Project.NameKey, ProjectState> {
+ static class Loader extends CacheLoader<String, ProjectState> {
private final ProjectState.Factory projectStateFactory;
private final GitRepositoryManager mgr;
@@ -193,19 +233,15 @@
}
@Override
- public ProjectState createEntry(Project.NameKey key) throws Exception {
+ public ProjectState load(String projectName) throws Exception {
+ Project.NameKey key = new Project.NameKey(projectName);
+ Repository git = mgr.openRepository(key);
try {
- Repository git = mgr.openRepository(key);
- try {
- final ProjectConfig cfg = new ProjectConfig(key);
- cfg.load(git);
- return projectStateFactory.create(cfg);
- } finally {
- git.close();
- }
-
- } catch (RepositoryNotFoundException notFound) {
- return null;
+ ProjectConfig cfg = new ProjectConfig(key);
+ cfg.load(git);
+ return projectStateFactory.create(cfg);
+ } finally {
+ git.close();
}
}
}
@@ -217,7 +253,7 @@
}
}
- static class Lister extends EntryCreator<ListKey, SortedSet<Project.NameKey>> {
+ static class Lister extends CacheLoader<ListKey, SortedSet<Project.NameKey>> {
private final GitRepositoryManager mgr;
@Inject
@@ -226,7 +262,7 @@
}
@Override
- public SortedSet<Project.NameKey> createEntry(ListKey key) throws Exception {
+ public SortedSet<Project.NameKey> load(ListKey key) throws Exception {
return mgr.list();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 29a6432..513f1b1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -14,37 +14,30 @@
package com.google.gerrit.server.project;
+import com.google.common.collect.Lists;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.reviewdb.client.AbstractAgreement;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
+import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.ReplicationUser;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GitReceivePackGroups;
import com.google.gerrit.server.config.GitUploadPackGroups;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -56,9 +49,6 @@
/** Access control management for a user accessing a project's data. */
public class ProjectControl {
- private static final Logger log =
- LoggerFactory.getLogger(ProjectControl.class);
-
public static final int VISIBLE = 1 << 0;
public static final int OWNER = 1 << 1;
@@ -124,28 +114,26 @@
private final Set<AccountGroup.UUID> receiveGroups;
private final String canonicalWebUrl;
- private final SchemaFactory<ReviewDb> schema;
private final CurrentUser user;
private final ProjectState state;
- private final GroupCache groupCache;
private final PermissionCollection.Factory permissionFilter;
+ private final Collection<ContributorAgreement> contributorAgreements;
private List<SectionMatcher> allSections;
private Map<String, RefControl> refControls;
private Boolean declaredOwner;
+
@Inject
ProjectControl(@GitUploadPackGroups Set<AccountGroup.UUID> uploadGroups,
@GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
- final SchemaFactory<ReviewDb> schema, final GroupCache groupCache,
- final PermissionCollection.Factory permissionFilter,
+ final ProjectCache pc, final PermissionCollection.Factory permissionFilter,
@CanonicalWebUrl @Nullable final String canonicalWebUrl,
@Assisted CurrentUser who, @Assisted ProjectState ps) {
this.uploadGroups = uploadGroups;
this.receiveGroups = receiveGroups;
- this.schema = schema;
- this.groupCache = groupCache;
this.permissionFilter = permissionFilter;
+ this.contributorAgreements = pc.getAllProjects().getConfig().getContributorAgreements();
this.canonicalWebUrl = canonicalWebUrl;
user = who;
state = ps;
@@ -198,7 +186,7 @@
/** Can this user see this project exists? */
public boolean isVisible() {
- return (visibleForReplication()
+ return (user instanceof InternalUser
|| canPerformOnAnyRef(Permission.READ)) && !isHidden();
}
@@ -209,14 +197,12 @@
/** Can this user see all the refs in this projects? */
public boolean allRefsAreVisible() {
- return visibleForReplication()
- || canPerformOnAllRefs(Permission.READ);
+ return allRefsAreVisibleExcept(Collections.<String> emptySet());
}
- /** Is this project completely visible for replication? */
- boolean visibleForReplication() {
- return user instanceof ReplicationUser
- && ((ReplicationUser) user).isEverythingVisible();
+ public boolean allRefsAreVisibleExcept(Set<String> except) {
+ return user instanceof InternalUser
+ || canPerformOnAllRefs(Permission.READ, except);
}
/** Is this user a project owner? Ownership does not imply {@link #isVisible()} */
@@ -247,12 +233,7 @@
}
Project project = state.getProject();
if (project.isUseContributorAgreements()) {
- try {
- return verifyActiveContributorAgreement();
- } catch (OrmException e) {
- log.error("Cannot query database for agreements", e);
- return new Capable("Cannot verify contribution agreement");
- }
+ return verifyActiveContributorAgreement();
}
return Capable.OK;
}
@@ -270,119 +251,60 @@
return all;
}
- private Capable verifyActiveContributorAgreement() throws OrmException {
+ private Capable verifyActiveContributorAgreement() {
if (! (user instanceof IdentifiedUser)) {
return new Capable("Must be logged in to verify Contributor Agreement");
}
final IdentifiedUser iUser = (IdentifiedUser) user;
- final ReviewDb db = schema.open();
- AbstractAgreement bestAgreement = null;
- ContributorAgreement bestCla = null;
- try {
+ boolean hasContactInfo = !missing(iUser.getAccount().getFullName())
+ && !missing(iUser.getAccount().getPreferredEmail())
+ && iUser.getAccount().isContactFiled();
- OUTER: for (AccountGroup.UUID groupUUID : iUser.getEffectiveGroups().getKnownGroups()) {
- AccountGroup group = groupCache.get(groupUUID);
- if (group == null) {
- continue;
- }
-
- final List<AccountGroupAgreement> temp =
- db.accountGroupAgreements().byGroup(group.getId()).toList();
-
- Collections.reverse(temp);
-
- for (final AccountGroupAgreement a : temp) {
- final ContributorAgreement cla =
- db.contributorAgreements().get(a.getAgreementId());
- if (cla == null) {
- continue;
- }
-
- bestAgreement = a;
- bestCla = cla;
- break OUTER;
- }
+ List<AccountGroup.UUID> okGroupIds = Lists.newArrayList();
+ List<AccountGroup.UUID> missingInfoGroupIds = Lists.newArrayList();
+ for (ContributorAgreement ca : contributorAgreements) {
+ List<AccountGroup.UUID> groupIds;
+ if (hasContactInfo || !ca.isRequireContactInformation()) {
+ groupIds = okGroupIds;
+ } else {
+ groupIds = missingInfoGroupIds;
}
- if (bestAgreement == null) {
- final List<AccountAgreement> temp =
- db.accountAgreements().byAccount(iUser.getAccountId()).toList();
+ for (PermissionRule rule : ca.getAccepted()) {
+ if ((rule.getAction() == Action.ALLOW) && (rule.getGroup() != null)
+ && (rule.getGroup().getUUID() != null)) {
+ groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get()));
+ }
+ }
+ }
- Collections.reverse(temp);
+ if (iUser.getEffectiveGroups().containsAnyOf(okGroupIds)) {
+ return Capable.OK;
+ }
- for (final AccountAgreement a : temp) {
- final ContributorAgreement cla =
- db.contributorAgreements().get(a.getAgreementId());
- if (cla == null) {
- continue;
- }
-
- bestAgreement = a;
- bestCla = cla;
+ if (iUser.getEffectiveGroups().containsAnyOf(missingInfoGroupIds)) {
+ final StringBuilder msg = new StringBuilder();
+ for (ContributorAgreement ca : contributorAgreements) {
+ if (ca.isRequireContactInformation()) {
+ msg.append(ca.getName());
break;
}
}
- } finally {
- db.close();
- }
-
-
- if (bestCla != null && !bestCla.isActive()) {
- final StringBuilder msg = new StringBuilder();
- msg.append(bestCla.getShortName());
- msg.append(" contributor agreement is expired.\n");
+ msg.append(" contributor agreement requires");
+ msg.append(" current contact information.\n");
if (canonicalWebUrl != null) {
- msg.append("\nPlease complete a new agreement");
+ msg.append("\nPlease review your contact information");
msg.append(":\n\n ");
msg.append(canonicalWebUrl);
msg.append("#");
- msg.append(PageLinks.SETTINGS_AGREEMENTS);
+ msg.append(PageLinks.SETTINGS_CONTACT);
msg.append("\n");
}
msg.append("\n");
return new Capable(msg.toString());
}
- if (bestCla != null && bestCla.isRequireContactInformation()) {
- boolean fail = false;
- fail |= missing(iUser.getAccount().getFullName());
- fail |= missing(iUser.getAccount().getPreferredEmail());
- fail |= !iUser.getAccount().isContactFiled();
-
- if (fail) {
- final StringBuilder msg = new StringBuilder();
- msg.append(bestCla.getShortName());
- msg.append(" contributor agreement requires");
- msg.append(" current contact information.\n");
- if (canonicalWebUrl != null) {
- msg.append("\nPlease review your contact information");
- msg.append(":\n\n ");
- msg.append(canonicalWebUrl);
- msg.append("#");
- msg.append(PageLinks.SETTINGS_CONTACT);
- msg.append("\n");
- }
- msg.append("\n");
- return new Capable(msg.toString());
- }
- }
-
- if (bestAgreement != null) {
- switch (bestAgreement.getStatus()) {
- case VERIFIED:
- return Capable.OK;
- case REJECTED:
- return new Capable(bestCla.getShortName()
- + " contributor agreement was rejected."
- + "\n (rejected on " + bestAgreement.getReviewedOn()
- + ")\n");
- case NEW:
- return new Capable(bestCla.getShortName()
- + " contributor agreement is still pending review.\n");
- }
- }
-
final StringBuilder msg = new StringBuilder();
msg.append(" A Contributor Agreement must be completed before uploading");
if (canonicalWebUrl != null) {
@@ -430,7 +352,7 @@
return false;
}
- private boolean canPerformOnAllRefs(String permission) {
+ private boolean canPerformOnAllRefs(String permission, Set<String> except) {
boolean canPerform = false;
Set<String> patterns = allRefPatterns(permission);
if (patterns.contains(AccessSection.ALL)) {
@@ -441,6 +363,8 @@
for (final String pattern : patterns) {
if (controlForRef(pattern).canPerform(permission)) {
canPerform = true;
+ } else if (except.contains(pattern)) {
+ continue;
} else {
return false;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 6b4780a..7de1fc1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.project;
-import com.google.gerrit.common.CollectionsUtil;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
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 db370e0..82c3a6d 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
@@ -23,6 +23,7 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import dk.brics.automaton.RegExp;
@@ -101,7 +102,7 @@
/** Can this user see this reference exists? */
public boolean isVisible() {
- return (projectControl.visibleForReplication() || canPerform(Permission.READ))
+ return (getCurrentUser() instanceof InternalUser || canPerform(Permission.READ))
&& canRead();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
index 40d4290..686ff59 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
@@ -14,14 +14,13 @@
package com.google.gerrit.server.project;
+import com.google.common.cache.Cache;
import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.util.MostSpecificComparator;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
-import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import java.util.Arrays;
@@ -38,9 +37,7 @@
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<EntryKey, EntryVal>> type =
- new TypeLiteral<Cache<EntryKey, EntryVal>>() {};
- core(type, CACHE_NAME);
+ cache(CACHE_NAME, EntryKey.class, EntryVal.class);
bind(SectionSortCache.class);
}
};
@@ -60,7 +57,7 @@
}
EntryKey key = new EntryKey(ref, sections);
- EntryVal val = cache.get(key);
+ EntryVal val = cache.getIfPresent(key);
if (val != null) {
int[] srcIdx = val.order;
if (srcIdx != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
index ce47d2d..b0f12b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query;
+import com.google.common.collect.Iterables;
import com.google.gwtorm.server.OrmException;
import java.util.Collection;
@@ -42,25 +43,43 @@
* @type <T> type of object the predicate can evaluate in memory.
*/
public abstract class Predicate<T> {
+ /** A predicate that matches any input, always, with no cost. */
+ @SuppressWarnings("unchecked")
+ public static <T> Predicate<T> any() {
+ return (Predicate<T>) Any.INSTANCE;
+ }
+
/** Combine the passed predicates into a single AND node. */
public static <T> Predicate<T> and(final Predicate<T>... that) {
+ if (that.length == 1) {
+ return that[0];
+ }
return new AndPredicate<T>(that);
}
/** Combine the passed predicates into a single AND node. */
public static <T> Predicate<T> and(
final Collection<? extends Predicate<T>> that) {
+ if (that.size() == 1) {
+ return Iterables.getOnlyElement(that);
+ }
return new AndPredicate<T>(that);
}
/** Combine the passed predicates into a single OR node. */
public static <T> Predicate<T> or(final Predicate<T>... that) {
+ if (that.length == 1) {
+ return that[0];
+ }
return new OrPredicate<T>(that);
}
/** Combine the passed predicates into a single OR node. */
public static <T> Predicate<T> or(
final Collection<? extends Predicate<T>> that) {
+ if (that.size() == 1) {
+ return Iterables.getOnlyElement(that);
+ }
return new OrPredicate<T>(that);
}
@@ -107,4 +126,36 @@
@Override
public abstract boolean equals(Object other);
+
+ private static class Any<T> extends Predicate<T> {
+ private static final Any<Object> INSTANCE = new Any<Object>();
+
+ private Any() {
+ }
+
+ @Override
+ public Predicate<T> copy(Collection<? extends Predicate<T>> children) {
+ return this;
+ }
+
+ @Override
+ public boolean match(T object) {
+ return true;
+ }
+
+ @Override
+ public int getCost() {
+ return 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return System.identityHashCode(this);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this;
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 7a85b6f..d6762db 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -14,11 +14,14 @@
package com.google.gerrit.server.query.change;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSet.Id;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.TrackingId;
@@ -28,7 +31,10 @@
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
import com.google.inject.Provider;
import org.eclipse.jgit.lib.ObjectId;
@@ -46,9 +52,61 @@
import java.util.Map;
public class ChangeData {
+ public static void ensureChangeLoaded(
+ Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+ Map<Change.Id, ChangeData> missing = Maps.newHashMap();
+ for (ChangeData cd : changes) {
+ if (cd.change == null) {
+ missing.put(cd.getId(), cd);
+ }
+ }
+ if (!missing.isEmpty()) {
+ for (Change change : db.get().changes().get(missing.keySet())) {
+ missing.get(change.getId()).change = change;
+ }
+ }
+ }
+
+ public static void ensureCurrentPatchSetLoaded(
+ Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+ Map<PatchSet.Id, ChangeData> missing = Maps.newHashMap();
+ for (ChangeData cd : changes) {
+ if (cd.currentPatchSet == null && cd.patches == null) {
+ missing.put(cd.change(db).currentPatchSetId(), cd);
+ }
+ }
+ if (!missing.isEmpty()) {
+ for (PatchSet ps : db.get().patchSets().get(missing.keySet())) {
+ ChangeData cd = missing.get(ps.getId());
+ cd.currentPatchSet = ps;
+ cd.patches = Lists.newArrayList(ps);
+ }
+ }
+ }
+
+ public static void ensureCurrentApprovalsLoaded(
+ Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+ List<ResultSet<PatchSetApproval>> pending = Lists.newArrayList();
+ for (ChangeData cd : changes) {
+ if (cd.currentApprovals == null && cd.approvals == null) {
+ pending.add(db.get().patchSetApprovals()
+ .byPatchSet(cd.change(db).currentPatchSetId()));
+ }
+ }
+ if (!pending.isEmpty()) {
+ int idx = 0;
+ for (ChangeData cd : changes) {
+ if (cd.currentApprovals == null && cd.approvals == null) {
+ cd.currentApprovals = pending.get(idx++).toList();
+ }
+ }
+ }
+ }
+
private final Change.Id legacyId;
private Change change;
private String commitMessage;
+ private PatchSet currentPatchSet;
private Collection<PatchSet> patches;
private Collection<PatchSetApproval> approvals;
private Map<PatchSet.Id,Collection<PatchSetApproval>> approvalsMap;
@@ -57,6 +115,7 @@
private Collection<PatchLineComment> comments;
private Collection<TrackingId> trackingIds;
private CurrentUser visibleTo;
+ private ChangeControl changeControl;
private List<ChangeMessage> messages;
public ChangeData(final Change.Id id) {
@@ -84,7 +143,14 @@
return null;
}
- PatchList p = cache.get(c, ps);
+ PatchList p;
+ try {
+ p = cache.get(c, ps);
+ } catch (PatchListNotAvailableException e) {
+ currentFiles = new String[0];
+ return currentFiles;
+ }
+
List<String> r = new ArrayList<String>(p.getPatches().size());
for (PatchListEntry e : p.getPatches()) {
if (Patch.COMMIT_MSG.equals(e.getNewName())) {
@@ -125,8 +191,13 @@
return visibleTo == user;
}
- void cacheVisibleTo(CurrentUser user) {
- visibleTo = user;
+ ChangeControl changeControl() {
+ return changeControl;
+ }
+
+ void cacheVisibleTo(ChangeControl ctl) {
+ visibleTo = ctl.getCurrentUser();
+ changeControl = ctl;
}
public Change change(Provider<ReviewDb> db) throws OrmException {
@@ -137,16 +208,19 @@
}
public PatchSet currentPatchSet(Provider<ReviewDb> db) throws OrmException {
- Change c = change(db);
- if (c == null) {
- return null;
- }
- for (PatchSet p : patches(db)) {
- if (p.getId().equals(c.currentPatchSetId())) {
- return p;
+ if (currentPatchSet == null) {
+ Change c = change(db);
+ if (c == null) {
+ return null;
+ }
+ for (PatchSet p : patches(db)) {
+ if (p.getId().equals(c.currentPatchSetId())) {
+ currentPatchSet = p;
+ return p;
+ }
}
}
- return null;
+ return currentPatchSet;
}
public Collection<PatchSetApproval> currentApprovals(Provider<ReviewDb> db)
@@ -155,24 +229,21 @@
Change c = change(db);
if (c == null) {
currentApprovals = Collections.emptyList();
+ } else if (approvals != null) {
+ Map<Id, Collection<PatchSetApproval>> map = approvalsMap(db);
+ currentApprovals = map.get(c.currentPatchSetId());
+ if (currentApprovals == null) {
+ currentApprovals = Collections.emptyList();
+ map.put(c.currentPatchSetId(), currentApprovals);
+ }
} else {
- currentApprovals = approvalsFor(db, c.currentPatchSetId());
+ currentApprovals = db.get().patchSetApprovals()
+ .byPatchSet(c.currentPatchSetId()).toList();
}
}
return currentApprovals;
}
- public Collection<PatchSetApproval> approvalsFor(Provider<ReviewDb> db,
- PatchSet.Id psId) throws OrmException {
- List<PatchSetApproval> r = new ArrayList<PatchSetApproval>();
- for (PatchSetApproval p : approvals(db)) {
- if (p.getPatchSetId().equals(psId)) {
- r.add(p);
- }
- }
- return r;
- }
-
public String commitMessage(GitRepositoryManager repoManager,
Provider<ReviewDb> db) throws IOException, OrmException {
if (commitMessage == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 4d8e806..ddc4c28 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -14,7 +14,9 @@
package com.google.gerrit.server.query.change;
+import com.google.common.collect.Lists;
import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
@@ -25,7 +27,8 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchListCache;
@@ -44,6 +47,7 @@
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -103,7 +107,7 @@
final ChangeControl.Factory changeControlFactory;
final ChangeControl.GenericFactory changeControlGenericFactory;
final AccountResolver accountResolver;
- final GroupCache groupCache;
+ final GroupBackend groupBackend;
final ApprovalTypes approvalTypes;
final AllProjectsName allProjectsName;
final PatchListCache patchListCache;
@@ -117,7 +121,8 @@
CapabilityControl.Factory capabilityControlFactory,
ChangeControl.Factory changeControlFactory,
ChangeControl.GenericFactory changeControlGenericFactory,
- AccountResolver accountResolver, GroupCache groupCache,
+ AccountResolver accountResolver,
+ GroupBackend groupBackend,
ApprovalTypes approvalTypes,
AllProjectsName allProjectsName,
PatchListCache patchListCache,
@@ -130,7 +135,7 @@
this.changeControlFactory = changeControlFactory;
this.changeControlGenericFactory = changeControlGenericFactory;
this.accountResolver = accountResolver;
- this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
this.approvalTypes = approvalTypes;
this.allProjectsName = allProjectsName;
this.patchListCache = patchListCache;
@@ -206,10 +211,7 @@
}
if ("draft".equalsIgnoreCase(value)) {
- if (currentUser instanceof IdentifiedUser) {
- return new HasDraftByPredicate(args.dbProvider,
- ((IdentifiedUser) currentUser).getAccountId());
- }
+ return new HasDraftByPredicate(args.dbProvider, self());
}
throw new IllegalArgumentException();
@@ -233,6 +235,14 @@
return new IsReviewedPredicate(args.dbProvider);
}
+ if ("owner".equalsIgnoreCase(value)) {
+ return new OwnerPredicate(args.dbProvider, self());
+ }
+
+ if ("reviewer".equalsIgnoreCase(value)) {
+ return new ReviewerPredicate(args.dbProvider, self());
+ }
+
try {
return status(value);
} catch (IllegalArgumentException e) {
@@ -303,58 +313,68 @@
@Operator
public Predicate<ChangeData> starredby(String who)
throws QueryParseException, OrmException {
- Account account = args.accountResolver.find(who);
- if (account == null) {
- throw error("User " + who + " not found");
+ if ("self".equals(who)) {
+ return new IsStarredByPredicate(args.dbProvider, currentUser);
}
- return new IsStarredByPredicate(args.dbProvider, //
- args.userFactory.create(args.dbProvider, account.getId()));
+ Set<Account.Id> m = parseAccount(who);
+ List<IsStarredByPredicate> p = Lists.newArrayListWithCapacity(m.size());
+ for (Account.Id id : m) {
+ p.add(new IsStarredByPredicate(args.dbProvider,
+ args.userFactory.create(args.dbProvider, id)));
+ }
+ return Predicate.or(p);
}
@Operator
public Predicate<ChangeData> watchedby(String who)
throws QueryParseException, OrmException {
- Account account = args.accountResolver.find(who);
- if (account == null) {
- throw error("User " + who + " not found");
+ Set<Account.Id> m = parseAccount(who);
+ List<IsWatchedByPredicate> p = Lists.newArrayListWithCapacity(m.size());
+ for (Account.Id id : m) {
+ if (currentUser instanceof IdentifiedUser
+ && id.equals(((IdentifiedUser) currentUser).getAccountId())) {
+ p.add(new IsWatchedByPredicate(args, currentUser));
+ } else {
+ p.add(new IsWatchedByPredicate(args,
+ args.userFactory.create(args.dbProvider, id)));
+ }
}
- return new IsWatchedByPredicate(args, args.userFactory.create(
- args.dbProvider, account.getId()));
+ return Predicate.or(p);
}
@Operator
public Predicate<ChangeData> draftby(String who) throws QueryParseException,
OrmException {
- Account account = args.accountResolver.find(who);
- if (account == null) {
- throw error("User " + who + " not found");
+ Set<Account.Id> m = parseAccount(who);
+ List<HasDraftByPredicate> p = Lists.newArrayListWithCapacity(m.size());
+ for (Account.Id id : m) {
+ p.add(new HasDraftByPredicate(args.dbProvider, id));
}
- return new HasDraftByPredicate(args.dbProvider, account.getId());
+ return Predicate.or(p);
}
@Operator
public Predicate<ChangeData> visibleto(String who)
throws QueryParseException, OrmException {
- Account account = args.accountResolver.find(who);
- if (account != null) {
- return visibleto(args.userFactory
- .create(args.dbProvider, account.getId()));
+ if ("self".equals(who)) {
+ return is_visible();
+ }
+ Set<Account.Id> m = args.accountResolver.findAll(who);
+ if (!m.isEmpty()) {
+ List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
+ for (Account.Id id : m) {
+ return visibleto(args.userFactory.create(args.dbProvider, id));
+ }
+ return Predicate.or(p);
}
// If its not an account, maybe its a group?
//
- AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(who));
- if (g != null) {
- return visibleto(new SingleGroupUser(args.capabilityControlFactory,
- g.getGroupUUID()));
- }
-
- Collection<AccountGroup> matches =
- args.groupCache.get(new AccountGroup.ExternalNameKey(who));
- if (matches != null && !matches.isEmpty()) {
+ Collection<GroupReference> suggestions = args.groupBackend.suggest(who);
+ if (!suggestions.isEmpty()) {
HashSet<AccountGroup.UUID> ids = new HashSet<AccountGroup.UUID>();
- for (AccountGroup group : matches) {
- ids.add(group.getGroupUUID());
+ for (GroupReference ref : suggestions) {
+ ids.add(ref.getUUID());
}
return visibleto(new SingleGroupUser(args.capabilityControlFactory, ids));
}
@@ -375,57 +395,43 @@
@Operator
public Predicate<ChangeData> owner(String who) throws QueryParseException,
OrmException {
- Set<Account.Id> m = args.accountResolver.findAll(who);
- if (m.isEmpty()) {
- throw error("User " + who + " not found");
- } else if (m.size() == 1) {
- Account.Id id = m.iterator().next();
- return new OwnerPredicate(args.dbProvider, id);
- } else {
- List<OwnerPredicate> p = new ArrayList<OwnerPredicate>(m.size());
- for (Account.Id id : m) {
- p.add(new OwnerPredicate(args.dbProvider, id));
- }
- return Predicate.or(p);
+ Set<Account.Id> m = parseAccount(who);
+ List<OwnerPredicate> p = Lists.newArrayListWithCapacity(m.size());
+ for (Account.Id id : m) {
+ p.add(new OwnerPredicate(args.dbProvider, id));
}
+ return Predicate.or(p);
}
@Operator
- public Predicate<ChangeData> ownerin(String group) throws QueryParseException,
- OrmException {
- AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(group));
+ public Predicate<ChangeData> ownerin(String group)
+ throws QueryParseException {
+ GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
if (g == null) {
throw error("Group " + group + " not found");
}
- return new OwnerinPredicate(args.dbProvider, args.userFactory, g.getGroupUUID());
+ return new OwnerinPredicate(args.dbProvider, args.userFactory, g.getUUID());
}
@Operator
public Predicate<ChangeData> reviewer(String who)
throws QueryParseException, OrmException {
- Set<Account.Id> m = args.accountResolver.findAll(who);
- if (m.isEmpty()) {
- throw error("User " + who + " not found");
- } else if (m.size() == 1) {
- Account.Id id = m.iterator().next();
- return new ReviewerPredicate(args.dbProvider, id);
- } else {
- List<ReviewerPredicate> p = new ArrayList<ReviewerPredicate>(m.size());
- for (Account.Id id : m) {
- p.add(new ReviewerPredicate(args.dbProvider, id));
- }
- return Predicate.or(p);
+ Set<Account.Id> m = parseAccount(who);
+ List<ReviewerPredicate> p = Lists.newArrayListWithCapacity(m.size());
+ for (Account.Id id : m) {
+ p.add(new ReviewerPredicate(args.dbProvider, id));
}
+ return Predicate.or(p);
}
@Operator
public Predicate<ChangeData> reviewerin(String group)
- throws QueryParseException, OrmException {
- AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(group));
+ throws QueryParseException {
+ GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
if (g == null) {
throw error("Group " + group + " not found");
}
- return new ReviewerinPredicate(args.dbProvider, args.userFactory, g.getGroupUUID());
+ return new ReviewerinPredicate(args.dbProvider, args.userFactory, g.getUUID());
}
@Operator
@@ -532,4 +538,23 @@
throw error("Unsupported query:" + query);
}
}
+
+ private Set<Account.Id> parseAccount(String who)
+ throws QueryParseException, OrmException {
+ if ("self".equals(who)) {
+ return Collections.singleton(self());
+ }
+ Set<Account.Id> matches = args.accountResolver.findAll(who);
+ if (matches.isEmpty()) {
+ throw error("User " + who + " not found");
+ }
+ return matches;
+ }
+
+ private Account.Id self() {
+ if (currentUser instanceof IdentifiedUser) {
+ return ((IdentifiedUser) currentUser).getAccountId();
+ }
+ throw new IllegalArgumentException();
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
index 413e6c4..b73465a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
@@ -55,15 +55,18 @@
}
try {
Change c = cd.change(db);
- if (c != null && changeControl.controlFor(c, user).isVisible(db.get())) {
- cd.cacheVisibleTo(user);
- return true;
- } else {
+ if (c == null) {
return false;
}
+
+ ChangeControl cc = changeControl.controlFor(c, user);
+ if (cc.isVisible(db.get())) {
+ cd.cacheVisibleTo(cc);
+ return true;
+ }
} catch (NoSuchChangeException e) {
- return false;
}
+ return false;
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java
new file mode 100644
index 0000000..adf4f19
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java
@@ -0,0 +1,323 @@
+// 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.query.change;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.events.AccountAttribute;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class ListChanges {
+ private final QueryProcessor imp;
+ private final Provider<ReviewDb> db;
+ private final ApprovalTypes approvalTypes;
+ private final CurrentUser user;
+ private final ChangeControl.Factory changeControlFactory;
+ private boolean reverse;
+ private Map<Account.Id, AccountAttribute> accounts;
+
+ @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+ private OutputFormat format = OutputFormat.TEXT;
+
+ @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", multiValued = true, usage = "Query string")
+ private List<String> queries;
+
+ @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "Maximum number of results to return")
+ void setLimit(int limit) {
+ imp.setLimit(limit);
+ }
+
+ @Option(name = "-P", metaVar = "SORTKEY", usage = "Previous changes before SORTKEY")
+ void setSortKeyAfter(String key) {
+ // Querying for the prior page of changes requires sortkey_after predicate.
+ // Changes are shown most recent->least recent. The previous page of
+ // results contains changes that were updated after the given key.
+ imp.setSortkeyAfter(key);
+ reverse = true;
+ }
+
+ @Option(name = "-N", metaVar = "SORTKEY", usage = "Next changes after SORTKEY")
+ void setSortKeyBefore(String key) {
+ // Querying for the next page of changes requires sortkey_before predicate.
+ // Changes are shown most recent->least recent. The next page contains
+ // changes that were updated before the given key.
+ imp.setSortkeyBefore(key);
+ }
+
+ @Inject
+ ListChanges(QueryProcessor qp,
+ Provider<ReviewDb> db,
+ ApprovalTypes at,
+ CurrentUser u,
+ ChangeControl.Factory cf) {
+ this.imp = qp;
+ this.db = db;
+ this.approvalTypes = at;
+ this.user = u;
+ this.changeControlFactory = cf;
+
+ accounts = Maps.newHashMap();
+ }
+
+ public OutputFormat getFormat() {
+ return format;
+ }
+
+ public ListChanges setFormat(OutputFormat fmt) {
+ this.format = fmt;
+ return this;
+ }
+
+ public void query(Writer out)
+ throws OrmException, QueryParseException, IOException {
+ if (imp.isDisabled()) {
+ throw new QueryParseException("query disabled");
+ }
+ if (queries == null || queries.isEmpty()) {
+ queries = Collections.singletonList("status:open");
+ } else if (queries.size() > 10) {
+ // Hard-code a default maximum number of queries to prevent
+ // users from submitting too much to the server in a single call.
+ throw new QueryParseException("limit of 10 queries");
+ }
+
+ List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(queries.size());
+ for (String query : queries) {
+ List<ChangeData> changes = imp.queryChanges(query);
+ boolean moreChanges = imp.getLimit() > 0 && changes.size() > imp.getLimit();
+ if (moreChanges) {
+ if (reverse) {
+ changes = changes.subList(1, changes.size());
+ } else {
+ changes = changes.subList(0, imp.getLimit());
+ }
+ }
+ ChangeData.ensureChangeLoaded(db, changes);
+ ChangeData.ensureCurrentPatchSetLoaded(db, changes);
+ ChangeData.ensureCurrentApprovalsLoaded(db, changes);
+
+ List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
+ for (ChangeData cd : changes) {
+ info.add(toChangeInfo(cd));
+ }
+ if (moreChanges && !info.isEmpty()) {
+ if (reverse) {
+ info.get(0)._moreChanges = true;
+ } else {
+ info.get(info.size() - 1)._moreChanges = true;
+ }
+ }
+ res.add(info);
+ }
+
+ if (!accounts.isEmpty()) {
+ for (Account account : db.get().accounts().get(accounts.keySet())) {
+ AccountAttribute a = accounts.get(account.getId());
+ a.name = Strings.emptyToNull(account.getFullName());
+ }
+ }
+
+ if (format.isJson()) {
+ format.newGson().toJson(
+ res.size() == 1 ? res.get(0) : res,
+ new TypeToken<List<ChangeInfo>>() {}.getType(),
+ out);
+ out.write('\n');
+ } else {
+ boolean firstQuery = true;
+ for (List<ChangeInfo> info : res) {
+ if (firstQuery) {
+ firstQuery = false;
+ } else {
+ out.write('\n');
+ }
+ for (ChangeInfo c : info) {
+ String id = new Change.Key(c.id).abbreviate();
+ String subject = c.subject;
+ if (subject.length() + id.length() > 80) {
+ subject = subject.substring(0, 80 - id.length());
+ }
+ out.write(id);
+ out.write(' ');
+ out.write(subject.replace('\n', ' '));
+ out.write('\n');
+ }
+ }
+ }
+ }
+
+ private ChangeInfo toChangeInfo(ChangeData cd) throws OrmException {
+ ChangeInfo out = new ChangeInfo();
+ Change in = cd.change(db);
+ out.project = in.getProject().get();
+ out.branch = in.getDest().getShortName();
+ out.topic = in.getTopic();
+ out.id = in.getKey().get();
+ out.subject = in.getSubject();
+ out.status = in.getStatus();
+ out.owner = asAccountAttribute(in.getOwner());
+ out.created = in.getCreatedOn();
+ out.updated = in.getLastUpdatedOn();
+ out._number = in.getId().get();
+ out._sortkey = in.getSortKey();
+ out.starred = user.getStarredChanges().contains(in.getId()) ? true : null;
+ out.labels = labelsFor(cd);
+ return out;
+ }
+
+ private AccountAttribute asAccountAttribute(Account.Id user) {
+ if (user == null) {
+ return null;
+ }
+ AccountAttribute a = accounts.get(user);
+ if (a == null) {
+ a = new AccountAttribute();
+ accounts.put(user, a);
+ }
+ return a;
+ }
+
+ private Map<String, LabelInfo> labelsFor(ChangeData cd) throws OrmException {
+ Change in = cd.change(db);
+ ChangeControl ctl = cd.changeControl();
+ if (ctl == null || ctl.getCurrentUser() != user) {
+ try {
+ ctl = changeControlFactory.controlFor(in);
+ } catch (NoSuchChangeException e) {
+ return null;
+ }
+ }
+
+ PatchSet ps = cd.currentPatchSet(db);
+ Map<String, LabelInfo> labels = Maps.newLinkedHashMap();
+ for (SubmitRecord rec : ctl.canSubmit(db.get(), ps, cd, true, false)) {
+ if (rec.labels == null) {
+ continue;
+ }
+ for (SubmitRecord.Label r : rec.labels) {
+ LabelInfo p = labels.get(r.label);
+ if (p == null || p._status.compareTo(r.status) < 0) {
+ LabelInfo n = new LabelInfo();
+ n._status = r.status;
+ switch (r.status) {
+ case OK:
+ n.approved = asAccountAttribute(r.appliedBy);
+ break;
+ case REJECT:
+ n.rejected = asAccountAttribute(r.appliedBy);
+ break;
+ }
+ n.optional = n._status == SubmitRecord.Label.Status.MAY ? true : null;
+ labels.put(r.label, n);
+ }
+ }
+ }
+
+ Collection<PatchSetApproval> approvals = null;
+ for (Map.Entry<String, LabelInfo> e : labels.entrySet()) {
+ if (e.getValue().approved != null || e.getValue().rejected != null) {
+ continue;
+ }
+
+ ApprovalType type = approvalTypes.byLabel(e.getKey());
+ if (type == null || type.getMin() == null || type.getMax() == null) {
+ // Unknown or misconfigured type can't have intermediate scores.
+ continue;
+ }
+
+ short min = type.getMin().getValue();
+ short max = type.getMax().getValue();
+ if (-1 <= min && max <= 1) {
+ // Types with a range of -1..+1 can't have intermediate scores.
+ continue;
+ }
+
+ if (approvals == null) {
+ approvals = cd.currentApprovals(db);
+ }
+ for (PatchSetApproval psa : approvals) {
+ short val = psa.getValue();
+ if (val != 0 && min < val && val < max
+ && psa.getCategoryId().equals(type.getCategory().getId())) {
+ if (0 < val) {
+ e.getValue().recommended = asAccountAttribute(psa.getAccountId());
+ e.getValue().value = val != 1 ? val : null;
+ } else {
+ e.getValue().disliked = asAccountAttribute(psa.getAccountId());
+ e.getValue().value = val != -1 ? val : null;
+ }
+ }
+ }
+ }
+ return labels;
+ }
+
+ static class ChangeInfo {
+ String project;
+ String branch;
+ String topic;
+ String id;
+ String subject;
+ Change.Status status;
+ Timestamp created;
+ Timestamp updated;
+ Boolean starred;
+
+ String _sortkey;
+ int _number;
+
+ AccountAttribute owner;
+ Map<String, LabelInfo> labels;
+ Boolean _moreChanges;
+ }
+
+ static class LabelInfo {
+ transient SubmitRecord.Label.Status _status;
+ AccountAttribute approved;
+ AccountAttribute rejected;
+
+ AccountAttribute recommended;
+ AccountAttribute disliked;
+ Short value;
+ Boolean optional;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index a2fa7fe..76945f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -55,6 +55,30 @@
private static final Logger log =
LoggerFactory.getLogger(QueryProcessor.class);
+ private final Comparator<ChangeData> cmpAfter =
+ new Comparator<ChangeData>() {
+ @Override
+ public int compare(ChangeData a, ChangeData b) {
+ try {
+ return a.change(db).getSortKey().compareTo(b.change(db).getSortKey());
+ } catch (OrmException e) {
+ return 0;
+ }
+ }
+ };
+
+ private final Comparator<ChangeData> cmpBefore =
+ new Comparator<ChangeData>() {
+ @Override
+ public int compare(ChangeData a, ChangeData b) {
+ try {
+ return b.change(db).getSortKey().compareTo(a.change(db).getSortKey());
+ } catch (OrmException e) {
+ return 0;
+ }
+ }
+ };
+
public static enum OutputFormat {
TEXT, JSON;
}
@@ -71,6 +95,9 @@
private final int maxLimit;
private OutputFormat outputFormat = OutputFormat.TEXT;
+ private int limit;
+ private String sortkeyAfter;
+ private String sortkeyBefore;
private boolean includePatchSets;
private boolean includeCurrentPatchSet;
private boolean includeApprovals;
@@ -97,6 +124,22 @@
.getMax();
}
+ int getLimit() {
+ return limit;
+ }
+
+ void setLimit(int n) {
+ limit = n;
+ }
+
+ void setSortkeyAfter(String sortkey) {
+ sortkeyAfter = sortkey;
+ }
+
+ void setSortkeyBefore(String sortkey) {
+ sortkeyBefore = sortkey;
+ }
+
public void setIncludePatchSets(boolean on) {
includePatchSets = on;
}
@@ -146,6 +189,14 @@
this.outputFormat = fmt;
}
+ /**
+ * Query for changes that match the query string.
+ * <p>
+ * If a limit was specified using {@link #setLimit(int)} this method may
+ * return up to {@code limit + 1} results, allowing the caller to determine if
+ * there are more than {@code limit} matches and suggest to its own caller
+ * that the query could be retried with {@link #setSortkeyBefore(String)}.
+ */
public List<ChangeData> queryChanges(final String queryString)
throws OrmException, QueryParseException {
final Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
@@ -175,19 +226,14 @@
}
}
- Collections.sort(results, new Comparator<ChangeData>() {
- @Override
- public int compare(ChangeData a, ChangeData b) {
- return b.getChange().getSortKey().compareTo(
- a.getChange().getSortKey());
- }
- });
-
+ Collections.sort(results, sortkeyAfter != null ? cmpAfter : cmpBefore);
int limit = limit(s);
if (limit < results.size()) {
results = results.subList(0, limit);
}
-
+ if (sortkeyAfter != null) {
+ Collections.reverse(results);
+ }
return results;
}
@@ -196,7 +242,7 @@
new BufferedWriter( //
new OutputStreamWriter(outputStream, "UTF-8")));
try {
- if (maxLimit <= 0) {
+ if (isDisabled()) {
ErrorMessage m = new ErrorMessage();
m.message = "query disabled";
show(m);
@@ -233,7 +279,7 @@
if (current != null) {
c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
eventFactory.addApprovals(c.currentPatchSet, //
- d.approvalsFor(db, current.getId()));
+ d.currentApprovals(db));
if (includeFiles) {
eventFactory.addPatchSetFileNames(c.currentPatchSet,
@@ -283,8 +329,13 @@
}
}
+ boolean isDisabled() {
+ return maxLimit <= 0;
+ }
+
private int limit(Predicate<ChangeData> s) {
- return queryBuilder.hasLimit(s) ? queryBuilder.getLimit(s) : maxLimit;
+ int n = queryBuilder.hasLimit(s) ? queryBuilder.getLimit(s) : maxLimit;
+ return limit > 0 ? Math.min(n, limit) + 1 : n;
}
@SuppressWarnings("unchecked")
@@ -293,9 +344,17 @@
Predicate<ChangeData> q = queryBuilder.parse(queryString);
if (!queryBuilder.hasSortKey(q)) {
- q = Predicate.and(q, queryBuilder.sortkey_before("z"));
+ if (sortkeyBefore != null) {
+ q = Predicate.and(q, queryBuilder.sortkey_before(sortkeyBefore));
+ } else if (sortkeyAfter != null) {
+ q = Predicate.and(q, queryBuilder.sortkey_after(sortkeyAfter));
+ } else {
+ q = Predicate.and(q, queryBuilder.sortkey_before("z"));
+ }
}
- q = Predicate.and(q, queryBuilder.limit(maxLimit), visibleToMe);
+ q = Predicate.and(q,
+ queryBuilder.limit(limit > 0 ? Math.min(limit, maxLimit) + 1 : maxLimit),
+ visibleToMe);
Predicate<ChangeData> s = queryRewriter.rewrite(q);
if (!(s instanceof ChangeDataSource)) {
@@ -303,7 +362,7 @@
}
if (!(s instanceof ChangeDataSource)) {
- throw new QueryParseException("cannot execute query: " + s);
+ throw new QueryParseException("invalid query: " + s);
}
return s;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
index cdce217..270b2e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -27,15 +27,15 @@
import java.util.Collections;
import java.util.Set;
-final class SingleGroupUser extends CurrentUser {
+public final class SingleGroupUser extends CurrentUser {
private final GroupMembership groups;
- SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
+ public SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
AccountGroup.UUID groupId) {
this(capabilityControlFactory, Collections.singleton(groupId));
}
- SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
+ public SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
Set<AccountGroup.UUID> groups) {
super(capabilityControlFactory, AccessPath.UNKNOWN);
this.groups = new ListGroupMembership(groups);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
index 2a98e96..cc48019 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -18,7 +18,7 @@
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
index d8809da..ff6dc6c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -32,9 +32,9 @@
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gwtorm.jdbc.JdbcExecutor;
import com.google.gwtorm.jdbc.JdbcSchema;
@@ -161,7 +161,7 @@
anonymous =
newGroup(c, "Anonymous Users", AccountGroup.ANONYMOUS_USERS);
anonymous.setDescription("Any user, signed-in or not");
- anonymous.setOwnerGroupId(admin.getId());
+ anonymous.setOwnerGroupUUID(admin.getGroupUUID());
anonymous.setType(AccountGroup.Type.SYSTEM);
c.accountGroups().insert(Collections.singleton(anonymous));
c.accountGroupNames().insert(
@@ -170,7 +170,7 @@
registered =
newGroup(c, "Registered Users", AccountGroup.REGISTERED_USERS);
registered.setDescription("Any signed-in user");
- registered.setOwnerGroupId(admin.getId());
+ registered.setOwnerGroupUUID(admin.getGroupUUID());
registered.setType(AccountGroup.Type.SYSTEM);
c.accountGroups().insert(Collections.singleton(registered));
c.accountGroupNames().insert(
@@ -178,7 +178,7 @@
final AccountGroup batchUsers = newGroup(c, "Non-Interactive Users", null);
batchUsers.setDescription("Users who perform batch actions on Gerrit");
- batchUsers.setOwnerGroupId(admin.getId());
+ batchUsers.setOwnerGroupUUID(admin.getGroupUUID());
batchUsers.setType(AccountGroup.Type.INTERNAL);
c.accountGroups().insert(Collections.singleton(batchUsers));
c.accountGroupNames().insert(
@@ -186,7 +186,7 @@
owners = newGroup(c, "Project Owners", AccountGroup.PROJECT_OWNERS);
owners.setDescription("Any owner of the project");
- owners.setOwnerGroupId(admin.getId());
+ owners.setOwnerGroupUUID(admin.getGroupUUID());
owners.setType(AccountGroup.Type.SYSTEM);
c.accountGroups().insert(Collections.singleton(owners));
c.accountGroupNames().insert(
@@ -219,7 +219,8 @@
}
}
try {
- MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), allProjectsName, git);
+ MetaDataUpdate md =
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjectsName, git);
md.getCommitBuilder().setAuthor(serverUser);
md.getCommitBuilder().setCommitter(serverUser);
@@ -251,8 +252,9 @@
all.getPermission(Permission.FORGE_AUTHOR, true) //
.add(rule(config, registered));
- meta.getPermission(Permission.READ, true) //
- .add(rule(config, owners));
+ Permission metaReadPermission = meta.getPermission(Permission.READ, true);
+ metaReadPermission.setExclusiveGroup(true);
+ metaReadPermission.add(rule(config, owners));
md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
if (!config.commit(md)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index f789300..0a34b44 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
- public static final Class<Schema_64> C = Schema_64.class;
+ public static final Class<Schema_69> C = Schema_69.class;
public static class Module extends AbstractModule {
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
index 75d8a39..133b856 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.schema;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
import com.google.gerrit.reviewdb.server.ReviewDb;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
index d49b34e..54ee9ab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
@@ -37,9 +37,9 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.GroupUUID;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmException;
@@ -158,13 +158,15 @@
// inheritable permissions. For example 'All-Projects'.
try {
git = mgr.createRepository(nameKey);
- } catch (RepositoryNotFoundException err) {
+ } catch (IOException err) {
throw new OrmException("Cannot create repository " + name, err);
}
+ } catch (IOException e) {
+ throw new OrmException(e);
}
try {
MetaDataUpdate md =
- new MetaDataUpdate(new NoReplication(), nameKey, git);
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, nameKey, git);
md.getCommitBuilder().setAuthor(serverUser);
md.getCommitBuilder().setCommitter(serverUser);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
index 2247fdb..4699a00 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
@@ -26,9 +26,9 @@
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.AllProjectsNameProvider;
import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.LocalDiskRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -81,7 +81,8 @@
try {
Repository git = mgr.openRepository(allProjects);
try {
- MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), allProjects, git);
+ MetaDataUpdate md =
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
md.getCommitBuilder().setAuthor(serverUser);
md.getCommitBuilder().setCommitter(serverUser);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
index 26890a3..127f9c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
@@ -23,9 +23,9 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmException;
@@ -33,7 +33,6 @@
import com.google.inject.Provider;
import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
@@ -88,12 +87,12 @@
Repository git;
try {
git = mgr.openRepository(allProjects);
- } catch (RepositoryNotFoundException e) {
+ } catch (IOException e) {
throw new OrmException(e);
}
try {
MetaDataUpdate md =
- new MetaDataUpdate(new NoReplication(), allProjects, git);
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
md.getCommitBuilder().setAuthor(serverUser);
md.getCommitBuilder().setCommitter(serverUser);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
new file mode 100644
index 0000000..3383364
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
@@ -0,0 +1,463 @@
+// 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.schema;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.primitives.Longs;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.GroupUUID;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.SystemReader;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+public class Schema_65 extends SchemaVersion {
+ private final AllProjectsName allProjects;
+ private final GitRepositoryManager mgr;
+ private final PersonIdent serverUser;
+ private final @AnonymousCowardName String anonymousCowardName;
+
+ @Inject
+ Schema_65(Provider<Schema_64> prior,
+ AllProjectsName allProjects,
+ GitRepositoryManager mgr,
+ @GerritPersonIdent PersonIdent serverUser,
+ @AnonymousCowardName String anonymousCowardName) {
+ super(prior);
+ this.allProjects = allProjects;
+ this.mgr = mgr;
+ this.serverUser = serverUser;
+ this.anonymousCowardName = anonymousCowardName;
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui)
+ throws OrmException, SQLException {
+ Repository git;
+ try {
+ git = mgr.openRepository(allProjects);
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ try {
+ MetaDataUpdate md =
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
+ ProjectConfig config = ProjectConfig.read(md);
+ Map<Integer, ContributorAgreement> agreements = getAgreementToAdd(db, config);
+ if (agreements.isEmpty()) {
+ return;
+ }
+ ui.message("Moved contributor agreements to project.config");
+
+ // Create the auto verify groups.
+ List<AccountGroup.UUID> adminGroupUUIDs = getAdministrateServerGroups(db, config);
+ for (ContributorAgreement agreement : agreements.values()) {
+ if (agreement.getAutoVerify() != null) {
+ getOrCreateGroupForIndividuals(db, config, adminGroupUUIDs, agreement);
+ }
+ }
+
+ // Scan AccountAgreement
+ long minTime = addAccountAgreements(db, config, adminGroupUUIDs, agreements);
+
+ ProjectConfig base = ProjectConfig.read(md, null);
+ for (ContributorAgreement agreement : agreements.values()) {
+ base.replace(agreement);
+ }
+ base.getAccountsSection().setSameGroupVisibility(
+ config.getAccountsSection().getSameGroupVisibility());
+
+ BatchMetaDataUpdate batch = base.openUpdate(md);
+ try {
+ // Scan AccountGroupAgreement
+ List<AccountGroupAgreement> groupAgreements =
+ getAccountGroupAgreements(db, agreements);
+
+ // Find the earliest change
+ for (AccountGroupAgreement aga : groupAgreements) {
+ minTime = Math.min(minTime, aga.getTime());
+ }
+ minTime -= 60 * 1000; // 1 Minute
+
+ CommitBuilder commit = new CommitBuilder();
+ commit.setAuthor(new PersonIdent(serverUser, new Date(minTime)));
+ commit.setCommitter(new PersonIdent(serverUser, new Date(minTime)));
+ commit.setMessage("Add the ContributorAgreements for upgrade to Gerrit Code Review schema 65\n");
+ batch.write(commit);
+
+ for (AccountGroupAgreement aga : groupAgreements) {
+ AccountGroup group = db.accountGroups().get(aga.groupId);
+ if (group == null) {
+ continue;
+ }
+
+ ContributorAgreement agreement = agreements.get(aga.claId);
+ agreement.getAccepted().add(new PermissionRule(config.resolve(group)));
+ base.replace(agreement);
+
+ PersonIdent ident = null;
+ if (aga.reviewedBy != null) {
+ Account ua = db.accounts().get(aga.reviewedBy);
+ if (ua != null) {
+ String name = ua.getFullName();
+ String email = ua.getPreferredEmail();
+
+ if (email == null || email.isEmpty()) {
+ // No preferred email is configured. Use a generic identity so we
+ // don't leak an address the user may have given us, but doesn't
+ // necessarily want to publish through Git records.
+ //
+ String user = ua.getUserName();
+ if (user == null || user.isEmpty()) {
+ user = "account-" + ua.getId().toString();
+ }
+
+ String host = SystemReader.getInstance().getHostname();
+ email = user + "@" + host;
+ }
+
+ if (name == null || name.isEmpty()) {
+ final int at = email.indexOf('@');
+ if (0 < at) {
+ name = email.substring(0, at);
+ } else {
+ name = anonymousCowardName;
+ }
+ }
+
+ ident = new PersonIdent(name, email, new Date(aga.getTime()), TimeZone.getDefault());
+ }
+ }
+ if (ident == null) {
+ ident = new PersonIdent(serverUser, new Date(aga.getTime()));
+ }
+
+ // Build the commits such that it keeps track of the date added and
+ // who added it.
+ commit = new CommitBuilder();
+ commit.setAuthor(ident);
+ commit.setCommitter(new PersonIdent(serverUser, new Date(aga.getTime())));
+
+ String msg = String.format("Accept %s contributor agreement for %s\n",
+ agreement.getName(), group.getName());
+ if (!Strings.isNullOrEmpty(aga.reviewComments)) {
+ msg += "\n" + aga.reviewComments + "\n";
+ }
+ commit.setMessage(msg);
+ batch.write(commit);
+ }
+
+ // Merge the agreements with the other data in project.config.
+ commit = new CommitBuilder();
+ commit.setAuthor(serverUser);
+ commit.setCommitter(serverUser);
+ commit.setMessage("Upgrade to Gerrit Code Review schema 65\n");
+ commit.addParentId(config.getRevision());
+ batch.write(config, commit);
+
+ // Save the the final metadata.
+ if (!batch.commitAt(config.getRevision())) {
+ throw new OrmException("Cannot update " + allProjects);
+ }
+ } finally {
+ batch.close();
+ }
+ } catch (IOException e) {
+ throw new OrmException(e);
+ } catch (ConfigInvalidException e) {
+ throw new OrmException(e);
+ } finally {
+ git.close();
+ }
+ }
+
+ private Map<Integer, ContributorAgreement> getAgreementToAdd(
+ ReviewDb db, ProjectConfig config) throws SQLException {
+ Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ ResultSet rs = stmt.executeQuery(
+ "SELECT short_name, id, require_contact_information," +
+ " short_description, agreement_url, auto_verify " +
+ "FROM contributor_agreements WHERE active = 'Y'");
+ try {
+ Map<Integer, ContributorAgreement> agreements = Maps.newHashMap();
+ while (rs.next()) {
+ String name = rs.getString(1);
+ if (config.getContributorAgreement(name) != null) {
+ continue; // already exists
+ }
+ ContributorAgreement a = config.getContributorAgreement(name, true);
+ agreements.put(rs.getInt(2), a);
+
+ a.setRequireContactInformation("Y".equals(rs.getString(3)));
+ a.setDescription(rs.getString(4));
+ a.setAgreementUrl(rs.getString(5));
+ if ("Y".equals(rs.getString(6))) {
+ a.setAutoVerify(new GroupReference(null, null));
+ }
+ }
+ return agreements;
+ } finally {
+ rs.close();
+ }
+ } finally {
+ stmt.close();
+ }
+ }
+
+ private AccountGroup createGroup(ReviewDb db, String groupName,
+ AccountGroup.UUID adminGroupUUID, String description)
+ throws OrmException {
+ final AccountGroup.Id groupId =
+ new AccountGroup.Id(db.nextAccountGroupId());
+ final AccountGroup.NameKey nameKey = new AccountGroup.NameKey(groupName);
+ final AccountGroup.UUID uuid = GroupUUID.make(groupName, serverUser);
+ final AccountGroup group = new AccountGroup(nameKey, groupId, uuid);
+ group.setOwnerGroupUUID(adminGroupUUID);
+ group.setDescription(description);
+ final AccountGroupName gn = new AccountGroupName(group);
+ // first insert the group name to validate that the group name hasn't
+ // already been used to create another group
+ db.accountGroupNames().insert(Collections.singleton(gn));
+ db.accountGroups().insert(Collections.singleton(group));
+ return group;
+ }
+
+ private List<AccountGroup.UUID> getAdministrateServerGroups(
+ ReviewDb db, ProjectConfig cfg) {
+ List<PermissionRule> rules = cfg.getAccessSection(AccessSection.GLOBAL_CAPABILITIES)
+ .getPermission(GlobalCapability.ADMINISTRATE_SERVER)
+ .getRules();
+
+ List<AccountGroup.UUID> groups =
+ Lists.newArrayListWithExpectedSize(rules.size());
+ for (PermissionRule rule : rules) {
+ if (rule.getAction() == Action.ALLOW) {
+ groups.add(rule.getGroup().getUUID());
+ }
+ }
+ if (groups.isEmpty()) {
+ throw new IllegalStateException("no administrator group found");
+ }
+
+ return groups;
+ }
+
+ private GroupReference getOrCreateGroupForIndividuals(ReviewDb db,
+ ProjectConfig config, List<AccountGroup.UUID> adminGroupUUIDs,
+ ContributorAgreement agreement)
+ throws OrmException {
+ if (!agreement.getAccepted().isEmpty()) {
+ return agreement.getAccepted().get(0).getGroup();
+ }
+
+ String name = "CLA Accepted - " + agreement.getName();
+ AccountGroupName agn =
+ db.accountGroupNames().get(new AccountGroup.NameKey(name));
+ AccountGroup ag;
+ if (agn != null) {
+ ag = db.accountGroups().get(agn.getId());
+ if (ag == null) {
+ throw new IllegalStateException(
+ "account group name exists but account group does not: " + name);
+ }
+
+ if (!adminGroupUUIDs.contains(ag.getOwnerGroupUUID())) {
+ throw new IllegalStateException(
+ "individual group exists with non admin owner group: " + name);
+ }
+ } else {
+ ag = createGroup(db, name, adminGroupUUIDs.get(0),
+ String.format("Users who have accepted the %s CLA", agreement.getName()));
+ }
+ GroupReference group = config.resolve(ag);
+ agreement.setAccepted(Lists.newArrayList(new PermissionRule(group)));
+ if (agreement.getAutoVerify() != null) {
+ agreement.setAutoVerify(group);
+ }
+
+ // Don't allow accounts in the same individual CLA group to see each
+ // other in same group visibility mode.
+ List<PermissionRule> sameGroupVisibility =
+ config.getAccountsSection().getSameGroupVisibility();
+ PermissionRule rule = new PermissionRule(group);
+ rule.setDeny();
+ if (!sameGroupVisibility.contains(rule)) {
+ sameGroupVisibility.add(rule);
+ }
+ return group;
+ }
+
+ private long addAccountAgreements(ReviewDb db, ProjectConfig config,
+ List<AccountGroup.UUID> adminGroupUUIDs,
+ Map<Integer, ContributorAgreement> agreements)
+ throws SQLException, OrmException {
+ Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ ResultSet rs = stmt.executeQuery(
+ "SELECT account_id, cla_id, accepted_on, reviewed_by," +
+ " reviewed_on, review_comments " +
+ "FROM account_agreements WHERE status = 'V'");
+ try {
+ long minTime = System.currentTimeMillis();
+ while (rs.next()) {
+ Account.Id accountId = new Account.Id(rs.getInt(1));
+ Account.Id reviewerId = new Account.Id(rs.getInt(4));
+ if (rs.wasNull()) {
+ reviewerId = accountId;
+ }
+
+ int claId = rs.getInt(2);
+ ContributorAgreement agreement = agreements.get(claId);
+ if (agreement == null) {
+ continue; // Agreement is invalid
+ }
+
+ Timestamp acceptedOn = rs.getTimestamp(3);
+ minTime = Math.min(minTime, acceptedOn.getTime());
+
+ // Enter Agreement
+ GroupReference individualGroup =
+ getOrCreateGroupForIndividuals(db, config, adminGroupUUIDs, agreement);
+ AccountGroup.Id groupId = db.accountGroups()
+ .byUUID(individualGroup.getUUID())
+ .toList()
+ .get(0)
+ .getId();
+
+ final AccountGroupMember.Key key =
+ new AccountGroupMember.Key(accountId, groupId);
+ AccountGroupMember m = db.accountGroupMembers().get(key);
+ if (m == null) {
+ m = new AccountGroupMember(key);
+ db.accountGroupMembersAudit().insert(
+ Collections.singleton(
+ new AccountGroupMemberAudit(m, reviewerId, acceptedOn)));
+ db.accountGroupMembers().insert(Collections.singleton(m));
+ }
+ }
+ return minTime;
+ } finally {
+ rs.close();
+ }
+ } finally {
+ stmt.close();
+ }
+ }
+
+ private static class AccountGroupAgreement {
+ private AccountGroup.Id groupId;
+ private int claId;
+ private Timestamp acceptedOn;
+ private Account.Id reviewedBy;
+ private Timestamp reviewedOn;
+ private String reviewComments;
+
+ private long getTime() {
+ return (reviewedOn == null) ? acceptedOn.getTime() : reviewedOn.getTime();
+ }
+ }
+
+ private List<AccountGroupAgreement> getAccountGroupAgreements(
+ ReviewDb db, Map<Integer, ContributorAgreement> agreements)
+ throws SQLException {
+
+ Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ ResultSet rs = stmt.executeQuery(
+ "SELECT group_id, cla_id, accepted_on, reviewed_by, reviewed_on, " +
+ " review_comments " +
+ "FROM account_group_agreements");
+ try {
+ List<AccountGroupAgreement> groupAgreements = Lists.newArrayList();
+ while (rs.next()) {
+ AccountGroupAgreement a = new AccountGroupAgreement();
+ a.groupId = new AccountGroup.Id(rs.getInt(1));
+ a.claId = rs.getInt(2);
+ if (!agreements.containsKey(a.claId)) {
+ continue; // Agreement is invalid
+ }
+ a.acceptedOn = rs.getTimestamp(3);
+ a.reviewedBy = new Account.Id(rs.getInt(4));
+ if (rs.wasNull()) {
+ a.reviewedBy = null;
+ }
+
+ a.reviewedOn = rs.getTimestamp(5);
+ if (rs.wasNull()) {
+ a.reviewedOn = null;
+ }
+
+ a.reviewComments = rs.getString(6);
+ if (rs.wasNull()) {
+ a.reviewComments = null;
+ }
+ groupAgreements.add(a);
+ }
+ Collections.sort(groupAgreements, new Comparator<AccountGroupAgreement>() {
+ @Override
+ public int compare(
+ AccountGroupAgreement a1, AccountGroupAgreement a2) {
+ return Longs.compare(a1.getTime(), a2.getTime());
+ }
+ });
+ return groupAgreements;
+ } finally {
+ rs.close();
+ }
+ } finally {
+ stmt.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java
new file mode 100644
index 0000000..94f5d2c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.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.server.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_66 extends SchemaVersion {
+
+ @Inject
+ Schema_66(Provider<Schema_65> prior) {
+ super(prior);
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui)
+ throws OrmException, SQLException {
+ final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ stmt.executeUpdate("UPDATE accounts SET reverse_patch_set_order = 'Y' "+
+ "WHERE display_patch_sets_in_reverse_order = 'Y'");
+ stmt.executeUpdate("UPDATE accounts SET show_username_in_review_category = 'Y' " +
+ "WHERE display_person_name_in_review_category = 'Y'");
+ } finally {
+ stmt.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java
new file mode 100644
index 0000000..7c7b880
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java
@@ -0,0 +1,95 @@
+// 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.schema;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+public class Schema_67 extends SchemaVersion {
+
+ @Inject
+ Schema_67(Provider<Schema_66> prior) {
+ super(prior);
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui)
+ throws OrmException, SQLException {
+ ui.message("Update ownerGroupId to ownerGroupUUID");
+
+ // Scan all AccountGroup, and find the ones that need the owner_group_id
+ // migrated to owner_group_uuid.
+ Map<AccountGroup.Id, AccountGroup.Id> idMap = Maps.newHashMap();
+ Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ ResultSet rs = stmt.executeQuery(
+ "SELECT group_id, owner_group_id FROM account_groups"
+ + " WHERE owner_group_uuid is NULL or owner_group_uuid =''");
+ try {
+ Map<Integer, ContributorAgreement> agreements = Maps.newHashMap();
+ while (rs.next()) {
+ AccountGroup.Id groupId = new AccountGroup.Id(rs.getInt(1));
+ AccountGroup.Id ownerId = new AccountGroup.Id(rs.getInt(2));
+ idMap.put(groupId, ownerId);
+ }
+ } finally {
+ rs.close();
+ }
+ } finally {
+ stmt.close();
+ }
+
+ // Lookup up all groups by ID.
+ Set<AccountGroup.Id> all =
+ Sets.newHashSet(Iterables.concat(idMap.keySet(), idMap.values()));
+ Map<AccountGroup.Id, AccountGroup> groups = Maps.newHashMap();
+ com.google.gwtorm.server.ResultSet<AccountGroup> rs =
+ db.accountGroups().get(all);
+ try {
+ for (AccountGroup group : rs) {
+ groups.put(group.getId(), group);
+ }
+ } finally {
+ rs.close();
+ }
+
+ // Update the ownerGroupUUID.
+ List<AccountGroup> toUpdate = Lists.newArrayListWithCapacity(idMap.size());
+ for (Entry<AccountGroup.Id, AccountGroup.Id> entry : idMap.entrySet()) {
+ AccountGroup group = groups.get(entry.getKey());
+ AccountGroup owner = groups.get(entry.getValue());
+ group.setOwnerGroupUUID(owner.getGroupUUID());
+ toUpdate.add(group);
+ }
+
+ db.accountGroups().update(toUpdate);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java
new file mode 100644
index 0000000..4dc2b6e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java
@@ -0,0 +1,60 @@
+// 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.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_68 extends SchemaVersion {
+ @Inject
+ Schema_68(Provider<Schema_67> prior) {
+ super(prior);
+ }
+
+ @Override
+ protected void migrateData(final ReviewDb db, final UpdateUI ui)
+ throws SQLException {
+ final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ stmt.execute("CREATE INDEX submodule_subscription_access_bySubscription"
+ + " ON submodule_subscriptions (submodule_project_name, submodule_branch_name)");
+ } catch (SQLException e) {
+ // the index creation might have failed because the index exists already,
+ // in this case the exception can be safely ignored,
+ // but there are also other possible reasons for an exception here that
+ // should not be ignored,
+ // -> ask the user whether to ignore this exception or not
+ ui.message("warning: Cannot create index for submodule subscriptions");
+ ui.message(e.getMessage());
+
+ if (ui.isBatch()) {
+ ui.message("you may ignore this warning when running in interactive mode");
+ throw e;
+ } else {
+ final boolean answer = ui.yesno(false, "Ignore warning and proceed with schema upgrade");
+ if (!answer) {
+ throw e;
+ }
+ }
+ } finally {
+ stmt.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java
new file mode 100644
index 0000000..3d6b93a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java
@@ -0,0 +1,230 @@
+// 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.schema;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapName;
+
+public class Schema_69 extends SchemaVersion {
+ private final GitRepositoryManager mgr;
+ private final PersonIdent serverUser;
+
+ @Inject
+ Schema_69(Provider<Schema_68> prior,
+ GitRepositoryManager mgr,
+ @GerritPersonIdent PersonIdent serverUser) {
+ super(prior);
+ this.mgr = mgr;
+ this.serverUser = serverUser;
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui)
+ throws OrmException, SQLException {
+
+ // Find all groups that have an LDAP type.
+ Map<AccountGroup.UUID, GroupReference> ldapUUIDMap = Maps.newHashMap();
+ Set<AccountGroup.UUID> toResolve = Sets.newHashSet();
+ List<AccountGroup.Id> toDelete = Lists.newArrayList();
+ List<AccountGroup.NameKey> namesToDelete = Lists.newArrayList();
+ Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+ try {
+ ResultSet rs = stmt.executeQuery(
+ "SELECT group_id, group_uuid, external_name, name FROM account_groups"
+ + " WHERE group_type ='LDAP'");
+ try {
+ while (rs.next()) {
+ AccountGroup.Id groupId = new AccountGroup.Id(rs.getInt(1));
+ AccountGroup.UUID groupUUID = new AccountGroup.UUID(rs.getString(2));
+ AccountGroup.NameKey name = new AccountGroup.NameKey(rs.getString(4));
+ String dn = rs.getString(3);
+
+ if (isNullOrEmpty(dn)) {
+ // The LDAP group does not have a DN. Determine if the UUID is used.
+ toResolve.add(groupUUID);
+ } else {
+ toDelete.add(groupId);
+ namesToDelete.add(name);
+ GroupReference ref = groupReference(dn);
+ ldapUUIDMap.put(groupUUID, ref);
+ }
+ }
+ } catch (NamingException e) {
+ throw new RuntimeException(e);
+ } finally {
+ rs.close();
+ }
+ } finally {
+ stmt.close();
+ }
+ if (toDelete.isEmpty() && toResolve.isEmpty()) {
+ return; // No ldap groups. Nothing to do.
+ }
+
+ ui.message("Update LDAP groups to be GroupReferences.");
+
+ // Update the groupOwnerUUID for LDAP groups to point to the new UUID.
+ List<AccountGroup> toUpdate = Lists.newArrayList();
+ Set<AccountGroup.UUID> resolveToUpdate = Sets.newHashSet();
+ Map<AccountGroup.UUID, AccountGroup> resolveGroups = Maps.newHashMap();
+ for (AccountGroup g : db.accountGroups().all()) {
+ if (ldapUUIDMap.containsKey(g.getGroupUUID())) {
+ continue; // Ignore the LDAP groups with a valid DN.
+ } else if (toResolve.contains(g.getGroupUUID())) {
+ resolveGroups.put(g.getGroupUUID(), g); // Keep the ones to resolve.
+ continue;
+ }
+
+ GroupReference ref = ldapUUIDMap.get(g.getOwnerGroupUUID());
+ if (ref != null) {
+ // Update the owner group UUID to the new ldap UUID scheme.
+ g.setOwnerGroupUUID(ref.getUUID());
+ toUpdate.add(g);
+ } else if (toResolve.contains(g.getOwnerGroupUUID())) {
+ // The unresolved group is used as an owner.
+ // Add to the list of LDAP groups to be made INTERNAL.
+ resolveToUpdate.add(g.getOwnerGroupUUID());
+ }
+ }
+
+ toResolve.removeAll(resolveToUpdate);
+
+ // Update project.config group references to use the new LDAP GroupReference
+ for (Project.NameKey name : mgr.list()) {
+ Repository git;
+ try {
+ git = mgr.openRepository(name);
+ } catch (RepositoryNotFoundException e) {
+ throw new OrmException(e);
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+
+ try {
+ MetaDataUpdate md =
+ new MetaDataUpdate(GitReferenceUpdated.DISABLED, name, git);
+ md.getCommitBuilder().setAuthor(serverUser);
+ md.getCommitBuilder().setCommitter(serverUser);
+
+ ProjectConfig config = ProjectConfig.read(md);
+
+ // Update the existing refences to the new reference.
+ boolean updated = false;
+ for (Map.Entry<AccountGroup.UUID, GroupReference> entry: ldapUUIDMap.entrySet()) {
+ GroupReference ref = config.getGroup(entry.getKey());
+ if (ref != null) {
+ updated = true;
+ ref.setName(entry.getValue().getName());
+ ref.setUUID(entry.getValue().getUUID());
+ config.resolve(ref);
+ }
+ }
+
+ // Determine if a toResolve group is used and should be made INTERNAL.
+ Iterator<AccountGroup.UUID> iter = toResolve.iterator();
+ while (iter.hasNext()) {
+ AccountGroup.UUID uuid = iter.next();
+ if (config.getGroup(uuid) != null) {
+ resolveToUpdate.add(uuid);
+ iter.remove();
+ }
+ }
+
+ if (!updated) {
+ continue;
+ }
+
+ md.setMessage("Switch LDAP group UUIDs to DNs\n");
+ if (!config.commit(md)) {
+ throw new OrmException("Cannot update " + name);
+ }
+ } catch (IOException e) {
+ throw new OrmException(e);
+ } catch (ConfigInvalidException e) {
+ throw new OrmException(e);
+ } finally {
+ git.close();
+ }
+ }
+
+ for (AccountGroup.UUID uuid : resolveToUpdate) {
+ AccountGroup group = resolveGroups.get(uuid);
+ group.setType(AccountGroup.Type.INTERNAL);
+ toUpdate.add(group);
+
+ ui.message(String.format(
+ "*** Group has no DN and is inuse. Updated to be INTERNAL: %s",
+ group.getName()));
+ }
+
+ for (AccountGroup.UUID uuid : toResolve) {
+ AccountGroup group = resolveGroups.get(uuid);
+ toDelete.add(group.getId());
+ namesToDelete.add(group.getNameKey());
+ }
+
+ // Update group owners
+ db.accountGroups().update(toUpdate);
+ // Delete existing LDAP groups
+ db.accountGroupNames().deleteKeys(namesToDelete);
+ db.accountGroups().deleteKeys(toDelete);
+ }
+
+ private static GroupReference groupReference(String dn)
+ throws NamingException {
+ LdapName name = new LdapName(dn);
+ Preconditions.checkState(!name.isEmpty(), "Invalid LDAP dn: %s", dn);
+ String cn = name.get(name.size() - 1);
+ int index = cn.indexOf('=');
+ if (index >= 0) {
+ cn = cn.substring(index + 1);
+ }
+ return new GroupReference(new AccountGroup.UUID("ldap:" + dn), "ldap/" + cn);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
index 64b3afa..eff5575 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
@@ -24,6 +24,8 @@
boolean yesno(boolean def, String msg);
+ boolean isBatch();
+
void pruneSchema(StatementExecutor e, List<String> pruneList)
throws OrmException;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
new file mode 100644
index 0000000..686a108
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.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.server.util;
+
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * The default RequestContext to use when not in a request scope e.g.
+ * ThreadLocalRequestContext is not set.
+ */
+@Singleton
+public class FallbackRequestContext implements RequestContext {
+
+ private final AnonymousUser user;
+
+ @Inject
+ FallbackRequestContext(AnonymousUser user) {
+ this.user = user;
+ }
+
+ @Override
+ public CurrentUser getCurrentUser() {
+ return user;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
index 9befc7d..fa07176 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.util;
import com.google.common.collect.Maps;
-import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.inject.Inject;
@@ -38,17 +37,15 @@
private final String url;
private final SocketAddress peer;
- private final CurrentUser user;
@Inject
GuiceRequestScopePropagator(
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
@RemotePeer Provider<SocketAddress> remotePeerProvider,
- Provider<CurrentUser> currentUserProvider) {
- super(ServletScopes.REQUEST);
+ ThreadLocalRequestContext local) {
+ super(ServletScopes.REQUEST, local);
this.url = urlProvider != null ? urlProvider.get() : null;
this.peer = remotePeerProvider.get();
- this.user = currentUserProvider.get();
}
/**
@@ -69,9 +66,6 @@
Providers.of(peer));
seedMap.put(Key.get(SocketAddress.class, RemotePeer.class), peer);
- seedMap.put(Key.get(typeOfProvider(CurrentUser.class)), Providers.of(user));
- seedMap.put(Key.get(CurrentUser.class), user);
-
return ServletScopes.continueRequest(callable, seedMap);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
similarity index 63%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
index 43039e1..ca8573f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.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.
@@ -12,11 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.util;
+import com.google.gerrit.server.CurrentUser;
-/** Configure a cache declared within a {@link CacheModule} instance. */
-public interface UnnamedCacheBinding<K, V> {
- /** Set the name of the cache. */
- public NamedCacheBinding<K, V> name(String cacheName);
+/**
+ * The RequestContext is an interface exposing the fields that are needed
+ * by the GerritGlobalModule scope.
+ */
+public interface RequestContext {
+
+ CurrentUser getCurrentUser();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
index 3661aa2..84c61e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -42,9 +42,12 @@
public abstract class RequestScopePropagator {
private final Scope scope;
+ private final ThreadLocalRequestContext local;
- protected RequestScopePropagator(Scope scope) {
+ protected RequestScopePropagator(Scope scope,
+ ThreadLocalRequestContext local) {
this.scope = scope;
+ this.local = local;
}
/**
@@ -70,26 +73,8 @@
* @return a new Callable which will execute in the current request scope.
*/
public final <T> Callable<T> wrap(final Callable<T> callable) {
- final Callable<T> wrapped = wrapImpl(new Callable<T>() {
- @Override
- public T call() throws Exception {
- RequestCleanup cleanup = scope.scope(
- Key.get(RequestCleanup.class),
- new Provider<RequestCleanup>() {
- @Override
- public RequestCleanup get() {
- return new RequestCleanup();
- }
- }).get();
-
- try {
- return callable.call();
- } finally {
- cleanup.run();
- }
- }
- });
-
+ final Callable<T> wrapped =
+ wrapImpl(context(local.getContext(), cleanup(callable)));
return new Callable<T>() {
@Override
public T call() throws Exception {
@@ -178,4 +163,41 @@
* @see #wrap(Callable)
*/
protected abstract <T> Callable<T> wrapImpl(final Callable<T> callable);
+
+ protected <T> Callable<T> context(final RequestContext context,
+ final Callable<T> callable) {
+ return new Callable<T>() {
+ @Override
+ public T call() throws Exception {
+ RequestContext old = local.setContext(context);
+ try {
+ return callable.call();
+ } finally {
+ local.setContext(old);
+ }
+ }
+ };
+ }
+
+ protected <T> Callable<T> cleanup(final Callable<T> callable) {
+ return new Callable<T>() {
+ @Override
+ public T call() throws Exception {
+ RequestCleanup cleanup = scope.scope(
+ Key.get(RequestCleanup.class),
+ new Provider<RequestCleanup>() {
+ @Override
+ public RequestCleanup get() {
+ return new RequestCleanup();
+ }
+ }).get();
+
+ try {
+ return callable.call();
+ } finally {
+ cleanup.run();
+ }
+ }
+ };
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
new file mode 100644
index 0000000..b411512
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
@@ -0,0 +1,87 @@
+// 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.util;
+
+import com.google.common.base.Objects;
+import com.google.gerrit.common.errors.NotSignedInException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.ProvisionException;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import javax.annotation.Nullable;
+
+/**
+ * ThreadLocalRequestContext manages the current RequestContext using a
+ * ThreadLocal. When the context is set, the fields exposed by the context
+ * are considered in scope. Otherwise, the FallbackRequestContext is used.
+ */
+public class ThreadLocalRequestContext {
+ private static final String FALLBACK = "FALLBACK";
+
+ public static Module module() {
+ return new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(ThreadLocalRequestContext.class);
+ bind(RequestContext.class).annotatedWith(Names.named(FALLBACK))
+ .to(FallbackRequestContext.class);
+ }
+
+ @Provides
+ RequestContext provideRequestContext(
+ @Named(FALLBACK) RequestContext fallback) {
+ return Objects.firstNonNull(local.get(), fallback);
+ }
+
+ @Provides
+ CurrentUser provideCurrentUser(RequestContext ctx) {
+ return ctx.getCurrentUser();
+ }
+
+ @Provides
+ IdentifiedUser provideCurrentUser(CurrentUser user) {
+ if (user instanceof IdentifiedUser) {
+ return (IdentifiedUser) user;
+ }
+ throw new ProvisionException(NotSignedInException.MESSAGE,
+ new NotSignedInException());
+ }
+ };
+ }
+
+ private static final ThreadLocal<RequestContext> local =
+ new ThreadLocal<RequestContext>();
+
+ @Inject
+ ThreadLocalRequestContext() {
+ }
+
+ public RequestContext setContext(@Nullable RequestContext ctx) {
+ RequestContext old = getContext();
+ local.set(ctx);
+ return old;
+ }
+
+ @Nullable
+ public RequestContext getContext() {
+ return local.get();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
index 581ccc1..e465247 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
@@ -31,8 +31,8 @@
private final ThreadLocal<C> threadLocal;
protected ThreadLocalRequestScopePropagator(Scope scope,
- ThreadLocal<C> threadLocal) {
- super(scope);
+ ThreadLocal<C> threadLocal, ThreadLocalRequestContext local) {
+ super(scope, local);
this.threadLocal = threadLocal;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
index d08bd1f..74c97f3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
@@ -26,7 +26,6 @@
import com.google.gerrit.reviewdb.client.ApprovalCategory.Id;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.project.ChangeControl;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -57,7 +56,7 @@
@Inject
FunctionState(final ApprovalTypes approvalTypes,
- final IdentifiedUser.GenericFactory userFactory, final GroupCache egc,
+ final IdentifiedUser.GenericFactory userFactory,
@Assisted final ChangeControl c, @Assisted final PatchSet.Id psId,
@Assisted final Collection<PatchSetApproval> all) {
this.approvalTypes = approvalTypes;
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/main/java/gerrit/PRED__load_commit_labels_1.java b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
index c760426..a0bb820 100644
--- a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
@@ -9,7 +9,9 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.rules.PrologEnvironment;
import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.util.Providers;
import com.googlecode.prolog_cafe.lang.IntegerTerm;
import com.googlecode.prolog_cafe.lang.JavaException;
@@ -44,10 +46,18 @@
try {
PrologEnvironment env = (PrologEnvironment) engine.control;
ReviewDb db = StoredValues.REVIEW_DB.get(engine);
- PatchSet.Id patchSetId = StoredValues.PATCH_SET_ID.get(engine);
+ PatchSet patchSet = StoredValues.PATCH_SET.get(engine);
+ ChangeData cd = StoredValues.CHANGE_DATA.getOrNull(engine);
ApprovalTypes types = env.getInjector().getInstance(ApprovalTypes.class);
- for (PatchSetApproval a : db.patchSetApprovals().byPatchSet(patchSetId)) {
+ Iterable<PatchSetApproval> approvals;
+ if (cd != null) {
+ approvals = cd.currentApprovals(Providers.of(db));
+ } else {
+ approvals = db.patchSetApprovals().byPatchSet(patchSet.getId());
+ }
+
+ for (PatchSetApproval a : approvals) {
if (a.getValue() == 0) {
continue;
}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
index 23cedce..8c70cad 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
@@ -20,7 +20,6 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PeerDaemonUser;
-import com.google.gerrit.server.ReplicationUser;
import com.google.gerrit.server.project.ChangeControl;
import com.googlecode.prolog_cafe.lang.EvaluationException;
@@ -61,8 +60,6 @@
resultTerm = anonymous;
} else if (curUser instanceof PeerDaemonUser) {
resultTerm = peerDaemon;
- } else if (curUser instanceof ReplicationUser) {
- resultTerm = replication;
} else {
throw new EvaluationException("Unknown user type");
}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
index 0a15608..1359de1 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
@@ -20,15 +20,12 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.rules.PrologEnvironment;
import com.google.gerrit.rules.StoredValues;
-import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Provider;
+import com.google.inject.util.Providers;
-import com.googlecode.prolog_cafe.lang.HashtableOfTerm;
import com.googlecode.prolog_cafe.lang.IllegalTypeException;
import com.googlecode.prolog_cafe.lang.IntegerTerm;
-import com.googlecode.prolog_cafe.lang.InternalException;
import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
import com.googlecode.prolog_cafe.lang.Operation;
import com.googlecode.prolog_cafe.lang.PInstantiationException;
@@ -39,6 +36,8 @@
import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;
+import java.util.Map;
+
/**
* Loads a CurrentUser object for a user identity.
* <p>
@@ -53,7 +52,6 @@
private static final long serialVersionUID = 1L;
private static final SymbolTerm user = intern("user", 1);
private static final SymbolTerm anonymous = intern("anonymous");
- private static final SymbolTerm current_user = intern("current_user");
PRED_current_user_2(Term a1, Term a2, Operation n) {
arg1 = a1;
@@ -71,24 +69,14 @@
throw new PInstantiationException(this, 1);
}
- HashtableOfTerm userHash = userHash(engine);
- Term userTerm = userHash.get(a1);
- if (userTerm != null && userTerm.isJavaObject()) {
- if (!(((JavaObjectTerm) userTerm).object() instanceof CurrentUser)) {
- userTerm = createUser(engine, a1, userHash);
- }
- } else {
- userTerm = createUser(engine, a1, userHash);
- }
-
- if (!a2.unify(userTerm, engine.trail)) {
+ if (!a2.unify(createUser(engine, a1), engine.trail)) {
return engine.fail();
}
return cont;
}
- public Term createUser(Prolog engine, Term key, HashtableOfTerm userHash) {
+ public Term createUser(Prolog engine, Term key) {
if (!key.isStructure()
|| key.arity() != 1
|| !((StructureTerm) key).functor().equals(user)) {
@@ -98,54 +86,30 @@
Term idTerm = key.arg(0);
CurrentUser user;
if (idTerm.isInteger()) {
+ Map<Account.Id, IdentifiedUser> cache = StoredValues.USERS.get(engine);
Account.Id accountId = new Account.Id(((IntegerTerm) idTerm).intValue());
-
- final ReviewDb db = StoredValues.REVIEW_DB.getOrNull(engine);
- IdentifiedUser.GenericFactory userFactory = userFactory(engine);
- if (db != null) {
- user = userFactory.create(new Provider<ReviewDb>() {
- public ReviewDb get() {
- return db;
- }
- }, accountId);
- } else {
- user = userFactory.create(accountId);
+ user = cache.get(accountId);
+ if (user == null) {
+ ReviewDb db = StoredValues.REVIEW_DB.getOrNull(engine);
+ IdentifiedUser.GenericFactory userFactory = userFactory(engine);
+ IdentifiedUser who;
+ if (db != null) {
+ who = userFactory.create(Providers.of(db), accountId);
+ } else {
+ who = userFactory.create(accountId);
+ }
+ cache.put(accountId, who);
+ user = who;
}
-
} else if (idTerm.equals(anonymous)) {
- user = anonymousUser(engine);
+ user = StoredValues.ANONYMOUS_USER.get(engine);
} else {
throw new IllegalTypeException(this, 1, "user(int)", key);
}
- Term userTerm = new JavaObjectTerm(user);
- userHash.put(key, userTerm);
- return userTerm;
- }
-
- private static HashtableOfTerm userHash(Prolog engine) {
- Term userHash = engine.getHashManager().get(current_user);
- if (userHash == null) {
- HashtableOfTerm users = new HashtableOfTerm();
- engine.getHashManager().put(current_user, new JavaObjectTerm(userHash));
- return users;
- }
-
- if (userHash.isJavaObject()) {
- Object obj = ((JavaObjectTerm) userHash).object();
- if (obj instanceof HashtableOfTerm) {
- return (HashtableOfTerm) obj;
- }
- }
-
- throw new InternalException(current_user + " is not HashtableOfTerm");
- }
-
- private static AnonymousUser anonymousUser(Prolog engine) {
- PrologEnvironment env = (PrologEnvironment) engine.control;
- return env.getInjector().getInstance(AnonymousUser.class);
+ return new JavaObjectTerm(user);
}
private static IdentifiedUser.GenericFactory userFactory(Prolog engine) {
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl
index 3313162..a75acc0 100644
--- a/gerrit-server/src/main/prolog/gerrit_common.pl
+++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -25,8 +25,7 @@
%% predicate that needs to obtain it.
%%
init :-
- define_hash(commit_labels),
- define_hash(current_user).
+ define_hash(commit_labels).
define_hash(A) :- hash_exists(A), !, hash_clear(A).
define_hash(A) :- atom(A), !, new_hash(_, [alias(A)]).
@@ -98,6 +97,10 @@
%% Lookup the range allowed to be used.
%%
user_label_range(Label, Who, Min, Max) :-
+ hash_get(commit_labels, '$fast_range', true), !,
+ atom(Label),
+ assume_range_from_label(Label, Who, Min, Max).
+user_label_range(Label, Who, Min, Max) :-
Who = user(_), !,
atom(Label),
current_user(Who, User),
@@ -106,6 +109,14 @@
clause(user:test_grant(Label, test_user(Name), range(Min, Max)), _)
.
+assume_range_from_label :-
+ hash_put(commit_labels, '$fast_range', true).
+
+assume_range_from_label(Label, Who, Min, Max) :-
+ commit_label(label(Label, Value), Who), !,
+ Min = Value, Max = Value.
+assume_range_from_label(_, _, 0, 0).
+
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%
@@ -138,6 +149,7 @@
is_all_ok([]).
is_all_ok([label(_, ok(__)) | Ls]) :- is_all_ok(Ls).
+is_all_ok([label(_, may(__)) | Ls]) :- is_all_ok(Ls).
is_all_ok(_) :- fail.
@@ -198,8 +210,8 @@
%%
legacy_submit_rule('MaxWithBlock', Label, Id, Min, Max, T) :- !, max_with_block(Label, Min, Max, T).
legacy_submit_rule('MaxNoBlock', Label, Id, Min, Max, T) :- !, max_no_block(Label, Max, T).
-legacy_submit_rule('NoBlock', Label, Id, Min, Max, T) :- !, T = ok(_).
-legacy_submit_rule('NoOp', Label, Id, Min, Max, T) :- !, T = ok(_).
+legacy_submit_rule('NoBlock', Label, Id, Min, Max, T) :- !, T = may(_).
+legacy_submit_rule('NoOp', Label, Id, Min, Max, T) :- !, T = may(_).
legacy_submit_rule(Fun, Label, Id, Min, Max, T) :- T = impossible(unsupported(Fun)).
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/documentation/pegdown.css b/gerrit-server/src/main/resources/com/google/gerrit/server/documentation/pegdown.css
new file mode 100644
index 0000000..eada653
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/documentation/pegdown.css
@@ -0,0 +1,39 @@
+body {
+ margin: 1em;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: #527bbd;
+ font-family: sans-serif;
+}
+
+h1, h2, h3 {
+ border-bottom: 2px solid silver;
+}
+
+pre {
+ border: 2px solid silver;
+ background: #ebebeb;
+ margin-left: 2em;
+ width: 100em;
+ color: darkgreen;
+ padding: 2px;
+}
+
+dl dt {
+ margin-top: 1em;
+}
+
+table.plugin_info {
+ border-collapse: separate;
+ border-spacing: 0;
+ text-align: left;
+ margin-left: 2em;
+}
+table.plugin_info th {
+ padding-right: 0.5em;
+ border-right: 2px solid silver;
+}
+table.plugin_info td {
+ padding-left: 0.5em;
+}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
index f2f0fc76..1eb6842 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
@@ -31,7 +31,8 @@
## The ChangeFooter.vm template will determine the contents of the footer
## text that will be appended to ALL emails related to changes.
##
---
+#set ($SPACE = " ")
+--$SPACE
#if ($email.changeUrl)
To view, visit $email.changeUrl
#set ($notblank = 1)
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
index ca2b03a..02bf815 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -22,11 +22,13 @@
import static org.junit.Assert.fail;
import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -43,6 +45,7 @@
import org.junit.Test;
import java.io.IOException;
+import java.util.Collections;
public class ProjectConfigTest extends LocalDiskRepositoryTestCase {
private final GroupReference developers = new GroupReference(
@@ -70,10 +73,31 @@
+ " exclusiveGroupPermissions = read submit create\n" //
+ " submit = group Developers\n" //
+ " push = group Developers\n" //
- + " read = group Developers\n")) //
+ + " read = group Developers\n" //
+ + "[accounts]\n" //
+ + " sameGroupVisibility = deny group Developers\n" //
+ + " sameGroupVisibility = block group Staff\n" //
+ + "[contributor-agreement \"Individual\"]\n" //
+ + " description = A simple description\n" //
+ + " accepted = group Developers\n" //
+ + " accepted = group Staff\n" //
+ + " requireContactInformation = true\n" //
+ + " autoVerify = group Developers\n" //
+ + " agreementUrl = http://www.example.com/agree\n")) //
));
ProjectConfig cfg = read(rev);
+ assertEquals(2, cfg.getAccountsSection().getSameGroupVisibility().size());
+ ContributorAgreement ca = cfg.getContributorAgreement("Individual");
+ assertEquals("Individual", ca.getName());
+ assertEquals("A simple description", ca.getDescription());
+ assertEquals("http://www.example.com/agree", ca.getAgreementUrl());
+ assertEquals(2, ca.getAccepted().size());
+ assertEquals(developers, ca.getAccepted().get(0).getGroup());
+ assertEquals("Staff", ca.getAccepted().get(1).getGroup().getName());
+ assertEquals("Developers", ca.getAutoVerify().getName());
+ assertTrue(ca.isRequireContactInformation());
+
AccessSection section = cfg.getAccessSection("refs/heads/*");
assertNotNull("has refs/heads/*", section);
assertNull("no refs/*", cfg.getAccessSection("refs/*"));
@@ -98,14 +122,30 @@
+ " exclusiveGroupPermissions = read submit\n" //
+ " submit = group Developers\n" //
+ " upload = group Developers\n" //
- + " read = group Developers\n")) //
+ + " read = group Developers\n" //
+ + "[accounts]\n" //
+ + " sameGroupVisibility = deny group Developers\n" //
+ + " sameGroupVisibility = block group Staff\n" //
+ + "[contributor-agreement \"Individual\"]\n" //
+ + " description = A simple description\n" //
+ + " accepted = group Developers\n" //
+ + " requireContactInformation = true\n" //
+ + " autoVerify = group Developers\n" //
+ + " agreementUrl = http://www.example.com/agree\n")) //
));
update(rev);
ProjectConfig cfg = read(rev);
AccessSection section = cfg.getAccessSection("refs/heads/*");
+ cfg.getAccountsSection().setSameGroupVisibility(
+ Collections.singletonList(new PermissionRule(cfg.resolve(staff))));
Permission submit = section.getPermission(Permission.SUBMIT);
submit.add(new PermissionRule(cfg.resolve(staff)));
+ ContributorAgreement ca = cfg.getContributorAgreement("Individual");
+ ca.setRequireContactInformation(false);
+ ca.setAccepted(Collections.singletonList(new PermissionRule(cfg.resolve(staff))));
+ ca.setAutoVerify(null);
+ ca.setDescription("A new description");
rev = commit(cfg);
assertEquals(""//
+ "[access \"refs/heads/*\"]\n" //
@@ -114,6 +154,12 @@
+ "\tsubmit = group Staff\n" //
+ " upload = group Developers\n" //
+ " read = group Developers\n"//
+ + "[accounts]\n" //
+ + " sameGroupVisibility = group Staff\n" //
+ + "[contributor-agreement \"Individual\"]\n" //
+ + " description = A new description\n" //
+ + " accepted = group Staff\n" //
+ + " agreementUrl = http://www.example.com/agree\n" //
+ "[project]\n"//
+ "\tstate = active\n", text(rev, "project.config"));
}
@@ -156,8 +202,9 @@
private RevCommit commit(ProjectConfig cfg) throws IOException,
MissingObjectException, IncorrectObjectTypeException {
- MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), //
- cfg.getProject().getNameKey(), //
+ MetaDataUpdate md = new MetaDataUpdate(
+ GitReferenceUpdated.DISABLED,
+ cfg.getProject().getNameKey(),
db);
util.tick(5);
util.setAuthorAndCommitter(md.getCommitBuilder());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java
deleted file mode 100644
index 7ae705f..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2011 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.git;
-
-import static com.google.gerrit.server.git.PushReplication.ReplicationConfig.encode;
-import static com.google.gerrit.server.git.PushReplication.ReplicationConfig.needsUrlEncoding;
-
-import junit.framework.TestCase;
-
-import org.eclipse.jgit.transport.URIish;
-
-import java.net.URISyntaxException;
-
-public class PushReplicationTest extends TestCase {
- public void testNeedsUrlEncoding() throws URISyntaxException {
- assertTrue(needsUrlEncoding(new URIish("http://host/path")));
- assertTrue(needsUrlEncoding(new URIish("https://host/path")));
- assertTrue(needsUrlEncoding(new URIish("amazon-s3://config/bucket/path")));
-
- assertFalse(needsUrlEncoding(new URIish("host:path")));
- assertFalse(needsUrlEncoding(new URIish("user@host:path")));
- assertFalse(needsUrlEncoding(new URIish("git://host/path")));
- assertFalse(needsUrlEncoding(new URIish("ssh://host/path")));
- }
-
- public void testUrlEncoding() {
- assertEquals("foo/bar/thing", encode("foo/bar/thing"));
- assertEquals("--%20All%20Projects%20--", encode("-- All Projects --"));
- assertEquals("name/with%20a%20space", encode("name/with a space"));
- assertEquals("name%0Awith-LF", encode("name\nwith-LF"));
- }
-}
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..8d061eb 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
@@ -26,6 +26,7 @@
import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.SubmoduleSubscriptionAccess;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gwtorm.client.KeyUtil;
import com.google.gwtorm.server.ListResultSet;
import com.google.gwtorm.server.ResultSet;
@@ -71,8 +72,9 @@
private ReviewDb schema;
private Provider<String> urlProvider;
private GitRepositoryManager repoManager;
- private ReplicationQueue replication;
+ private GitReferenceUpdated replication;
+ @SuppressWarnings("unchecked")
@Override
@Before
public void setUp() throws Exception {
@@ -83,7 +85,7 @@
subscriptions = createStrictMock(SubmoduleSubscriptionAccess.class);
urlProvider = createStrictMock(Provider.class);
repoManager = createStrictMock(GitRepositoryManager.class);
- replication = createStrictMock(ReplicationQueue.class);
+ replication = createStrictMock(GitReferenceUpdated.class);
}
private void doReplay() {
@@ -637,7 +639,7 @@
expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
.andReturn(targetRepository);
- replication.scheduleUpdate(targetBranchNameKey.getParentKey(),
+ replication.fire(targetBranchNameKey.getParentKey(),
targetBranchNameKey.get());
expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
@@ -738,7 +740,7 @@
expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
.andReturn(targetRepository);
- replication.scheduleUpdate(targetBranchNameKey.getParentKey(),
+ replication.fire(targetBranchNameKey.getParentKey(),
targetBranchNameKey.get());
expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
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 77150e6..e4d9418 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
@@ -20,9 +20,9 @@
import static com.google.gerrit.common.data.Permission.READ;
import static com.google.gerrit.common.data.Permission.SUBMIT;
-import com.google.common.collect.Iterables;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
import com.google.gerrit.common.data.Capable;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.PermissionRange;
@@ -31,22 +31,18 @@
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;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.FactoryModule;
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;
@@ -55,11 +51,9 @@
import org.eclipse.jgit.lib.Config;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@@ -285,6 +279,10 @@
}
@Override
+ public void remove(Project p) {
+ }
+
+ @Override
public Iterable<Project.NameKey> all() {
return Collections.emptySet();
}
@@ -324,10 +322,9 @@
local.createInMemory();
local.getProject().setParentName(parent.getProject().getName());
- sectionSorter =
- new PermissionCollection.Factory(
- new SectionSortCache(
- new ConcurrentHashMapCache<SectionSortCache.EntryKey, SectionSortCache.EntryVal>()));
+ Cache<SectionSortCache.EntryKey, SectionSortCache.EntryVal> c =
+ CacheBuilder.newBuilder().build();
+ sectionSorter = new PermissionCollection.Factory(new SectionSortCache(c));
}
private static void assertOwner(String ref, ProjectControl u) {
@@ -379,12 +376,10 @@
}
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(),
- Collections.<AccountGroup.UUID> emptySet(), schema, groupCache,
+ Collections.<AccountGroup.UUID> emptySet(), projectCache,
sectionSorter,
canonicalWebUrl, new MockUser(name, memberOf),
newProjectState());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
index 39cbfe4..cc8d47d 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -19,6 +19,9 @@
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.GerritPersonIdentProvider;
import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.AnonymousCowardNameProvider;
+import com.google.gerrit.server.config.FactoryModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -27,7 +30,6 @@
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.TypeLiteral;
@@ -63,7 +65,7 @@
final File site = new File(UUID.randomUUID().toString());
final SitePaths paths = new SitePaths(site);
- SchemaUpdater u = Guice.createInjector(new AbstractModule() {
+ SchemaUpdater u = Guice.createInjector(new FactoryModule() {
@Override
protected void configure() {
bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).toInstance(db);
@@ -88,6 +90,10 @@
bind(GitRepositoryManager.class) //
.to(LocalDiskRepositoryManager.class);
+
+ bind(String.class) //
+ .annotatedWith(AnonymousCowardName.class) //
+ .toProvider(AnonymousCowardNameProvider.class);
}
}).getInstance(SchemaUpdater.class);
@@ -102,6 +108,11 @@
}
@Override
+ public boolean isBatch() {
+ return true;
+ }
+
+ @Override
public void pruneSchema(StatementExecutor e, List<String> pruneList)
throws OrmException {
for (String sql : pruneList) {
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-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
index a44f84f..b1f956f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
@@ -20,6 +20,8 @@
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.GerritPersonIdentProvider;
import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.AnonymousCowardNameProvider;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -122,6 +124,10 @@
bind(GitRepositoryManager.class) //
.to(LocalDiskRepositoryManager.class);
+
+ bind(String.class) //
+ .annotatedWith(AnonymousCowardName.class) //
+ .toProvider(AnonymousCowardNameProvider.class);
}
}).getBinding(Key.get(SchemaVersion.class, Current.class))
.getProvider().get();
diff --git a/gerrit-sshd/.gitignore b/gerrit-sshd/.gitignore
index 194bedc..8deb9bd 100644
--- a/gerrit-sshd/.gitignore
+++ b/gerrit-sshd/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-sshd.iml
\ No newline at end of file
diff --git a/gerrit-sshd/.settings/org.eclipse.core.resources.prefs b/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
index c780f44..0871ea8 100644
--- a/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,6 @@
-#Thu Jul 28 11:02:36 PDT 2011
+#Tue May 15 09:21:12 PDT 2012
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding//src/test/java=UTF-8
encoding/<project>=UTF-8
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index 55e8725..31b2422 100644
--- a/gerrit-sshd/pom.xml
+++ b/gerrit-sshd/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-sshd</artifactId>
@@ -67,7 +67,7 @@
<dependency>
<groupId>com.google.gerrit</groupId>
- <artifactId>gerrit-ehcache</artifactId>
+ <artifactId>gerrit-cache-h2</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
index eb8a5c2..af5df25 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -36,6 +36,9 @@
protected ProjectControl projectControl;
@Inject
+ private SshScope sshScope;
+
+ @Inject
private GitRepositoryManager repoManager;
@Inject
@@ -56,7 +59,7 @@
@Override
public void start(final Environment env) {
Context ctx = context.subContext(newSession(), context.getCommandLine());
- final Context old = SshScope.set(ctx);
+ final Context old = sshScope.set(ctx);
try {
startThread(new ProjectCommandRunnable() {
@Override
@@ -76,7 +79,7 @@
}
});
} finally {
- SshScope.set(old);
+ sshScope.set(old);
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
new file mode 100644
index 0000000..d28d102
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
@@ -0,0 +1,124 @@
+// 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.collect.Lists;
+import com.google.common.util.concurrent.Atomics;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.inject.Provider;
+
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.Environment;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Command that executes some other command. */
+public class AliasCommand extends BaseCommand {
+ private final DispatchCommandProvider root;
+ private final Provider<CurrentUser> currentUser;
+ private final CommandName command;
+ private final AtomicReference<Command> atomicCmd;
+
+ AliasCommand(@CommandName(Commands.ROOT) DispatchCommandProvider root,
+ Provider<CurrentUser> currentUser, CommandName command) {
+ this.root = root;
+ this.currentUser = currentUser;
+ this.command = command;
+ this.atomicCmd = Atomics.newReference();
+ }
+
+ @Override
+ public void start(Environment env) throws IOException {
+ try {
+ begin(env);
+ } catch (UnloggedFailure e) {
+ String msg = e.getMessage();
+ if (!msg.endsWith("\n")) {
+ msg += "\n";
+ }
+ err.write(msg.getBytes(ENC));
+ err.flush();
+ onExit(e.exitCode);
+ }
+ }
+
+ private void begin(Environment env) throws UnloggedFailure, IOException {
+ Map<String, Provider<Command>> map = root.getMap();
+ for (String name : chain(command)) {
+ Provider<? extends Command> p = map.get(name);
+ if (p == null) {
+ throw new UnloggedFailure(1, getName() + ": not found");
+ }
+
+ Command cmd = p.get();
+ if (!(cmd instanceof DispatchCommand)) {
+ throw new UnloggedFailure(1, getName() + ": not found");
+ }
+ map = ((DispatchCommand) cmd).getMap();
+ }
+
+ Provider<? extends Command> p = map.get(command.value());
+ if (p == null) {
+ throw new UnloggedFailure(1, getName() + ": not found");
+ }
+
+ Command cmd = p.get();
+ checkRequiresCapability(cmd);
+ if (cmd instanceof BaseCommand) {
+ BaseCommand bc = (BaseCommand)cmd;
+ bc.setName(getName());
+ bc.setArguments(getArguments());
+ }
+ provideStateTo(cmd);
+ atomicCmd.set(cmd);
+ cmd.start(env);
+ }
+
+ @Override
+ public void destroy() {
+ Command cmd = atomicCmd.getAndSet(null);
+ if (cmd != null) {
+ cmd.destroy();
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+
+ private static LinkedList<String> chain(CommandName command) {
+ LinkedList<String> chain = Lists.newLinkedList();
+ while (command != null) {
+ chain.addFirst(command.value());
+ command = Commands.parentOf(command);
+ }
+ chain.removeLast();
+ return chain;
+ }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java
new file mode 100644
index 0000000..ee28e03
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.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;
+
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.sshd.server.Command;
+
+/** Resolves an alias to another command. */
+public class AliasCommandProvider implements Provider<Command> {
+ private final CommandName command;
+
+ @Inject
+ @CommandName(Commands.ROOT)
+ private DispatchCommandProvider root;
+
+ @Inject
+ private Provider<CurrentUser> currentUser;
+
+ public AliasCommandProvider(CommandName command) {
+ this.command = command;
+ }
+
+ @Override
+ public Command get() {
+ return new AliasCommand(root, currentUser, command);
+ }
+}
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..2b4543b 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);
@@ -71,6 +73,9 @@
private ExitCallback exit;
@Inject
+ private SshScope sshScope;
+
+ @Inject
private CmdLineParser.Factory cmdLineParserFactory;
@Inject
@@ -87,7 +92,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 +100,10 @@
/** Unparsed command line options. */
private String[] argv;
+ public BaseCommand() {
+ task = Atomics.newReference();
+ }
+
public void setInputStream(final InputStream in) {
this.in = in;
}
@@ -111,18 +120,27 @@
this.exit = callback;
}
+ String getName() {
+ return commandName;
+ }
+
void setName(final String prefix) {
this.commandName = prefix;
}
+ String[] getArguments() {
+ return argv;
+ }
+
public void setArguments(final String[] argv) {
this.argv = argv;
}
@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 +261,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;
}
@@ -277,7 +291,9 @@
*/
protected void onExit(final int rc) {
exit.onExit(rc);
- cleanup.run();
+ if (cleanup != null) {
+ cleanup.run();
+ }
}
/** Wrap the supplied output stream in a UTF-8 encoded PrintWriter. */
@@ -376,55 +392,59 @@
@Override
public void cancel() {
- final Context old = SshScope.set(context);
- try {
- onExit(STATUS_CANCEL);
- } finally {
- SshScope.set(old);
+ synchronized (this) {
+ final Context old = sshScope.set(context);
+ try {
+ onExit(STATUS_CANCEL);
+ } finally {
+ sshScope.set(old);
+ }
}
}
@Override
public void run() {
- final Thread thisThread = Thread.currentThread();
- final String thisName = thisThread.getName();
- int rc = 0;
- final Context old = SshScope.set(context);
- try {
- context.started = System.currentTimeMillis();
- thisThread.setName("SSH " + taskName);
-
- if (thunk instanceof ProjectCommandRunnable) {
- ((ProjectCommandRunnable) thunk).executeParseCommand();
- projectName = ((ProjectCommandRunnable) thunk).getProjectName();
- }
-
+ synchronized (this) {
+ final Thread thisThread = Thread.currentThread();
+ final String thisName = thisThread.getName();
+ int rc = 0;
+ final Context old = sshScope.set(context);
try {
- thunk.run();
- } catch (NoSuchProjectException e) {
- throw new UnloggedFailure(1, e.getMessage() + " no such project");
- } catch (NoSuchChangeException e) {
- throw new UnloggedFailure(1, e.getMessage() + " no such change");
- }
+ context.started = System.currentTimeMillis();
+ thisThread.setName("SSH " + taskName);
- out.flush();
- err.flush();
- } catch (Throwable e) {
- try {
+ if (thunk instanceof ProjectCommandRunnable) {
+ ((ProjectCommandRunnable) thunk).executeParseCommand();
+ projectName = ((ProjectCommandRunnable) thunk).getProjectName();
+ }
+
+ try {
+ thunk.run();
+ } catch (NoSuchProjectException e) {
+ throw new UnloggedFailure(1, e.getMessage() + " no such project");
+ } catch (NoSuchChangeException e) {
+ throw new UnloggedFailure(1, e.getMessage() + " no such change");
+ }
+
out.flush();
- } catch (Throwable e2) {
- }
- try {
err.flush();
- } catch (Throwable e2) {
- }
- rc = handleError(e);
- } finally {
- try {
- onExit(rc);
+ } catch (Throwable e) {
+ try {
+ out.flush();
+ } catch (Throwable e2) {
+ }
+ try {
+ err.flush();
+ } catch (Throwable e2) {
+ }
+ rc = handleError(e);
} finally {
- SshScope.set(old);
- thisThread.setName(thisName);
+ try {
+ onExit(rc);
+ } finally {
+ sshScope.set(old);
+ thisThread.setName(thisName);
+ }
}
}
}
@@ -505,6 +525,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 66e6add..be6659d 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
@@ -53,6 +53,7 @@
private final DispatchCommandProvider dispatcher;
private final SshLog log;
+ private final SshScope sshScope;
private final ScheduledExecutorService startExecutor;
private final Executor destroyExecutor;
@@ -60,9 +61,10 @@
CommandFactoryProvider(
@CommandName(Commands.ROOT) final DispatchCommandProvider d,
@GerritServerConfig final Config cfg, final WorkQueue workQueue,
- final SshLog l) {
+ final SshLog l, final SshScope s) {
dispatcher = d;
log = l;
+ sshScope = s;
int threads = cfg.getInt("sshd","commandStartThreads", 2);
startExecutor = workQueue.createQueue(threads, "SshCommandStart");
@@ -120,7 +122,7 @@
public void setSession(final ServerSession session) {
final SshSession s = session.getAttribute(SshSession.KEY);
- this.ctx = new Context(s, commandLine);
+ this.ctx = sshScope.newContext(s, commandLine);
}
public void start(final Environment env) throws IOException {
@@ -145,7 +147,7 @@
private void onStart() throws IOException {
synchronized (this) {
- final Context old = SshScope.set(ctx);
+ final Context old = sshScope.set(ctx);
try {
cmd = dispatcher.get();
cmd.setArguments(argv);
@@ -167,7 +169,7 @@
});
cmd.start(env);
} finally {
- SshScope.set(old);
+ sshScope.set(old);
}
}
}
@@ -211,14 +213,14 @@
private void onDestroy() {
synchronized (this) {
if (cmd != null) {
- final Context old = SshScope.set(ctx);
+ final Context old = sshScope.set(ctx);
try {
cmd.destroy();
log(BaseCommand.STATUS_CANCEL);
} finally {
ctx = null;
cmd = null;
- SshScope.set(old);
+ sshScope.set(old);
}
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
index 12cff9c..5340d6f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
@@ -97,6 +97,13 @@
return false;
}
+ static CommandName parentOf(CommandName name) {
+ if (name instanceof NestedCommandNameImpl) {
+ return ((NestedCommandNameImpl) name).parent;
+ }
+ return null;
+ }
+
private static final class NestedCommandNameImpl implements CommandName {
private final CommandName parent;
private final String name;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index a96a661..a69a2f1 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -65,6 +65,7 @@
private final IdentifiedUser.GenericFactory userFactory;
private final PeerDaemonUser.Factory peerFactory;
private final Config config;
+ private final SshScope sshScope;
private final Set<PublicKey> myHostKeys;
private volatile PeerKeyCache peerKeyCache;
@@ -72,12 +73,13 @@
DatabasePubKeyAuth(final SshKeyCacheImpl skc, final SshLog l,
final IdentifiedUser.GenericFactory uf, final PeerDaemonUser.Factory pf,
final SitePaths site, final KeyPairProvider hostKeyProvider,
- final @GerritServerConfig Config cfg) {
+ final @GerritServerConfig Config cfg, final SshScope s) {
sshKeyCache = skc;
sshLog = l;
userFactory = uf;
peerFactory = pf;
config = cfg;
+ sshScope = s;
myHostKeys = myHostKeys(hostKeyProvider);
peerKeyCache = new PeerKeyCache(site.peer_keys);
}
@@ -171,24 +173,24 @@
// session, record a login event in the log and add
// a close listener to record a logout event.
//
- Context ctx = new Context(sd, null);
- Context old = SshScope.set(ctx);
+ Context ctx = sshScope.newContext(sd, null);
+ Context old = sshScope.set(ctx);
try {
sshLog.onLogin();
} finally {
- SshScope.set(old);
+ sshScope.set(old);
}
session.getIoSession().getCloseFuture().addListener(
new IoFutureListener<IoFuture>() {
@Override
public void operationComplete(IoFuture future) {
- final Context ctx = new Context(sd, null);
- final Context old = SshScope.set(ctx);
+ final Context ctx = sshScope.newContext(sd, null);
+ final Context old = sshScope.set(ctx);
try {
sshLog.onLogout();
} finally {
- SshScope.set(old);
+ sshScope.set(old);
}
}
});
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..f0810ba 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,10 @@
package com.google.gerrit.sshd;
+import com.google.common.collect.Sets;
+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,19 +31,19 @@
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.
*/
final class DispatchCommand extends BaseCommand {
interface Factory {
- DispatchCommand create(String prefix, Map<String, Provider<Command>> map);
+ DispatchCommand create(Map<String, Provider<Command>> map);
}
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;
@@ -49,11 +52,15 @@
private List<String> args = new ArrayList<String>();
@Inject
- DispatchCommand(final Provider<CurrentUser> cu, @Assisted final String pfx,
+ DispatchCommand(final Provider<CurrentUser> cu,
@Assisted final Map<String, Provider<Command>> all) {
currentUser = cu;
- prefix = pfx;
commands = all;
+ atomicCmd = Atomics.newReference();
+ }
+
+ Map<String, Provider<Command>> getMap() {
+ return commands;
}
@Override
@@ -64,25 +71,19 @@
final Provider<Command> p = commands.get(commandName);
if (p == null) {
String msg =
- (prefix.isEmpty() ? "Gerrit Code Review" : prefix) + ": "
+ (getName().isEmpty() ? "Gerrit Code Review" : getName()) + ": "
+ commandName + ": not found";
throw new UnloggedFailure(1, msg);
}
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())
+ if (getName().isEmpty())
bc.setName(commandName);
else
- bc.setName(prefix + " " + commandName);
+ bc.setName(getName() + " " + commandName);
bc.setArguments(args.toArray(new String[args.size()]));
} else if (!args.isEmpty()) {
@@ -90,10 +91,7 @@
}
provideStateTo(cmd);
-
- synchronized (this) {
- this.cmd = cmd;
- }
+ atomicCmd.set(cmd);
cmd.start(env);
} catch (UnloggedFailure e) {
@@ -107,17 +105,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;
- }
}
}
@@ -125,22 +131,22 @@
protected String usage() {
final StringBuilder usage = new StringBuilder();
usage.append("Available commands");
- if (!prefix.isEmpty()) {
+ if (!getName().isEmpty()) {
usage.append(" of ");
- usage.append(prefix);
+ usage.append(getName());
}
usage.append(" are:\n");
usage.append("\n");
- for (Map.Entry<String, Provider<Command>> e : commands.entrySet()) {
+ for (String name : Sets.newTreeSet(commands.keySet())) {
usage.append(" ");
- usage.append(e.getKey());
+ usage.append(name);
usage.append("\n");
}
usage.append("\n");
usage.append("See '");
- if (prefix.indexOf(' ') < 0) {
- usage.append(prefix);
+ if (getName().indexOf(' ') < 0) {
+ usage.append(getName());
usage.append(' ');
}
usage.append("COMMAND --help' for more information.\n");
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..b76ff71 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}.
@@ -39,27 +38,45 @@
@Inject
private DispatchCommand.Factory factory;
- 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);
- }
-
- public DispatchCommandProvider(final String dispatcherName,
- final CommandName cn) {
- this.dispatcherName = dispatcherName;
this.parent = cn;
}
@Override
public DispatchCommand get() {
- return factory.create(dispatcherName, getMap());
+ return factory.create(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);
+ }
+ };
+ }
+
+ ConcurrentMap<String, Provider<Command>> getMap() {
if (map == null) {
synchronized (this) {
if (map == null) {
@@ -71,10 +88,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 +99,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/NoShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
index be513b3..d2fdf78 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
@@ -58,6 +58,7 @@
static class SendMessage implements Command, SessionAware {
private final Provider<MessageFactory> messageFactory;
+ private final SshScope sshScope;
private InputStream in;
private OutputStream out;
@@ -66,8 +67,9 @@
private Context context;
@Inject
- SendMessage(Provider<MessageFactory> messageFactory) {
+ SendMessage(Provider<MessageFactory> messageFactory, SshScope sshScope) {
this.messageFactory = messageFactory;
+ this.sshScope = sshScope;
}
public void setInputStream(final InputStream in) {
@@ -87,16 +89,16 @@
}
public void setSession(final ServerSession session) {
- this.context = new Context(session.getAttribute(SshSession.KEY), "");
+ this.context = sshScope.newContext(session.getAttribute(SshSession.KEY), "");
}
public void start(final Environment env) throws IOException {
- Context old = SshScope.set(context);
+ Context old = sshScope.set(context);
String message;
try {
message = messageFactory.get().getMessage();
} finally {
- SshScope.set(old);
+ sshScope.set(old);
}
err.write(Constants.encode(message.toString()));
err.flush();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
new file mode 100644
index 0000000..4dbb8d7
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/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;
+
+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/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..03485f7
--- /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 !commands.isEmpty() ? this : null;
+ }
+}
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/SshCurrentUserProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java
deleted file mode 100644
index 5839cf1..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2010 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.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-@Singleton
-class SshCurrentUserProvider implements Provider<CurrentUser> {
- private final Provider<SshSession> session;
- private final Provider<IdentifiedUser> identifiedProvider;
-
- @Inject
- SshCurrentUserProvider(Provider<SshSession> s, Provider<IdentifiedUser> p) {
- session = s;
- identifiedProvider = p;
- }
-
- @Override
- public CurrentUser get() {
- final CurrentUser user = session.get().getCurrentUser();
- if (user instanceof IdentifiedUser) {
- return identifiedProvider.get();
- }
- return session.get().getCurrentUser();
- }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index d382a57..664ce45 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -18,7 +18,7 @@
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.gerrit.common.Version;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.ssh.SshInfo;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java
deleted file mode 100644
index 516b59c..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2010 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.common.errors.NotSignedInException;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-import com.google.inject.Singleton;
-
-@Singleton
-class SshIdentifiedUserProvider implements Provider<IdentifiedUser> {
- private final Provider<SshSession> session;
- private final IdentifiedUser.RequestFactory factory;
-
- @Inject
- SshIdentifiedUserProvider(Provider<SshSession> s,
- IdentifiedUser.RequestFactory f) {
- session = s;
- factory = f;
- }
-
- @Override
- public IdentifiedUser get() {
- final CurrentUser user = session.get().getCurrentUser();
- if (user instanceof IdentifiedUser) {
- return factory.create(user.getAccessPath(), //
- ((IdentifiedUser) user).getAccountId());
- }
- throw new ProvisionException(NotSignedInException.MESSAGE,
- new NotSignedInException());
- }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 1f5ac28..0a1f708 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -16,13 +16,13 @@
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
import com.google.gerrit.common.errors.InvalidSshKeyException;
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.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
@@ -42,6 +42,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.ExecutionException;
/** Provides the {@link SshKeyCacheEntry}. */
@Singleton
@@ -57,9 +58,10 @@
return new CacheModule() {
@Override
protected void configure() {
- final TypeLiteral<Cache<String, Iterable<SshKeyCacheEntry>>> type =
- new TypeLiteral<Cache<String, Iterable<SshKeyCacheEntry>>>() {};
- core(type, CACHE_NAME).populateWith(Loader.class);
+ cache(CACHE_NAME,
+ String.class,
+ new TypeLiteral<Iterable<SshKeyCacheEntry>>(){})
+ .loader(Loader.class);
bind(SshKeyCacheImpl.class);
bind(SshKeyCache.class).to(SshKeyCacheImpl.class);
}
@@ -71,20 +73,27 @@
.asList(new SshKeyCacheEntry[0]));
}
- private final Cache<String, Iterable<SshKeyCacheEntry>> cache;
+ private final LoadingCache<String, Iterable<SshKeyCacheEntry>> cache;
@Inject
SshKeyCacheImpl(
- @Named(CACHE_NAME) final Cache<String, Iterable<SshKeyCacheEntry>> cache) {
+ @Named(CACHE_NAME) LoadingCache<String, Iterable<SshKeyCacheEntry>> cache) {
this.cache = cache;
}
- public Iterable<SshKeyCacheEntry> get(String username) {
- return cache.get(username);
+ Iterable<SshKeyCacheEntry> get(String username) {
+ try {
+ return cache.get(username);
+ } catch (ExecutionException e) {
+ log.warn("Cannot load SSH keys for " + username, e);
+ return Collections.emptyList();
+ }
}
public void evict(String username) {
- cache.remove(username);
+ if (username != null) {
+ cache.invalidate(username);
+ }
}
@Override
@@ -107,7 +116,7 @@
}
}
- static class Loader extends EntryCreator<String, Iterable<SshKeyCacheEntry>> {
+ static class Loader extends CacheLoader<String, Iterable<SshKeyCacheEntry>> {
private final SchemaFactory<ReviewDb> schema;
@Inject
@@ -116,8 +125,7 @@
}
@Override
- public Iterable<SshKeyCacheEntry> createEntry(String username)
- throws Exception {
+ public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
final ReviewDb db = schema.open();
try {
final AccountExternalId.Key key =
@@ -143,11 +151,6 @@
}
}
- @Override
- public Iterable<SshKeyCacheEntry> missing(String username) {
- return Collections.emptyList();
- }
-
private void add(ReviewDb db, List<SshKeyCacheEntry> kl, AccountSshKey k) {
try {
kl.add(new SshKeyCacheEntry(k.getKey(), SshUtil.parse(k)));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
index 32d5a07..8fbea9d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -14,7 +14,7 @@
package com.google.gerrit.sshd;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PeerDaemonUser;
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 54b0bb5..33e4bae 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
@@ -15,27 +15,34 @@
package com.google.gerrit.sshd;
import static com.google.inject.Scopes.SINGLETON;
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
+import com.google.common.collect.Maps;
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;
import com.google.gerrit.server.PeerDaemonUser;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.ChangeUserName;
import com.google.gerrit.server.config.FactoryModule;
import com.google.gerrit.server.config.GerritRequestModule;
+import com.google.gerrit.server.config.GerritServerConfig;
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;
import com.google.gerrit.sshd.args4j.SocketAddressHandler;
@@ -43,24 +50,41 @@
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.Inject;
+import com.google.inject.internal.UniqueAnnotations;
import com.google.inject.servlet.RequestScoped;
import org.apache.sshd.common.KeyPairProvider;
import org.apache.sshd.server.CommandFactory;
import org.apache.sshd.server.PublickeyAuthenticator;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
import org.kohsuke.args4j.spi.OptionHandler;
import java.net.SocketAddress;
+import java.util.Map;
/** Configures standard dependencies for {@link SshDaemon}. */
public class SshModule extends FactoryModule {
+ private final Map<String, String> aliases;
+
+ @Inject
+ SshModule(@GerritServerConfig Config cfg) {
+ aliases = Maps.newHashMap();
+ for (String name : cfg.getNames("ssh-alias")) {
+ aliases.put(name, cfg.getString("ssh-alias", null, name));
+ }
+ }
+
@Override
protected void configure() {
bindScope(RequestScoped.class, SshScope.REQUEST);
bind(RequestScopePropagator.class).to(SshScope.Propagator.class);
+ bind(SshScope.class).in(SINGLETON);
configureRequestScope();
configureCmdLineParser();
+ configureAliases();
install(SshKeyCacheImpl.module());
bind(SshLog.class);
@@ -70,7 +94,7 @@
factory(PeerDaemonUser.Factory.class);
bind(DispatchCommandProvider.class).annotatedWith(Commands.CMD_ROOT)
- .toInstance(new DispatchCommandProvider("", Commands.CMD_ROOT));
+ .toInstance(new DispatchCommandProvider(Commands.CMD_ROOT));
bind(CommandFactoryProvider.class);
bind(CommandFactory.class).toProvider(CommandFactoryProvider.class);
bind(WorkQueue.Executor.class).annotatedWith(StreamCommandExecutor.class)
@@ -87,12 +111,37 @@
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().toInstance(registerInParentInjectors());
listener().to(SshLog.class);
listener().to(SshDaemon.class);
}
});
}
+ private void configureAliases() {
+ CommandName gerrit = Commands.named("gerrit");
+ for (Map.Entry<String, String> e : aliases.entrySet()) {
+ String name = e.getKey();
+ String[] dest = e.getValue().split("[ \\t]+");
+ CommandName cmd = Commands.named(dest[0]);
+ for (int i = 1; i < dest.length; i++) {
+ cmd = Commands.named(cmd, dest[i]);
+ }
+ bind(Commands.key(gerrit, name))
+ .toProvider(new AliasCommandProvider(cmd));
+ }
+ }
+
private void configureRequestScope() {
bind(SshScope.Context.class).toProvider(SshScope.ContextProvider.class);
@@ -101,11 +150,6 @@
bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
SshRemotePeerProvider.class).in(SshScope.REQUEST);
- bind(CurrentUser.class).toProvider(SshCurrentUserProvider.class).in(
- SshScope.REQUEST);
- bind(IdentifiedUser.class).toProvider(SshIdentifiedUserProvider.class).in(
- SshScope.REQUEST);
-
bind(WorkQueue.Executor.class).annotatedWith(CommandExecutor.class)
.toProvider(CommandExecutorProvider.class).in(SshScope.REQUEST);
@@ -118,6 +162,8 @@
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);
registerOptionHandler(SocketAddress.class, SocketAddressHandler.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/SshScope.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
index 92609b5..d6f66ca 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
@@ -14,8 +14,13 @@
package com.google.gerrit.sshd;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
+import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
@@ -26,10 +31,10 @@
/** Guice scopes for state during an SSH connection. */
class SshScope {
- static class Context {
- private static final Key<RequestCleanup> RC_KEY =
- Key.get(RequestCleanup.class);
+ private static final Key<RequestCleanup> RC_KEY =
+ Key.get(RequestCleanup.class);
+ class Context implements RequestContext {
private final RequestCleanup cleanup;
private final SshSession session;
private final String commandLine;
@@ -56,10 +61,6 @@
finished = p.finished;
}
- Context(final SshSession s, final String c) {
- this(s, c, System.currentTimeMillis());
- }
-
String getCommandLine() {
return commandLine;
}
@@ -68,6 +69,16 @@
return session;
}
+ @Override
+ public CurrentUser getCurrentUser() {
+ final CurrentUser user = session.getCurrentUser();
+ if (user instanceof IdentifiedUser) {
+ return userFactory.create(user.getAccessPath(), //
+ ((IdentifiedUser) user).getAccountId());
+ }
+ return user;
+ }
+
synchronized <T> T get(Key<T> key, Provider<T> creator) {
@SuppressWarnings("unchecked")
T t = (T) map.get(key);
@@ -100,15 +111,19 @@
}
static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
- Propagator() {
- super(REQUEST, current);
+ private final SshScope sshScope;
+
+ @Inject
+ Propagator(SshScope sshScope, ThreadLocalRequestContext local) {
+ super(REQUEST, current, local);
+ this.sshScope = sshScope;
}
@Override
protected Context continuingContext(Context ctx) {
// The cleanup is not chained, since the RequestScopePropagator executors
// the Context's cleanup when finished executing.
- return new Context(ctx, ctx.getSession(), ctx.getCommandLine());
+ return sshScope.newContinuingContext(ctx);
}
}
@@ -123,9 +138,28 @@
return ctx;
}
- static Context set(Context ctx) {
+ private final ThreadLocalRequestContext local;
+ private final IdentifiedUser.RequestFactory userFactory;
+
+ @Inject
+ SshScope(ThreadLocalRequestContext local,
+ IdentifiedUser.RequestFactory userFactory) {
+ this.local = local;
+ this.userFactory = userFactory;
+ }
+
+ Context newContext(SshSession session, String commandLine) {
+ return new Context(session, commandLine, System.currentTimeMillis());
+ }
+
+ private Context newContinuingContext(Context ctx) {
+ return new Context(ctx, ctx.getSession(), ctx.getCommandLine());
+ }
+
+ Context set(Context ctx) {
Context old = current.get();
current.set(ctx);
+ local.setContext(ctx);
return old;
}
@@ -149,7 +183,4 @@
return "SshScopes.REQUEST";
}
};
-
- private SshScope() {
- }
}
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..33459c2 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.
@@ -41,6 +43,7 @@
* key, or a key on this daemon's peer host key ring.
*/
public final class SuExec extends BaseCommand {
+ private final SshScope sshScope;
private final DispatchCommandProvider dispatcher;
private Provider<CurrentUser> caller;
@@ -57,18 +60,21 @@
@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,
+ SuExec(final SshScope sshScope,
+ @CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
final Provider<CurrentUser> caller, final Provider<SshSession> session,
final IdentifiedUser.GenericFactory userFactory,
final SshScope.Context callingContext) {
+ this.sshScope = sshScope;
this.dispatcher = dispatcher;
this.caller = caller;
this.session = session;
this.userFactory = userFactory;
this.callingContext = callingContext;
+ atomicCmd = Atomics.newReference();
}
@Override
@@ -78,18 +84,15 @@
parseCommandLine();
final Context ctx = callingContext.subContext(newSession(), join(args));
- final Context old = SshScope.set(ctx);
+ final Context old = sshScope.set(ctx);
try {
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);
+ sshScope.set(old);
}
} else {
@@ -136,11 +139,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/AccountGroupUUIDHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java
index 49bf695..8e7c3ff 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java
@@ -14,8 +14,10 @@
package com.google.gerrit.sshd.args4j;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -27,25 +29,25 @@
import org.kohsuke.args4j.spi.Setter;
public class AccountGroupUUIDHandler extends OptionHandler<AccountGroup.UUID> {
- private final GroupCache groupCache;
+ private final GroupBackend groupBackend;
@Inject
- public AccountGroupUUIDHandler(final GroupCache groupCache,
+ public AccountGroupUUIDHandler(final GroupBackend groupBackend,
@Assisted final CmdLineParser parser, @Assisted final OptionDef option,
@Assisted final Setter<AccountGroup.UUID> setter) {
super(parser, option, setter);
- this.groupCache = groupCache;
+ this.groupBackend = groupBackend;
}
@Override
public final int parseArguments(final Parameters params)
throws CmdLineException {
final String n = params.getParameter(0);
- final AccountGroup group = groupCache.get(new AccountGroup.NameKey(n));
+ final GroupReference group = GroupBackends.findBestSuggestion(groupBackend, n);
if (group == null) {
throw new CmdLineException(owner, "Group \"" + n + "\" does not exist");
}
- setter.addValue(group.getGroupUUID());
+ setter.addValue(group.getUUID());
return 1;
}
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/args4j/ObjectIdHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ObjectIdHandler.java
new file mode 100644
index 0000000..adb5ad6
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ObjectIdHandler.java
@@ -0,0 +1,47 @@
+// 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.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.ObjectId;
+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 ObjectIdHandler extends OptionHandler<ObjectId> {
+
+ @Inject
+ public ObjectIdHandler(@Assisted final CmdLineParser parser,
+ @Assisted final OptionDef option, @Assisted final Setter<ObjectId> setter) {
+ super(parser, option, setter);
+ }
+
+ @Override
+ public int parseArguments(Parameters params) throws CmdLineException {
+ final String n = params.getParameter(0);
+ setter.addValue(ObjectId.fromString(n));
+ return 1;
+ }
+
+ @Override
+ public String getDefaultMetaVariable() {
+ return "COMMIT";
+ }
+}
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 950bf341..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
new file mode 100644
index 0000000..4350d1e
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -0,0 +1,98 @@
+// 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.PermissionDeniedException;
+import com.google.gerrit.server.git.BanCommit;
+import com.google.gerrit.server.git.BanCommitResult;
+import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BanCommitCommand extends SshCommand {
+ @Option(name = "--reason", aliases = {"-r"}, metaVar = "REASON", usage = "reason for banning the commit")
+ private String reason;
+
+ @Argument(index = 0, required = true, metaVar = "PROJECT",
+ usage = "name of the project for which the commit should be banned")
+ private ProjectControl projectControl;
+
+ @Argument(index = 1, required = true, multiValued = true, metaVar = "COMMIT",
+ usage = "commit(s) that should be banned")
+ private List<ObjectId> commitsToBan = new ArrayList<ObjectId>();
+
+ @Inject
+ private BanCommit.Factory banCommitFactory;
+
+ @Override
+ protected void run() throws Failure {
+ try {
+ final BanCommitResult result =
+ banCommitFactory.create().ban(projectControl, commitsToBan, reason);
+
+ 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> 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);
+ } catch (IOException e) {
+ throw die(e);
+ } catch (MergeException e) {
+ throw die(e);
+ } catch (InterruptedException e) {
+ throw die(e);
+ }
+ }
+
+ private static void printCommits(final PrintWriter stdout,
+ final List<ObjectId> commits) {
+ boolean first = true;
+ for (final ObjectId c : commits) {
+ if (!first) {
+ stdout.print(",\n");
+ }
+ stdout.print(c.getName());
+ first = false;
+ }
+ stdout.print("\n\n");
+ }
+}
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..500c84a 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
@@ -14,37 +14,33 @@
package com.google.gerrit.sshd.commands;
-import com.google.gerrit.ehcache.EhcachePoolImpl;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.common.cache.Cache;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
-import net.sf.ehcache.CacheManager;
-import net.sf.ehcache.Ehcache;
-
-import java.util.Arrays;
import java.util.SortedSet;
-import java.util.TreeSet;
-abstract class CacheCommand extends BaseCommand {
+abstract class CacheCommand extends SshCommand {
@Inject
- protected EhcachePoolImpl cachePool;
+ protected DynamicMap<Cache<?, ?>> cacheMap;
protected SortedSet<String> cacheNames() {
- final SortedSet<String> names = new TreeSet<String>();
- for (final Ehcache c : getAllCaches()) {
- names.add(c.getName());
+ SortedSet<String> names = Sets.newTreeSet();
+ for (String plugin : cacheMap.plugins()) {
+ for (String name : cacheMap.byPlugin(plugin).keySet()) {
+ names.add(cacheNameOf(plugin, name));
+ }
}
return names;
}
- protected Ehcache[] getAllCaches() {
- final CacheManager cacheMgr = cachePool.getCacheManager();
- final String[] cacheNames = cacheMgr.getCacheNames();
- Arrays.sort(cacheNames);
- final Ehcache[] r = new Ehcache[cacheNames.length];
- for (int i = 0; i < cacheNames.length; i++) {
- r[i] = cacheMgr.getEhcache(cacheNames[i]);
+ protected String cacheNameOf(String plugin, String name) {
+ if ("gerrit".equals(plugin)) {
+ return name;
+ } else {
+ return plugin + "." + name;
}
- return r;
}
}
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..8e307b9 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) {
@@ -79,7 +79,7 @@
@Option(name = "--branch", aliases = {"-b"}, metaVar = "BRANCH", usage = "initial branch name\n"
+ "(default: master)")
- private String branch = Constants.MASTER;
+ private List<String> branch;
@Option(name = "--empty-commit", usage = "to create initial empty commit")
private boolean createEmptyCommit;
@@ -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 16461b6..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
@@ -35,6 +36,7 @@
// SlaveCommandModule.
command(gerrit).toProvider(new DispatchCommandProvider(gerrit));
+ command(gerrit, "ban-commit").to(BanCommitCommand.class);
command(gerrit, "flush-caches").to(FlushCaches.class);
command(gerrit, "ls-projects").to(ListProjectsCommand.class);
command(gerrit, "ls-groups").to(ListGroupsCommand.class);
@@ -45,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..b01c83b 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,23 @@
package com.google.gerrit.sshd.commands;
+import com.google.common.cache.Cache;
+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 com.google.inject.Provider;
-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.Map;
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 +46,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 +56,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,26 +88,29 @@
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() {
try {
- for (final Ehcache c : getAllCaches()) {
- final String name = c.getName();
- if (flush(name)) {
- try {
- c.removeAll();
- } catch (Throwable e) {
- p.println("error: cannot flush cache \"" + name + "\": " + e);
+ for (String plugin : cacheMap.plugins()) {
+ for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
+ cacheMap.byPlugin(plugin).entrySet()) {
+ String n = cacheNameOf(plugin, entry.getKey());
+ if (flush(n)) {
+ try {
+ entry.getValue().get().invalidateAll();
+ } catch (Throwable err) {
+ stderr.println("error: cannot flush cache \"" + n + "\": " + err);
+ }
}
}
}
} 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/ListProjectsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index d0bbc72..13e3f17 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -30,11 +30,13 @@
@Override
public void run() throws Exception {
parseCommandLine(impl);
- if (impl.isShowTree() && (impl.getShowBranch() != null)) {
- throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
- }
- if (impl.isShowTree() && impl.isShowDescription()) {
- throw new UnloggedFailure(1, "fatal: --tree and --description options are not compatible.");
+ if (!impl.getFormat().isJson()) {
+ if (impl.isShowTree() && (impl.getShowBranch() != null)) {
+ throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
+ }
+ if (impl.isShowTree() && impl.isShowDescription()) {
+ throw new UnloggedFailure(1, "fatal: --tree and --description options are not compatible.");
+ }
}
impl.display(out);
}
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..c5cdbd5 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
@@ -33,8 +33,8 @@
command(gerrit, "gsql").to(AdminQueryShell.class);
command(gerrit, "set-reviewers").to(SetReviewersCommand.class);
command(gerrit, "receive-pack").to(Receive.class);
- 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/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..644cf13
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.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.sshd.commands;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+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 = Lists.newArrayList(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..d60465c
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.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.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+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 java.util.List;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginReloadCommand extends SshCommand {
+ @Argument(index = 0, metaVar = "NAME", usage = "plugins to reload/restart")
+ private List<String> names;
+
+ @Inject
+ private PluginLoader loader;
+
+ @Override
+ protected void run() throws UnloggedFailure {
+ if (names == null || names.isEmpty()) {
+ loader.rescan();
+ } else {
+ try {
+ loader.reload(names);
+ } catch (InvalidPluginException e) {
+ throw die(e.getMessage());
+ } catch (PluginInstallException e) {
+ throw die(e.getMessage());
+ }
+ }
+ }
+}
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
deleted file mode 100644
index bc4e0bb..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (C) 2009 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.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.inject.Inject;
-
-import org.apache.sshd.server.Environment;
-import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.Option;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-/** Force a project to replicate, again. */
-final class Replicate extends BaseCommand {
- @Option(name = "--all", usage = "push all known projects")
- private boolean all;
-
- @Option(name = "--url", metaVar = "PATTERN", usage = "pattern to match URL on")
- private String urlMatch;
-
- @Argument(index = 0, multiValued = true, metaVar = "PROJECT", usage = "project name")
- private List<String> projectNames = new ArrayList<String>(2);
-
- @Inject
- IdentifiedUser currentUser;
-
- @Inject
- private PushAllProjectsOp.Factory pushAllOpFactory;
-
- @Inject
- private ReplicationQueue replication;
-
- @Inject
- 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 {
- if (all && projectNames.size() > 0) {
- throw new Failure(1, "error: cannot combine --all and PROJECT");
- }
-
- if (!replication.isEnabled()) {
- throw new Failure(1, "error: replication not enabled");
- }
-
- if (all) {
- pushAllOpFactory.create(urlMatch).start(0, TimeUnit.SECONDS);
-
- } else {
- for (final String name : projectNames) {
- final Project.NameKey key = new Project.NameKey(name);
- if (projectCache.get(key) != null) {
- replication.scheduleFullSync(key, urlMatch);
- } else {
- throw new Failure(1, "error: '" + name + "': not a Gerrit 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..e2ac772 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
@@ -14,19 +14,20 @@
package com.google.gerrit.sshd.commands;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheStats;
+import com.google.common.collect.Maps;
import com.google.gerrit.common.Version;
-import com.google.gerrit.lifecycle.LifecycleListener;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.cache.h2.H2CacheImpl;
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;
-
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Statistics;
-import net.sf.ehcache.config.CacheConfiguration;
+import com.google.inject.Provider;
import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.session.IoSession;
@@ -36,7 +37,6 @@
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;
@@ -45,8 +45,11 @@
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
+import java.util.Map;
+import java.util.SortedMap;
/** Show the current cache states. */
+@RequiresCapability(GlobalCapability.VIEW_CACHES)
final class ShowCaches extends CacheCommand {
private static volatile long serverStarted;
@@ -68,9 +71,6 @@
private boolean showJVM;
@Inject
- private IdentifiedUser currentUser;
-
- @Inject
private WorkQueue workQueue;
@Inject
@@ -80,98 +80,81 @@
@SitePath
private File sitePath;
- private PrintWriter p;
+ @Option(name = "--width", aliases = {"-w"}, metaVar = "COLS", usage = "width of output table")
+ private int columns = 80;
+ private int nw;
@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();
+ public void start(Environment env) throws IOException {
+ String s = env.getEnv().get(Environment.ENV_COLUMNS);
+ if (s != null && !s.isEmpty()) {
+ try {
+ columns = Integer.parseInt(s);
+ } catch (NumberFormatException err) {
+ columns = 80;
}
- });
+ }
+ super.start(env);
}
- private void display() {
- p = toPrintWriter(out);
-
+ @Override
+ protected void run() {
+ nw = columns - 50;
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(//
- "%1s %-18s %-4s|%-20s| %-5s |%-14s|\n" //
+ stdout.print(String.format(//
+ "%1s %-"+nw+"s|%-21s| %-5s |%-9s|\n" //
, "" //
, "Name" //
- , "Max" //
- , "Object Count" //
+ , "Entries" //
, "AvgGet" //
, "Hit Ratio" //
));
- p.print(String.format(//
- "%1s %-18s %-4s|%6s %6s %6s| %-5s |%-4s %-4s %-4s|\n" //
+ stdout.print(String.format(//
+ "%1s %-"+nw+"s|%6s %6s %7s| %-5s |%-4s %-4s|\n" //
, "" //
, "" //
- , "Age" //
- , "Disk" //
, "Mem" //
- , "Cnt" //
- , "" //
, "Disk" //
+ , "Space" //
+ , "" //
, "Mem" //
- , "Agg" //
+ , "Disk" //
));
- p.print("------------------"
- + "-------+--------------------+----------+--------------+\n");
- for (final Ehcache cache : getAllCaches()) {
- final CacheConfiguration cfg = cache.getCacheConfiguration();
- final boolean useDisk = cfg.isDiskPersistent() || cfg.isOverflowToDisk();
- final Statistics stat = cache.getStatistics();
- final long total = stat.getCacheHits() + stat.getCacheMisses();
-
- if (useDisk) {
- p.print(String.format(//
- "D %-18s %-4s|%6s %6s %6s| %7s |%4s %4s %4s|\n" //
- , cache.getName() //
- , interval(cfg.getTimeToLiveSeconds()) //
- , count(stat.getDiskStoreObjectCount()) //
- , count(stat.getMemoryStoreObjectCount()) //
- , count(stat.getObjectCount()) //
- , duration(stat.getAverageGetTime()) //
- , percent(stat.getOnDiskHits(), total) //
- , percent(stat.getInMemoryHits(), total) //
- , percent(stat.getCacheHits(), total) //
- ));
- } else {
- p.print(String.format(//
- " %-18s %-4s|%6s %6s %6s| %7s |%4s %4s %4s|\n" //
- , cache.getName() //
- , interval(cfg.getTimeToLiveSeconds()) //
- , "", "" //
- , count(stat.getObjectCount()) //
- , duration(stat.getAverageGetTime()) //
- , "", "" //
- , percent(stat.getCacheHits(), total) //
- ));
- }
+ stdout.print("--");
+ for (int i = 0; i < nw; i++) {
+ stdout.print('-');
}
- p.print('\n');
+ stdout.print("+---------------------+---------+---------+\n");
+
+ Map<String, H2CacheImpl<?, ?>> disks = Maps.newTreeMap();
+ printMemoryCaches(disks, sortedCoreCaches());
+ printMemoryCaches(disks, sortedPluginCaches());
+ for (Map.Entry<String, H2CacheImpl<?, ?>> entry : disks.entrySet()) {
+ H2CacheImpl<?, ?> cache = entry.getValue();
+ CacheStats stat = cache.stats();
+ H2CacheImpl.DiskStats disk = cache.diskStats();
+ stdout.print(String.format(
+ "D %-"+nw+"s|%6s %6s %7s| %7s |%4s %4s|\n",
+ entry.getKey(),
+ count(cache.size()),
+ count(disk.size()),
+ bytes(disk.space()),
+ duration(stat.averageLoadPenalty()),
+ percent(stat.hitCount(), stat.requestCount()),
+ percent(disk.hitCount(), disk.requestCount())));
+ }
+ stdout.print('\n');
if (gc) {
System.gc();
@@ -187,7 +170,52 @@
jvmSummary();
}
- p.flush();
+ stdout.flush();
+ }
+
+ private void printMemoryCaches(
+ Map<String, H2CacheImpl<?, ?>> disks,
+ Map<String, Cache<?,?>> caches) {
+ for (Map.Entry<String, Cache<?,?>> entry : caches.entrySet()) {
+ Cache<?,?> cache = entry.getValue();
+ if (cache instanceof H2CacheImpl) {
+ disks.put(entry.getKey(), (H2CacheImpl<?,?>)cache);
+ continue;
+ }
+ CacheStats stat = cache.stats();
+ stdout.print(String.format(
+ " %-"+nw+"s|%6s %6s %7s| %7s |%4s %4s|\n",
+ entry.getKey(),
+ count(cache.size()),
+ "",
+ "",
+ duration(stat.averageLoadPenalty()),
+ percent(stat.hitCount(), stat.requestCount()),
+ ""));
+ }
+ }
+
+ private Map<String, Cache<?, ?>> sortedCoreCaches() {
+ SortedMap<String, Cache<?, ?>> m = Maps.newTreeMap();
+ for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
+ cacheMap.byPlugin("gerrit").entrySet()) {
+ m.put(cacheNameOf("gerrit", entry.getKey()), entry.getValue().get());
+ }
+ return m;
+ }
+
+ private Map<String, Cache<?, ?>> sortedPluginCaches() {
+ SortedMap<String, Cache<?, ?>> m = Maps.newTreeMap();
+ for (String plugin : cacheMap.plugins()) {
+ if ("gerrit".equals(plugin)) {
+ continue;
+ }
+ for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
+ cacheMap.byPlugin(plugin).entrySet()) {
+ m.put(cacheNameOf(plugin, entry.getKey()), entry.getValue().get());
+ }
+ }
+ return m;
}
private void memSummary() {
@@ -200,17 +228,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 +252,7 @@
case SLEEPING: tasksSleeping++; break;
}
}
- p.format(
+ stdout.format(
"Tasks: %4d total = %4d running + %4d ready + %4d sleeping\n",
tasksTotal,
tasksRunning,
@@ -245,7 +273,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 +282,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) {
@@ -325,45 +353,24 @@
return String.format("%6d", cnt);
}
- private String duration(double ms) {
- if (Math.abs(ms) <= 0.05) {
+ private String duration(double ns) {
+ if (ns < 0.5) {
return "";
}
- String suffix = "ms";
- if (ms >= 1000) {
- ms /= 1000;
+ String suffix = "ns";
+ if (ns >= 1000.0) {
+ ns /= 1000.0;
+ suffix = "us";
+ }
+ if (ns >= 1000.0) {
+ ns /= 1000.0;
+ suffix = "ms";
+ }
+ if (ns >= 1000.0) {
+ ns /= 1000.0;
suffix = "s ";
}
- return String.format("%4.1f%s", ms, suffix);
- }
-
- private String interval(double ttl) {
- if (ttl == 0) {
- return "inf";
- }
-
- String suffix = "s";
- if (ttl >= 60) {
- ttl /= 60;
- suffix = "m";
-
- if (ttl >= 60) {
- ttl /= 60;
- suffix = "h";
- }
-
- if (ttl >= 24) {
- ttl /= 24;
- suffix = "d";
-
- if (ttl >= 365) {
- ttl /= 365;
- suffix = "y";
- }
- }
- }
-
- return Integer.toString((int) ttl) + suffix;
+ return String.format("%4.1f%s", ns, suffix);
}
private String percent(final long value, final long total) {
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-util-cli/.gitignore b/gerrit-util-cli/.gitignore
index 194bedc..35069e7 100644
--- a/gerrit-util-cli/.gitignore
+++ b/gerrit-util-cli/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-util-cli.iml
\ No newline at end of file
diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml
index 4ecbda4..4886d09 100644
--- a/gerrit-util-cli/pom.xml
+++ b/gerrit-util-cli/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-util-cli</artifactId>
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
index de2f7e9..d4174cb 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -46,6 +46,7 @@
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.OptionDef;
import org.kohsuke.args4j.spi.BooleanOptionHandler;
+import org.kohsuke.args4j.spi.EnumOptionHandler;
import org.kohsuke.args4j.spi.OptionHandler;
import org.kohsuke.args4j.spi.Setter;
@@ -66,7 +67,6 @@
* args4j style format prior to invoking args4j for parsing.
*/
public class CmdLineParser {
-
public interface Factory {
CmdLineParser create(Object bean);
}
@@ -118,6 +118,67 @@
out.write('\n');
}
+ public void printQueryStringUsage(String name, StringWriter out) {
+ out.write(name);
+
+ char next = '?';
+ List<NamedOptionDef> booleans = new ArrayList<NamedOptionDef>();
+ for (@SuppressWarnings("rawtypes") OptionHandler handler : parser.options) {
+ if (handler.option instanceof NamedOptionDef) {
+ NamedOptionDef n = (NamedOptionDef) handler.option;
+
+ if (handler instanceof BooleanOptionHandler) {
+ booleans.add(n);
+ continue;
+ }
+
+ if (!n.required()) {
+ out.write('[');
+ }
+ out.write(next);
+ next = '&';
+ if (n.name().startsWith("--")) {
+ out.write(n.name().substring(2));
+ } else if (n.name().startsWith("-")) {
+ out.write(n.name().substring(1));
+ } else {
+ out.write(n.name());
+ }
+ out.write('=');
+
+ String var = handler.getDefaultMetaVariable();
+ if (handler instanceof EnumOptionHandler) {
+ var = var.substring(1, var.length() - 1);
+ var = var.replaceAll(" ", "");
+ }
+ out.write(var);
+ if (!n.required()) {
+ out.write(']');
+ }
+ if (n.isMultiValued()) {
+ out.write('*');
+ }
+ }
+ }
+ for (NamedOptionDef n : booleans) {
+ if (!n.required()) {
+ out.write('[');
+ }
+ out.write(next);
+ next = '&';
+ if (n.name().startsWith("--")) {
+ out.write(n.name().substring(2));
+ } else if (n.name().startsWith("-")) {
+ out.write(n.name().substring(1));
+ } else {
+ out.write(n.name());
+ }
+ if (!n.required()) {
+ out.write(']');
+ }
+ }
+ }
+
public boolean wasHelpRequestedByOption() {
return parser.help.value;
}
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java
index 7af544c..1ea73cc 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java
@@ -38,7 +38,6 @@
return (Key<OptionHandler<T>>) Key.get(handlerType);
}
- @SuppressWarnings("unchecked")
public static <T> Module moduleFor(final Class<T> type, Class<? extends OptionHandler<T>> impl) {
return new FactoryModuleBuilder()
.implement(handlerOf(type), impl)
diff --git a/gerrit-util-ssl/.gitignore b/gerrit-util-ssl/.gitignore
index 194bedc..e552ad5 100644
--- a/gerrit-util-ssl/.gitignore
+++ b/gerrit-util-ssl/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-util-ssl.iml
\ No newline at end of file
diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml
index 2e49d47..beedb8f 100644
--- a/gerrit-util-ssl/pom.xml
+++ b/gerrit-util-ssl/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-util-ssl</artifactId>
diff --git a/gerrit-war/.gitignore b/gerrit-war/.gitignore
index 194bedc..dc8c7ad 100644
--- a/gerrit-war/.gitignore
+++ b/gerrit-war/.gitignore
@@ -2,4 +2,5 @@
/.classpath
/.project
/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-war.iml
\ No newline at end of file
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index 733d976a..1f3750e 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -22,7 +22,7 @@
<parent>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
</parent>
<artifactId>gerrit-war</artifactId>
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
index 233d53d..52467a0 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
@@ -14,7 +14,7 @@
package com.google.gerrit.httpd;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.Singleton;
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..519dec8 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
@@ -18,11 +18,12 @@
import static com.google.inject.Stage.PRODUCTION;
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;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.AuthConfigModule;
import com.google.gerrit.server.config.CanonicalWebUrlModule;
@@ -32,11 +33,12 @@
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.contact.HttpContactStoreConnection;
import com.google.gerrit.server.git.LocalDiskRepositoryManager;
-import com.google.gerrit.server.git.PushReplication;
import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
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 +114,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
@@ -193,10 +200,10 @@
modules.add(new ChangeHookRunner.Module());
modules.add(new ReceiveCommitsExecutorModule());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
- modules.add(new EhcachePoolImpl.Module());
+ modules.add(new DefaultCacheFactory.Module());
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() {
@@ -209,18 +216,20 @@
private Injector createSshInjector() {
final List<Module> modules = new ArrayList<Module>();
- modules.add(new SshModule());
+ modules.add(sysInjector.getInstance(SshModule.class));
modules.add(new MasterCommandModule());
return sysInjector.createChildInjector(modules);
}
private Injector createWebInjector() {
final List<Module> modules = new ArrayList<Module>();
+ modules.add(RequestContextFilter.module());
modules.add(sysInjector.getInstance(GitOverHttpModule.class));
modules.add(sshInjector.getInstance(WebModule.class));
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/gerrit-war/src/main/resources/log4j.properties b/gerrit-war/src/main/resources/log4j.properties
index 5993790..45f630e 100644
--- a/gerrit-war/src/main/resources/log4j.properties
+++ b/gerrit-war/src/main/resources/log4j.properties
@@ -48,10 +48,6 @@
log4j.logger.org.openid4java.server.RealmVerifier=ERROR
log4j.logger.org.openid4java.message.AuthSuccess=ERROR
-# Silence non-critical messages from ehcache
-#
-log4j.logger.net.sf.ehcache=WARN
-
# Silence non-critical messages from c3p0 (if used).
#
log4j.logger.com.mchange.v2.c3p0=WARN
diff --git a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
index 117bf61..3ae9440 100644
--- a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
+++ b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://jetty.eclipse.org/configure.dtd">
+<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
<!--
Jetty configuration to place "gerrit.war" into the root context,
diff --git a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml
index 652acad..59cc040 100644
--- a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml
+++ b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://jetty.eclipse.org/configure.dtd">
+<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
<!--
Jetty configuration to correctly handle SSL/HTTPS traffic when
diff --git a/pom.xml b/pom.xml
index 1933329..7b323bb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-parent</artifactId>
<packaging>pom</packaging>
- <version>2.4-SNAPSHOT</version>
+ <version>2.5-SNAPSHOT</version>
<name>Gerrit Code Review - Parent</name>
<url>http://code.google.com/p/gerrit/</url>
@@ -46,11 +46,11 @@
</issueManagement>
<properties>
- <jgitVersion>1.3.0.201202151440-r.75-gff13648</jgitVersion>
+ <jgitVersion>1.3.0.201202151440-r.140-g8c73245</jgitVersion>
<gwtormVersion>1.4</gwtormVersion>
<gwtjsonrpcVersion>1.3</gwtjsonrpcVersion>
<gwtexpuiVersion>1.2.5</gwtexpuiVersion>
- <gwtVersion>2.3.0</gwtVersion>
+ <gwtVersion>2.4.0</gwtVersion>
<slf4jVersion>1.6.1</slf4jVersion>
<guiceVersion>3.0</guiceVersion>
<jettyVersion>7.2.1.v20101111</jettyVersion>
@@ -74,7 +74,7 @@
<module>gerrit-antlr</module>
<module>gerrit-common</module>
- <module>gerrit-ehcache</module>
+ <module>gerrit-cache-h2</module>
<module>gerrit-httpd</module>
<module>gerrit-launcher</module>
<module>gerrit-main</module>
@@ -87,6 +87,10 @@
<module>gerrit-gwtdebug</module>
<module>gerrit-war</module>
+ <module>gerrit-extension-api</module>
+ <module>gerrit-plugin-api</module>
+ <module>gerrit-plugin-archetype</module>
+
<module>gerrit-gwtui</module>
</modules>
@@ -333,7 +337,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
- <version>1.4</version>
+ <version>1.6</version>
</plugin>
<plugin>
@@ -363,7 +367,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>gwt-maven-plugin</artifactId>
- <version>2.3.0</version>
+ <version>2.4.0</version>
</plugin>
<plugin>
@@ -425,6 +429,14 @@
</configuration>
</plugin>
</plugins>
+
+ <extensions>
+ <extension>
+ <groupId>net.anzix.aws</groupId>
+ <artifactId>s3-maven-wagon</artifactId>
+ <version>3.2</version>
+ </extension>
+ </extensions>
</build>
<dependencies>
@@ -444,6 +456,18 @@
<dependencyManagement>
<dependencies>
<dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ <version>2.1</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>12.0</version>
+ </dependency>
+
+ <dependency>
<groupId>gwtorm</groupId>
<artifactId>gwtorm</artifactId>
<version>${gwtormVersion}</version>
@@ -536,12 +560,6 @@
</dependency>
<dependency>
- <groupId>net.sf.ehcache</groupId>
- <artifactId>ehcache-core</artifactId>
- <version>1.7.2</version>
- </dependency>
-
- <dependency>
<groupId>args4j</groupId>
<artifactId>args4j</artifactId>
<version>2.0.16</version>
@@ -805,6 +823,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>
@@ -825,11 +849,6 @@
</repository>
<repository>
- <id>gson</id>
- <url>https://google-gson.googlecode.com/svn/mavenrepo/</url>
- </repository>
-
- <repository>
<id>objectweb-repository</id>
<url>http://maven.objectweb.org/maven2/</url>
</repository>
@@ -838,5 +857,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..e5909e54
--- /dev/null
+++ b/tools/deploy_api.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+
+set -e
+
+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 $type gerrit-extension-api $VER"
+mvn deploy:deploy-file \
+ -DgroupId=com.google.gerrit \
+ -DartifactId=gerrit-extension-api \
+ -Dversion=$VER \
+ -Dpackaging=jar \
+ -Dfile=gerrit-extension-api/target/gerrit-extension-api-$VER-all.jar \
+ -DrepositoryId=gerrit-api-repository \
+ -Durl=$URL
+
+mvn deploy:deploy-file \
+ -DgroupId=com.google.gerrit \
+ -DartifactId=gerrit-extension-api \
+ -Dversion=$VER \
+ -Dpackaging=java-source \
+ -Dfile=gerrit-extension-api/target/gerrit-extension-api-$VER-all-sources.jar \
+ -Djava-source=false \
+ -DrepositoryId=gerrit-api-repository \
+ -Durl=$URL
+
+
+echo "Deploying $type gerrit-plugin-api $VER"
+mvn deploy:deploy-file \
+ -DgroupId=com.google.gerrit \
+ -DartifactId=gerrit-plugin-api \
+ -Dversion=$VER \
+ -Dpackaging=jar \
+ -Dfile=gerrit-plugin-api/target/gerrit-plugin-api-$VER.jar \
+ -DrepositoryId=gerrit-api-repository \
+ -Durl=$URL
+
+mvn deploy:deploy-file \
+ -DgroupId=com.google.gerrit \
+ -DartifactId=gerrit-plugin-api \
+ -Dversion=$VER \
+ -Dpackaging=java-source \
+ -Dfile=gerrit-plugin-api/target/gerrit-plugin-api-$VER-sources.jar \
+ -Djava-source=false \
+ -DrepositoryId=gerrit-api-repository \
+ -Durl=$URL