c1a25104e9
A change to show case this problem was pushed for the cookbook plugin [1]. [1] https://gerrit-review.googlesource.com/#/c/68850 Change-Id: I17abca24018c4512025abbcb0c691da9bd18c4f6 Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
2000 lines
60 KiB
Plaintext
2000 lines
60 KiB
Plaintext
= Gerrit Code Review - Plugin Development
|
|
|
|
The Gerrit server functionality can be extended by installing plugins.
|
|
This page describes how plugins for Gerrit can be developed.
|
|
|
|
Depending on how tightly the extension code is coupled with the Gerrit
|
|
server code, there is a distinction between `plugins` and `extensions`.
|
|
|
|
[[plugin]]
|
|
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.
|
|
|
|
[[extension]]
|
|
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 visibility reduces the extension's
|
|
dependencies, enabling it to be compatible across a wider range
|
|
of server versions.
|
|
|
|
Most of this documentation refers to either type as a plugin.
|
|
|
|
[[getting-started]]
|
|
== Getting started
|
|
|
|
To get started with the development of a plugin there are two
|
|
recommended ways:
|
|
|
|
. use the Gerrit Plugin Maven archetype to create a new plugin project:
|
|
+
|
|
With the Gerrit Plugin Maven archetype you can create a skeleton for a
|
|
plugin project.
|
|
+
|
|
----
|
|
mvn archetype:generate -DarchetypeGroupId=com.google.gerrit \
|
|
-DarchetypeArtifactId=gerrit-plugin-archetype \
|
|
-DarchetypeVersion=2.12-SNAPSHOT \
|
|
-DgroupId=com.googlesource.gerrit.plugins.testplugin \
|
|
-DartifactId=testplugin
|
|
----
|
|
+
|
|
Maven will ask for additional properties and then create the plugin in
|
|
the current directory. To change the default property values answer 'n'
|
|
when Maven asks to confirm the properties configuration. It will then
|
|
ask again for all properties including those with predefined default
|
|
values.
|
|
|
|
. clone the sample plugin:
|
|
+
|
|
This is a project that demonstrates the various features of the
|
|
plugin API. It can be taken as an example to develop an own plugin.
|
|
+
|
|
----
|
|
$ git clone https://gerrit.googlesource.com/plugins/cookbook-plugin
|
|
----
|
|
+
|
|
When starting from this example one should take care to adapt the
|
|
`Gerrit-ApiVersion` in the `pom.xml` to the version of Gerrit for which
|
|
the plugin is developed. If the plugin is developed for a released
|
|
Gerrit version (no `SNAPSHOT` version) then the URL for the
|
|
`gerrit-api-repository` in the `pom.xml` needs to be changed to
|
|
`https://gerrit-api.storage.googleapis.com/release/`.
|
|
|
|
[[API]]
|
|
== API
|
|
|
|
There are two different API formats offered against which plugins can
|
|
be developed:
|
|
|
|
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
|
|
====
|
|
|
|
[[plugin_name]]
|
|
=== Plugin Name
|
|
|
|
A plugin can optionally provide its own plugin name.
|
|
|
|
====
|
|
Gerrit-PluginName: replication
|
|
====
|
|
|
|
This is useful for plugins that contribute plugin-owned capabilities that
|
|
are stored in the `project.config` file. Another use case is to be able to put
|
|
project specific plugin configuration section in `project.config`. In this
|
|
case it is advantageous to reserve the plugin name to access the configuration
|
|
section in the `project.config` file.
|
|
|
|
If `Gerrit-PluginName` is omitted, then the plugin's name is determined from
|
|
the plugin file name.
|
|
|
|
If a plugin provides its own name, then that plugin cannot be deployed
|
|
multiple times under different file names on one Gerrit site.
|
|
|
|
For Maven driven plugins, the following line must be included in the pom.xml
|
|
file:
|
|
|
|
[source,xml]
|
|
----
|
|
<manifestEntries>
|
|
<Gerrit-PluginName>name</Gerrit-PluginName>
|
|
</manifestEntries>
|
|
----
|
|
|
|
For Buck driven plugins, the following line must be included in the BUCK
|
|
configuration file:
|
|
|
|
[source,python]
|
|
----
|
|
manifest_entries = [
|
|
'Gerrit-PluginName: name',
|
|
]
|
|
----
|
|
|
|
A plugin can get its own name injected at runtime:
|
|
|
|
[source,java]
|
|
----
|
|
public class MyClass {
|
|
|
|
private final String pluginName;
|
|
|
|
@Inject
|
|
public MyClass(@PluginName String pluginName) {
|
|
this.pluginName = pluginName;
|
|
}
|
|
|
|
[...]
|
|
}
|
|
----
|
|
|
|
A plugin can get its canonical web URL injected at runtime:
|
|
|
|
[source,java]
|
|
----
|
|
public class MyClass {
|
|
|
|
private final String url;
|
|
|
|
@Inject
|
|
public MyClass(@PluginCanonicalWebUrl String url) {
|
|
this.url = url;
|
|
}
|
|
|
|
[...]
|
|
}
|
|
----
|
|
|
|
The URL is composed of the server's canonical web URL and the plugin's
|
|
name, i.e. `http://review.example.com:8080/plugin-name`.
|
|
|
|
The canonical web URL may be injected into any .jar plugin regardless of
|
|
whether or not the plugin provides an HTTP servlet.
|
|
|
|
[[reload_method]]
|
|
=== 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.
|
|
|
|
To reload/restart a plugin the link:cmd-plugin-reload.html[plugin reload]
|
|
command can be used.
|
|
|
|
[[init_step]]
|
|
=== Init step
|
|
|
|
Plugins can contribute their own "init step" during the Gerrit init
|
|
wizard. This is useful for guiding the Gerrit administrator through
|
|
the settings needed by the plugin to work properly.
|
|
|
|
For instance plugins to integrate Jira issues to Gerrit changes may
|
|
contribute their own "init step" to allow configuring the Jira URL,
|
|
credentials and possibly verify connectivity to validate them.
|
|
|
|
====
|
|
Gerrit-InitStep: tld.example.project.MyInitStep
|
|
====
|
|
|
|
MyInitStep needs to follow the standard Gerrit InitStep syntax
|
|
and behavior: writing to the console using the injected ConsoleUI
|
|
and accessing / changing configuration settings using Section.Factory.
|
|
|
|
In addition to the standard Gerrit init injections, plugins receive
|
|
the @PluginName String injection containing their own plugin name.
|
|
|
|
During their initialization plugins may get access to the
|
|
`project.config` file of the `All-Projects` project and they are able
|
|
to store configuration parameters in it. For this a plugin `InitStep`
|
|
can get `com.google.gerrit.pgm.init.api.AllProjectsConfig` injected:
|
|
|
|
[source,java]
|
|
----
|
|
public class MyInitStep implements InitStep {
|
|
private final String pluginName;
|
|
private final ConsoleUI ui;
|
|
private final AllProjectsConfig allProjectsConfig;
|
|
|
|
public MyInitStep(@PluginName String pluginName, ConsoleUI ui,
|
|
AllProjectsConfig allProjectsConfig) {
|
|
this.pluginName = pluginName;
|
|
this.ui = ui;
|
|
this.allProjectsConfig = allProjectsConfig;
|
|
}
|
|
|
|
@Override
|
|
public void run() throws Exception {
|
|
}
|
|
|
|
@Override
|
|
public void postRun() throws Exception {
|
|
ui.message("\n");
|
|
ui.header(pluginName + " Integration");
|
|
boolean enabled = ui.yesno(true, "By default enabled for all projects");
|
|
Config cfg = allProjectsConfig.load().getConfig();
|
|
if (enabled) {
|
|
cfg.setBoolean("plugin", pluginName, "enabled", enabled);
|
|
} else {
|
|
cfg.unset("plugin", pluginName, "enabled");
|
|
}
|
|
allProjectsConfig.save(pluginName, "Initialize " + pluginName + " Integration");
|
|
}
|
|
}
|
|
----
|
|
|
|
Bear in mind that the Plugin's InitStep class will be loaded but
|
|
the standard Gerrit runtime environment is not available and the plugin's
|
|
own Guice modules were not initialized.
|
|
This means the InitStep for a plugin is not executed in the same way that
|
|
the plugin executes within the server, and may mean a plugin author cannot
|
|
trivially reuse runtime code during init.
|
|
|
|
For instance a plugin that wants to verify connectivity may need to statically
|
|
call the constructor of their connection class, passing in values obtained
|
|
from the Section.Factory rather than from an injected Config object.
|
|
|
|
Plugins' InitSteps are executed during the "Gerrit Plugin init" phase, after
|
|
the extraction of the plugins embedded in the distribution .war file into
|
|
`$GERRIT_SITE/plugins` and before the DB Schema initialization or upgrade.
|
|
|
|
A plugin's InitStep cannot refer to Gerrit's DB Schema or any other Gerrit
|
|
runtime objects injected at startup.
|
|
|
|
[source,java]
|
|
----
|
|
public class MyInitStep implements InitStep {
|
|
private final ConsoleUI ui;
|
|
private final Section.Factory sections;
|
|
private final String pluginName;
|
|
|
|
@Inject
|
|
public GitBlitInitStep(final ConsoleUI ui, Section.Factory sections, @PluginName String pluginName) {
|
|
this.ui = ui;
|
|
this.sections = sections;
|
|
this.pluginName = pluginName;
|
|
}
|
|
|
|
@Override
|
|
public void run() throws Exception {
|
|
ui.header("\nMy plugin");
|
|
|
|
Section mySection = getSection("myplugin", null);
|
|
mySection.string("Link name", "linkname", "MyLink");
|
|
}
|
|
|
|
@Override
|
|
public void postRun() throws Exception {
|
|
}
|
|
}
|
|
----
|
|
|
|
[[classpath]]
|
|
== 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.
|
|
|
|
[[events]]
|
|
== Listening to Events
|
|
|
|
Certain operations in Gerrit trigger events. Plugins may receive
|
|
notifications of these events by implementing the corresponding
|
|
listeners.
|
|
|
|
* `com.google.gerrit.common.EventListener`:
|
|
+
|
|
Allows to listen to events. These are the same
|
|
link:cmd-stream-events.html#events[events] that are also streamed by
|
|
the link:cmd-stream-events.html[gerrit stream-events] command.
|
|
|
|
* `com.google.gerrit.extensions.events.LifecycleListener`:
|
|
+
|
|
Plugin start and stop
|
|
|
|
* `com.google.gerrit.extensions.events.NewProjectCreatedListener`:
|
|
+
|
|
Project creation
|
|
|
|
* `com.google.gerrit.extensions.events.ProjectDeletedListener`:
|
|
+
|
|
Project deletion
|
|
|
|
* `com.google.gerrit.extensions.events.HeadUpdatedListener`:
|
|
+
|
|
Update of HEAD on a project
|
|
|
|
* `com.google.gerrit.extensions.events.UsageDataPublishedListener`:
|
|
+
|
|
Publication of usage data
|
|
|
|
* `com.google.gerrit.extensions.events.GarbageCollectorListener`:
|
|
+
|
|
Garbage collection ran on a project
|
|
|
|
[[stream-events]]
|
|
== Sending Events to the Events Stream
|
|
|
|
Plugins may send events to the events stream where consumers of
|
|
Gerrit's `stream-events` ssh command will receive them.
|
|
|
|
To send an event, the plugin must invoke one of the `postEvent`
|
|
methods in the `ChangeHookRunner` class, passing an instance of
|
|
its own custom event class derived from
|
|
`com.google.gerrit.server.events.Event`.
|
|
|
|
Plugins which define new Events should register them via the
|
|
`com.google.gerrit.server.events.EventTypes.registerClass()`
|
|
method. This will make the EventType known to the system.
|
|
Deserialzing events with the
|
|
`com.google.gerrit.server.events.EventDeserializer` class requires
|
|
that the event be registered in EventTypes.
|
|
|
|
[[validation]]
|
|
== Validation Listeners
|
|
|
|
Certain operations in Gerrit can be validated by plugins by
|
|
implementing the corresponding link:config-validation.html[listeners].
|
|
|
|
[[receive-pack]]
|
|
== Receive Pack Initializers
|
|
|
|
Plugins may provide ReceivePack initializers which will be invoked
|
|
by Gerrit just before a ReceivePack instance will be used. Usually,
|
|
plugins will make use of the setXXX methods on the ReceivePack to
|
|
set additional properties on it.
|
|
|
|
[[post-receive-hook]]
|
|
== Post Receive-Pack Hooks
|
|
|
|
Plugins may register PostReceiveHook instances in order to get
|
|
notified when JGit successfully receives a pack. This may be useful
|
|
for those plugins which would like to monitor changes in Git
|
|
repositories.
|
|
|
|
[[pre-upload-hook]]
|
|
== Pre Upload-Pack Hooks
|
|
|
|
Plugins may register PreUploadHook instances in order to get
|
|
notified when JGit is about to upload a pack. This may be useful
|
|
for those plugins which would like to monitor usage in Git
|
|
repositories.
|
|
|
|
[[ssh]]
|
|
== 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:
|
|
|
|
[source,java]
|
|
----
|
|
import com.google.gerrit.sshd.SshCommand;
|
|
import com.google.gerrit.sshd.CommandMetaData;
|
|
|
|
@CommandMetaData(name="print", description="Print hello command")
|
|
class PrintHello extends SshCommand {
|
|
@Override
|
|
protected 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` annotation:
|
|
|
|
[source,java]
|
|
----
|
|
import com.google.gerrit.extensions.annotations.Export;
|
|
import com.google.gerrit.sshd.SshCommand;
|
|
|
|
@Export("print")
|
|
class PrintHello extends SshCommand {
|
|
@Override
|
|
protected 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:
|
|
|
|
[source,java]
|
|
----
|
|
import com.google.gerrit.sshd.PluginCommandModule;
|
|
|
|
class MyCommands extends PluginCommandModule {
|
|
@Override
|
|
protected void configureCommands() {
|
|
command(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
|
|
----
|
|
|
|
[[multiple-commands]]
|
|
=== Multiple Commands bound to one implementation
|
|
|
|
Multiple SSH commands can be bound to the same implementation class. For
|
|
example a Gerrit Shell plugin can bind different shell commands to the same
|
|
implementation class:
|
|
|
|
[source,java]
|
|
----
|
|
public class SshShellModule extends PluginCommandModule {
|
|
@Override
|
|
protected void configureCommands() {
|
|
command("ls").to(ShellCommand.class);
|
|
command("ps").to(ShellCommand.class);
|
|
[...]
|
|
}
|
|
}
|
|
----
|
|
|
|
With the possible implementation:
|
|
|
|
[source,java]
|
|
----
|
|
public class ShellCommand extends SshCommand {
|
|
@Override
|
|
protected void run() throws UnloggedFailure {
|
|
String cmd = getName().substring(getPluginName().length() + 1);
|
|
ProcessBuilder proc = new ProcessBuilder(cmd);
|
|
Process cmd = proc.start();
|
|
[...]
|
|
}
|
|
}
|
|
----
|
|
|
|
And the call:
|
|
|
|
----
|
|
$ ssh -p 29418 review.example.com shell ls
|
|
$ ssh -p 29418 review.example.com shell ps
|
|
----
|
|
|
|
[[root-level-commands]]
|
|
=== Root Level Commands
|
|
|
|
Single command plugins are also supported. In this scenario plugin binds
|
|
SSH command to its own name. `SshModule` must inherit from
|
|
`SingleCommandPluginModule` class:
|
|
|
|
[source,java]
|
|
----
|
|
public class SshModule extends SingleCommandPluginModule {
|
|
@Override
|
|
protected void configure(LinkedBindingBuilder<Command> b) {
|
|
b.to(ShellCommand.class);
|
|
}
|
|
}
|
|
----
|
|
|
|
If the plugin above is deployed under sh.jar file in `$site/plugins`
|
|
directory, generic commands can be called without specifying the
|
|
actual SSH command. Note in the example below, that the called commands
|
|
`ls` and `ps` was not explicitly bound:
|
|
|
|
----
|
|
$ ssh -p 29418 review.example.com sh ls
|
|
$ ssh -p 29418 review.example.com sh ps
|
|
----
|
|
|
|
[[simple-configuration]]
|
|
== Simple Configuration in `gerrit.config`
|
|
|
|
In Gerrit, global configuration is stored in the `gerrit.config` file.
|
|
If a plugin needs global configuration, this configuration should be
|
|
stored in a `plugin` subsection in the `gerrit.config` file.
|
|
|
|
This approach of storing the plugin configuration is only suitable for
|
|
plugins that have a simple configuration that only consists of
|
|
key-value pairs. With this approach it is not possible to have
|
|
subsections in the plugin configuration. Plugins that require a complex
|
|
configuration need to store their configuration in their
|
|
link:#configuration[own configuration file] where they can make use of
|
|
subsections. On the other hand storing the plugin configuration in a
|
|
'plugin' subsection in the `gerrit.config` file has the advantage that
|
|
administrators have all configuration parameters in one file, instead
|
|
of having one configuration file per plugin.
|
|
|
|
To avoid conflicts with other plugins, it is recommended that plugins
|
|
only use the `plugin` subsection with their own name. For example the
|
|
`helloworld` plugin should store its configuration in the
|
|
`plugin.helloworld` subsection:
|
|
|
|
----
|
|
[plugin "helloworld"]
|
|
language = Latin
|
|
----
|
|
|
|
Via the `com.google.gerrit.server.config.PluginConfigFactory` class a
|
|
plugin can easily access its configuration and there is no need for a
|
|
plugin to parse the `gerrit.config` file on its own:
|
|
|
|
[source,java]
|
|
----
|
|
@Inject
|
|
private com.google.gerrit.server.config.PluginConfigFactory cfg;
|
|
|
|
[...]
|
|
|
|
String language = cfg.getFromGerritConfig("helloworld")
|
|
.getString("language", "English");
|
|
----
|
|
|
|
[[configuration]]
|
|
== Configuration in own config file
|
|
|
|
Plugins can store their configuration in an own configuration file.
|
|
This makes sense if the plugin configuration is rather complex and
|
|
requires the usage of subsections. Plugins that have a simple
|
|
key-value pair configuration can store their configuration in a
|
|
link:#simple-configuration[`plugin` subsection of the `gerrit.config`
|
|
file].
|
|
|
|
The plugin configuration file must be named after the plugin and must
|
|
be located in the `etc` folder of the review site. For example a
|
|
configuration file for a `default-reviewer` plugin could look like
|
|
this:
|
|
|
|
.$site_path/etc/default-reviewer.config
|
|
----
|
|
[branch "refs/heads/master"]
|
|
reviewer = Project Owners
|
|
reviewer = john.doe@example.com
|
|
[match "file:^.*\.txt"]
|
|
reviewer = My Info Developers
|
|
----
|
|
|
|
Via the `com.google.gerrit.server.config.PluginConfigFactory` class a
|
|
plugin can easily access its configuration:
|
|
|
|
[source,java]
|
|
----
|
|
@Inject
|
|
private com.google.gerrit.server.config.PluginConfigFactory cfg;
|
|
|
|
[...]
|
|
|
|
String[] reviewers = cfg.getGlobalPluginConfig("default-reviewer")
|
|
.getStringList("branch", "refs/heads/master", "reviewer");
|
|
----
|
|
|
|
|
|
[[simple-project-specific-configuration]]
|
|
== Simple Project Specific Configuration in `project.config`
|
|
|
|
In Gerrit, project specific configuration is stored in the project's
|
|
`project.config` file on the `refs/meta/config` branch. If a plugin
|
|
needs configuration on project level (e.g. to enable its functionality
|
|
only for certain projects), this configuration should be stored in a
|
|
`plugin` subsection in the project's `project.config` file.
|
|
|
|
This approach of storing the plugin configuration is only suitable for
|
|
plugins that have a simple configuration that only consists of
|
|
key-value pairs. With this approach it is not possible to have
|
|
subsections in the plugin configuration. Plugins that require a complex
|
|
configuration need to store their configuration in their
|
|
link:#project-specific-configuration[own configuration file] where they
|
|
can make use of subsections. On the other hand storing the plugin
|
|
configuration in a 'plugin' subsection in the `project.config` file has
|
|
the advantage that project owners have all configuration parameters in
|
|
one file, instead of having one configuration file per plugin.
|
|
|
|
To avoid conflicts with other plugins, it is recommended that plugins
|
|
only use the `plugin` subsection with their own name. For example the
|
|
`helloworld` plugin should store its configuration in the
|
|
`plugin.helloworld` subsection:
|
|
|
|
----
|
|
[plugin "helloworld"]
|
|
enabled = true
|
|
----
|
|
|
|
Via the `com.google.gerrit.server.config.PluginConfigFactory` class a
|
|
plugin can easily access its project specific configuration and there
|
|
is no need for a plugin to parse the `project.config` file on its own:
|
|
|
|
[source,java]
|
|
----
|
|
@Inject
|
|
private com.google.gerrit.server.config.PluginConfigFactory cfg;
|
|
|
|
[...]
|
|
|
|
boolean enabled = cfg.getFromProjectConfig(project, "helloworld")
|
|
.getBoolean("enabled", false);
|
|
----
|
|
|
|
It is also possible to get missing configuration parameters inherited
|
|
from the parent projects:
|
|
|
|
[source,java]
|
|
----
|
|
@Inject
|
|
private com.google.gerrit.server.config.PluginConfigFactory cfg;
|
|
|
|
[...]
|
|
|
|
boolean enabled = cfg.getFromProjectConfigWithInheritance(project, "helloworld")
|
|
.getBoolean("enabled", false);
|
|
----
|
|
|
|
Project owners can edit the project configuration by fetching the
|
|
`refs/meta/config` branch, editing the `project.config` file and
|
|
pushing the commit back.
|
|
|
|
Plugin configuration values that are stored in the `project.config`
|
|
file can be exposed in the ProjectInfoScreen to allow project owners
|
|
to see and edit them from the UI.
|
|
|
|
For this an instance of `ProjectConfigEntry` needs to be bound for each
|
|
parameter. The export name must be a valid Git variable name. The
|
|
variable name is case-insensitive, allows only alphanumeric characters
|
|
and '-', and must start with an alphabetic character.
|
|
|
|
The example below shows how the parameters `plugin.helloworld.enabled`
|
|
and `plugin.helloworld.language` are bound to be editable from the
|
|
Web UI. For the parameter `plugin.helloworld.enabled` "Enable Greeting"
|
|
is provided as display name and the default value is set to `true`.
|
|
For the parameter `plugin.helloworld.language` "Preferred Language"
|
|
is provided as display name and "en" is set as default value.
|
|
|
|
[source,java]
|
|
----
|
|
class Module extends AbstractModule {
|
|
@Override
|
|
protected void configure() {
|
|
bind(ProjectConfigEntry.class)
|
|
.annotatedWith(Exports.named("enabled"))
|
|
.toInstance(new ProjectConfigEntry("Enable Greeting", true));
|
|
bind(ProjectConfigEntry.class)
|
|
.annotatedWith(Exports.named("language"))
|
|
.toInstance(new ProjectConfigEntry("Preferred Language", "en"));
|
|
}
|
|
}
|
|
----
|
|
|
|
By overwriting the `onUpdate` method of `ProjectConfigEntry` plugins
|
|
can be notified when this configuration parameter is updated on a
|
|
project.
|
|
|
|
[[project-specific-configuration]]
|
|
== Project Specific Configuration in own config file
|
|
|
|
Plugins can store their project specific configuration in an own
|
|
configuration file in the projects `refs/meta/config` branch.
|
|
This makes sense if the plugins project specific configuration is
|
|
rather complex and requires the usage of subsections. Plugins that
|
|
have a simple key-value pair configuration can store their project
|
|
specific configuration in a link:#simple-project-specific-configuration[
|
|
`plugin` subsection of the `project.config` file].
|
|
|
|
The plugin configuration file in the `refs/meta/config` branch must be
|
|
named after the plugin. For example a configuration file for a
|
|
`default-reviewer` plugin could look like this:
|
|
|
|
.default-reviewer.config
|
|
----
|
|
[branch "refs/heads/master"]
|
|
reviewer = Project Owners
|
|
reviewer = john.doe@example.com
|
|
[match "file:^.*\.txt"]
|
|
reviewer = My Info Developers
|
|
----
|
|
|
|
Via the `com.google.gerrit.server.config.PluginConfigFactory` class a
|
|
plugin can easily access its project specific configuration:
|
|
|
|
[source,java]
|
|
----
|
|
@Inject
|
|
private com.google.gerrit.server.config.PluginConfigFactory cfg;
|
|
|
|
[...]
|
|
|
|
String[] reviewers = cfg.getProjectPluginConfig(project, "default-reviewer")
|
|
.getStringList("branch", "refs/heads/master", "reviewer");
|
|
----
|
|
|
|
It is also possible to get missing configuration parameters inherited
|
|
from the parent projects:
|
|
|
|
[source,java]
|
|
----
|
|
@Inject
|
|
private com.google.gerrit.server.config.PluginConfigFactory cfg;
|
|
|
|
[...]
|
|
|
|
String[] reviewers = cfg.getProjectPluginConfigWithInheritance(project, "default-reviewer")
|
|
.getStringList("branch", "refs/heads/master", "reviewer");
|
|
----
|
|
|
|
Project owners can edit the project configuration by fetching the
|
|
`refs/meta/config` branch, editing the `<plugin-name>.config` file and
|
|
pushing the commit back.
|
|
|
|
== React on changes in project configuration
|
|
|
|
If a plugin wants to react on changes in the project configuration, it
|
|
can implement a `GitReferenceUpdatedListener` and filter on events for
|
|
the `refs/meta/config` branch:
|
|
|
|
[source,java]
|
|
----
|
|
public class MyListener implements GitReferenceUpdatedListener {
|
|
|
|
private final MetaDataUpdate.Server metaDataUpdateFactory;
|
|
|
|
@Inject
|
|
MyListener(MetaDataUpdate.Server metaDataUpdateFactory) {
|
|
this.metaDataUpdateFactory = metaDataUpdateFactory;
|
|
}
|
|
|
|
@Override
|
|
public void onGitReferenceUpdated(Event event) {
|
|
if (event.getRefName().equals(RefNames.REFS_CONFIG)) {
|
|
Project.NameKey p = new Project.NameKey(event.getProjectName());
|
|
try {
|
|
ProjectConfig oldCfg = parseConfig(p, event.getOldObjectId());
|
|
ProjectConfig newCfg = parseConfig(p, event.getNewObjectId());
|
|
|
|
if (oldCfg != null && newCfg != null
|
|
&& !oldCfg.getProject().getSubmitType().equals(newCfg.getProject().getSubmitType())) {
|
|
// submit type has changed
|
|
...
|
|
}
|
|
} catch (IOException | ConfigInvalidException e) {
|
|
...
|
|
}
|
|
}
|
|
}
|
|
|
|
private ProjectConfig parseConfig(Project.NameKey p, String idStr)
|
|
throws IOException, ConfigInvalidException, RepositoryNotFoundException {
|
|
ObjectId id = ObjectId.fromString(idStr);
|
|
if (ObjectId.zeroId().equals(id)) {
|
|
return null;
|
|
}
|
|
return ProjectConfig.read(metaDataUpdateFactory.create(p), id);
|
|
}
|
|
}
|
|
----
|
|
|
|
|
|
[[capabilities]]
|
|
== Plugin Owned Capabilities
|
|
|
|
Plugins may provide their own capabilities and restrict usage of SSH
|
|
commands or `UiAction` to the users who are granted those capabilities.
|
|
|
|
Plugins define the capabilities by overriding the `CapabilityDefinition`
|
|
abstract class:
|
|
|
|
[source,java]
|
|
----
|
|
public class PrintHelloCapability extends CapabilityDefinition {
|
|
@Override
|
|
public String getDescription() {
|
|
return "Print Hello";
|
|
}
|
|
}
|
|
----
|
|
|
|
If no Guice modules are declared in the manifest, capability may
|
|
use auto-registration by providing an `@Export` annotation:
|
|
|
|
[source,java]
|
|
----
|
|
@Export("printHello")
|
|
public class PrintHelloCapability extends CapabilityDefinition {
|
|
[...]
|
|
}
|
|
----
|
|
|
|
Otherwise the capability must be bound in a plugin module:
|
|
|
|
[source,java]
|
|
----
|
|
public class HelloWorldModule extends AbstractModule {
|
|
@Override
|
|
protected void configure() {
|
|
bind(CapabilityDefinition.class)
|
|
.annotatedWith(Exports.named("printHello"))
|
|
.to(PrintHelloCapability.class);
|
|
}
|
|
}
|
|
----
|
|
|
|
With a plugin-owned capability defined in this way, it is possible to restrict
|
|
usage of an SSH command or `UiAction` to members of the group that were granted
|
|
this capability in the usual way, using the `RequiresCapability` annotation:
|
|
|
|
[source,java]
|
|
----
|
|
@RequiresCapability("printHello")
|
|
@CommandMetaData(name="print", description="Print greeting in different languages")
|
|
public final class PrintHelloWorldCommand extends SshCommand {
|
|
[...]
|
|
}
|
|
----
|
|
|
|
Or with `UiAction`:
|
|
|
|
[source,java]
|
|
----
|
|
@RequiresCapability("printHello")
|
|
public class SayHelloAction extends UiAction<RevisionResource>
|
|
implements RestModifyView<RevisionResource, SayHelloAction.Input> {
|
|
[...]
|
|
}
|
|
----
|
|
|
|
Capability scope was introduced to differentiate between plugin-owned
|
|
capabilities and core capabilities. Per default the scope of the
|
|
`@RequiresCapability` annotation is `CapabilityScope.CONTEXT`, that means:
|
|
|
|
* when `@RequiresCapability` is used within a plugin the scope of the
|
|
capability is assumed to be that plugin.
|
|
|
|
* If `@RequiresCapability` is used within the core Gerrit Code Review server
|
|
(and thus is outside of a plugin) the scope is the core server and will use
|
|
the `GlobalCapability` known to Gerrit Code Review server.
|
|
|
|
If a plugin needs to use a core capability name (e.g. "administrateServer")
|
|
this can be specified by setting `scope = CapabilityScope.CORE`:
|
|
|
|
[source,java]
|
|
----
|
|
@RequiresCapability(value = "administrateServer", scope =
|
|
CapabilityScope.CORE)
|
|
[...]
|
|
----
|
|
|
|
[[ui_extension]]
|
|
== UI Extension
|
|
|
|
Plugins can contribute UI actions on core Gerrit pages. This is useful
|
|
for workflow customization or exposing plugin functionality through the
|
|
UI in addition to SSH commands and the REST API.
|
|
|
|
For instance a plugin to integrate Jira with Gerrit changes may
|
|
contribute a "File bug" button to allow filing a bug from the change
|
|
page or plugins to integrate continuous integration systems may
|
|
contribute a "Schedule" button to allow a CI build to be scheduled
|
|
manually from the patch set panel.
|
|
|
|
Two different places on core Gerrit pages are supported:
|
|
|
|
* Change screen
|
|
* Project info screen
|
|
|
|
Plugins contribute UI actions by implementing the `UiAction` interface:
|
|
|
|
[source,java]
|
|
----
|
|
@RequiresCapability("printHello")
|
|
class HelloWorldAction implements UiAction<RevisionResource>,
|
|
RestModifyView<RevisionResource, HelloWorldAction.Input> {
|
|
static class Input {
|
|
boolean french;
|
|
String message;
|
|
}
|
|
|
|
private Provider<CurrentUser> user;
|
|
|
|
@Inject
|
|
HelloWorldAction(Provider<CurrentUser> user) {
|
|
this.user = user;
|
|
}
|
|
|
|
@Override
|
|
public String apply(RevisionResource rev, Input input) {
|
|
final String greeting = input.french
|
|
? "Bonjour"
|
|
: "Hello";
|
|
return String.format("%s %s from change %s, patch set %d!",
|
|
greeting,
|
|
Strings.isNullOrEmpty(input.message)
|
|
? Objects.firstNonNull(user.get().getUserName(), "world")
|
|
: input.message,
|
|
rev.getChange().getId().toString(),
|
|
rev.getPatchSet().getPatchSetId());
|
|
}
|
|
|
|
@Override
|
|
public Description getDescription(
|
|
RevisionResource resource) {
|
|
return new Description()
|
|
.setLabel("Say hello")
|
|
.setTitle("Say hello in different languages");
|
|
}
|
|
}
|
|
----
|
|
|
|
Sometimes plugins may want to be able to change the state of a patch set or
|
|
change in the `UiAction.apply()` method and reflect these changes on the core
|
|
UI. For example a buildbot plugin which exposes a 'Schedule' button on the
|
|
patch set panel may want to disable that button after the build was scheduled
|
|
and update the tooltip of that button. But because of Gerrit's caching
|
|
strategy the following must be taken into consideration.
|
|
|
|
The browser is allowed to cache the `UiAction` information until something on
|
|
the change is modified. More accurately the change row needs to be modified in
|
|
the database to have a more recent `lastUpdatedOn` or a new `rowVersion`, or
|
|
the +refs/meta/config+ of the project or any parents needs to change to a new
|
|
SHA-1. The ETag SHA-1 computation code can be found in the
|
|
`ChangeResource.getETag()` method.
|
|
|
|
The easiest way to accomplish this is to update `lastUpdatedOn` of the change:
|
|
|
|
[source,java]
|
|
----
|
|
@Override
|
|
public Object apply(RevisionResource rcrs, Input in) {
|
|
// schedule a build
|
|
[...]
|
|
// update change
|
|
ReviewDb db = dbProvider.get();
|
|
db.changes().beginTransaction(change.getId());
|
|
try {
|
|
change = db.changes().atomicUpdate(
|
|
change.getId(),
|
|
new AtomicUpdate<Change>() {
|
|
@Override
|
|
public Change update(Change change) {
|
|
ChangeUtil.updated(change);
|
|
return change;
|
|
}
|
|
});
|
|
db.commit();
|
|
} finally {
|
|
db.rollback();
|
|
}
|
|
[...]
|
|
}
|
|
----
|
|
|
|
`UiAction` must be bound in a plugin module:
|
|
|
|
[source,java]
|
|
----
|
|
public class Module extends AbstractModule {
|
|
@Override
|
|
protected void configure() {
|
|
install(new RestApiModule() {
|
|
@Override
|
|
protected void configure() {
|
|
post(REVISION_KIND, "say-hello")
|
|
.to(HelloWorldAction.class);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
----
|
|
|
|
The module above must be declared in the `pom.xml` for Maven driven
|
|
plugins:
|
|
|
|
[source,xml]
|
|
----
|
|
<manifestEntries>
|
|
<Gerrit-Module>com.googlesource.gerrit.plugins.cookbook.Module</Gerrit-Module>
|
|
</manifestEntries>
|
|
----
|
|
|
|
or in the `BUCK` configuration file for Buck driven plugins:
|
|
|
|
[source,python]
|
|
----
|
|
manifest_entries = [
|
|
'Gerrit-Module: com.googlesource.gerrit.plugins.cookbook.Module',
|
|
]
|
|
----
|
|
|
|
In some use cases more user input must be gathered, for that `UiAction` can be
|
|
combined with the JavaScript API. This would display a small popup near the
|
|
activation button to gather additional input from the user. The JS file is
|
|
typically put in the `static` folder within the plugin's directory:
|
|
|
|
[source,javascript]
|
|
----
|
|
Gerrit.install(function(self) {
|
|
function onSayHello(c) {
|
|
var f = c.textfield();
|
|
var t = c.checkbox();
|
|
var b = c.button('Say hello', {onclick: function(){
|
|
c.call(
|
|
{message: f.value, french: t.checked},
|
|
function(r) {
|
|
c.hide();
|
|
window.alert(r);
|
|
c.refresh();
|
|
});
|
|
}});
|
|
c.popup(c.div(
|
|
c.prependLabel('Greeting message', f),
|
|
c.br(),
|
|
c.label(t, 'french'),
|
|
c.br(),
|
|
b));
|
|
f.focus();
|
|
}
|
|
self.onAction('revision', 'say-hello', onSayHello);
|
|
});
|
|
----
|
|
|
|
The JS module must be exposed as a `WebUiPlugin` and bound as
|
|
an HTTP Module:
|
|
|
|
[source,java]
|
|
----
|
|
public class HttpModule extends HttpPluginModule {
|
|
@Override
|
|
protected void configureServlets() {
|
|
DynamicSet.bind(binder(), WebUiPlugin.class)
|
|
.toInstance(new JavaScriptPlugin("hello.js"));
|
|
}
|
|
}
|
|
----
|
|
|
|
The HTTP module above must be declared in the `pom.xml` for Maven
|
|
driven plugins:
|
|
|
|
[source,xml]
|
|
----
|
|
<manifestEntries>
|
|
<Gerrit-HttpModule>com.googlesource.gerrit.plugins.cookbook.HttpModule</Gerrit-HttpModule>
|
|
</manifestEntries>
|
|
----
|
|
|
|
or in the `BUCK` configuration file for Buck driven plugins
|
|
|
|
[source,python]
|
|
----
|
|
manifest_entries = [
|
|
'Gerrit-HttpModule: com.googlesource.gerrit.plugins.cookbook.HttpModule',
|
|
]
|
|
----
|
|
|
|
If `UiAction` is annotated with the `@RequiresCapability` annotation, then the
|
|
capability check is done during the `UiAction` gathering, so the plugin author
|
|
doesn't have to set `UiAction.Description.setVisible()` explicitly in this
|
|
case.
|
|
|
|
The following prerequisities must be met, to satisfy the capability check:
|
|
|
|
* user is authenticated
|
|
* user is a member of a group which has the `Administrate Server` capability, or
|
|
* user is a member of a group which has the required capability
|
|
|
|
The `apply` method is called when the button is clicked. If `UiAction` is
|
|
combined with JavaScript API (its own JavaScript function is provided),
|
|
then a popup dialog is normally opened to gather additional user input.
|
|
A new button is placed on the popup dialog to actually send the request.
|
|
|
|
Every `UiAction` exposes a REST API endpoint. The endpoint from the example above
|
|
can be accessed from any REST client, i. e.:
|
|
|
|
====
|
|
curl -X POST -H "Content-Type: application/json" \
|
|
-d '{message: "François", french: true}' \
|
|
--digest --user joe:secret \
|
|
http://host:port/a/changes/1/revisions/1/cookbook~say-hello
|
|
"Bonjour François from change 1, patch set 1!"
|
|
====
|
|
|
|
A special case is to bind an endpoint without a view name. This is
|
|
particularly useful for `DELETE` requests:
|
|
|
|
[source,java]
|
|
----
|
|
public class Module extends AbstractModule {
|
|
@Override
|
|
protected void configure() {
|
|
install(new RestApiModule() {
|
|
@Override
|
|
protected void configure() {
|
|
delete(PROJECT_KIND)
|
|
.to(DeleteProject.class);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
----
|
|
|
|
For a `UiAction` bound this way, a JS API function can be provided.
|
|
|
|
Currently only one restriction exists: per plugin only one `UiAction`
|
|
can be bound per resource without view name. To define a JS function
|
|
for the `UiAction`, "/" must be used as the name:
|
|
|
|
[source,javascript]
|
|
----
|
|
Gerrit.install(function(self) {
|
|
function onDeleteProject(c) {
|
|
[...]
|
|
}
|
|
self.onAction('project', '/', onDeleteProject);
|
|
});
|
|
----
|
|
|
|
[[top-menu-extensions]]
|
|
== Top Menu Extensions
|
|
|
|
Plugins can contribute items to Gerrit's top menu.
|
|
|
|
A single top menu extension can have multiple elements and will be put as
|
|
the last element in Gerrit's top menu.
|
|
|
|
Plugins define the top menu entries by implementing `TopMenu` interface:
|
|
|
|
[source,java]
|
|
----
|
|
public class MyTopMenuExtension implements TopMenu {
|
|
|
|
@Override
|
|
public List<MenuEntry> getEntries() {
|
|
return Lists.newArrayList(
|
|
new MenuEntry("Top Menu Entry", Lists.newArrayList(
|
|
new MenuItem("Gerrit", "http://gerrit.googlecode.com/"))));
|
|
}
|
|
}
|
|
----
|
|
|
|
Plugins can also add additional menu items to Gerrit's top menu entries
|
|
by defining a `MenuEntry` that has the same name as a Gerrit top menu
|
|
entry:
|
|
|
|
[source,java]
|
|
----
|
|
public class MyTopMenuExtension implements TopMenu {
|
|
|
|
@Override
|
|
public List<MenuEntry> getEntries() {
|
|
return Lists.newArrayList(
|
|
new MenuEntry(GerritTopMenu.PROJECTS, Lists.newArrayList(
|
|
new MenuItem("Browse Repositories", "https://gerrit.googlesource.com/"))));
|
|
}
|
|
}
|
|
----
|
|
|
|
`MenuItems` that are bound for the `MenuEntry` with the name
|
|
`GerritTopMenu.PROJECTS` can contain a `${projectName}` placeholder
|
|
which is automatically replaced by the actual project name.
|
|
|
|
E.g. plugins may register an link:#http[HTTP Servlet] to handle project
|
|
specific requests and add an menu item for this:
|
|
|
|
[source,java]
|
|
---
|
|
new MenuItem("My Screen", "/plugins/myplugin/project/${projectName}");
|
|
---
|
|
|
|
This also enables plugins to provide menu items for project aware
|
|
screens:
|
|
|
|
[source,java]
|
|
---
|
|
new MenuItem("My Screen", "/x/my-screen/for/${projectName}");
|
|
---
|
|
|
|
If no Guice modules are declared in the manifest, the top menu extension may use
|
|
auto-registration by providing an `@Listen` annotation:
|
|
|
|
[source,java]
|
|
----
|
|
@Listen
|
|
public class MyTopMenuExtension implements TopMenu {
|
|
[...]
|
|
}
|
|
----
|
|
|
|
Otherwise the top menu extension must be bound in the plugin module used
|
|
for the Gerrit system injector (Gerrit-Module entry in MANIFEST.MF):
|
|
|
|
[source,java]
|
|
----
|
|
package com.googlesource.gerrit.plugins.helloworld;
|
|
|
|
public class HelloWorldModule extends AbstractModule {
|
|
@Override
|
|
protected void configure() {
|
|
DynamicSet.bind(binder(), TopMenu.class).to(MyTopMenuExtension.class);
|
|
}
|
|
}
|
|
----
|
|
|
|
[source,manifest]
|
|
----
|
|
Gerrit-ApiType: plugin
|
|
Gerrit-Module: com.googlesource.gerrit.plugins.helloworld.HelloWorldModule
|
|
----
|
|
|
|
It is also possible to show some menu entries only if the user has a
|
|
certain capability:
|
|
|
|
[source,java]
|
|
----
|
|
public class MyTopMenuExtension implements TopMenu {
|
|
private final String pluginName;
|
|
private final Provider<CurrentUser> userProvider;
|
|
private final List<MenuEntry> menuEntries;
|
|
|
|
@Inject
|
|
public MyTopMenuExtension(@PluginName String pluginName,
|
|
Provider<CurrentUser> userProvider) {
|
|
this.pluginName = pluginName;
|
|
this.userProvider = userProvider;
|
|
menuEntries = new ArrayList<TopMenu.MenuEntry>();
|
|
|
|
// add menu entry that is only visible to users with a certain capability
|
|
if (canSeeMenuEntry()) {
|
|
menuEntries.add(new MenuEntry("Top Menu Entry", Collections
|
|
.singletonList(new MenuItem("Gerrit", "http://gerrit.googlecode.com/"))));
|
|
}
|
|
|
|
// add menu entry that is visible to all users (even anonymous users)
|
|
menuEntries.add(new MenuEntry("Top Menu Entry", Collections
|
|
.singletonList(new MenuItem("Documentation", "/plugins/myplugin/"))));
|
|
}
|
|
|
|
private boolean canSeeMenuEntry() {
|
|
if (userProvider.get().isIdentifiedUser()) {
|
|
CapabilityControl ctl = userProvider.get().getCapabilities();
|
|
return ctl.canPerform(pluginName + "-" + MyCapability.ID)
|
|
|| ctl.canAdministrateServer();
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public List<MenuEntry> getEntries() {
|
|
return menuEntries;
|
|
}
|
|
}
|
|
----
|
|
|
|
[[gwt_ui_extension]]
|
|
== GWT UI Extension
|
|
Plugins can extend the Gerrit UI with own GWT code.
|
|
|
|
The Maven archetype 'gerrit-plugin-gwt-archetype' can be used to
|
|
generate a GWT plugin skeleton. How to use the Maven plugin archetypes
|
|
is described in the link:#getting-started[Getting started] section.
|
|
|
|
The generated GWT plugin has a link:#top-menu-extensions[top menu] that
|
|
opens a GWT dialog box when the user clicks on it.
|
|
|
|
In addition to the Gerrit-Plugin API a GWT plugin depends on
|
|
`gerrit-plugin-gwtui`. This dependency must be specified in the
|
|
`pom.xml`:
|
|
|
|
[source,xml]
|
|
----
|
|
<dependency>
|
|
<groupId>com.google.gerrit</groupId>
|
|
<artifactId>gerrit-plugin-gwtui</artifactId>
|
|
<version>${Gerrit-ApiVersion}</version>
|
|
</dependency>
|
|
----
|
|
|
|
A GWT plugin must contain a GWT module file, e.g. `HelloPlugin.gwt.xml`,
|
|
that bundles together all the configuration settings of the GWT plugin:
|
|
|
|
[source,xml]
|
|
----
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<module rename-to="hello_gwt_plugin">
|
|
<!-- Inherit the core Web Toolkit stuff. -->
|
|
<inherits name="com.google.gwt.user.User"/>
|
|
<!-- Other module inherits -->
|
|
<inherits name="com.google.gerrit.Plugin"/>
|
|
<inherits name="com.google.gwt.http.HTTP"/>
|
|
<!-- Using GWT built-in themes adds a number of static -->
|
|
<!-- resources to the plugin. No theme inherits lines were -->
|
|
<!-- added in order to make this plugin as simple as possible -->
|
|
<!-- Specify the app entry point class. -->
|
|
<entry-point class="${package}.client.HelloPlugin"/>
|
|
<stylesheet src="hello.css"/>
|
|
</module>
|
|
----
|
|
|
|
The GWT module must inherit `com.google.gerrit.Plugin` and
|
|
`com.google.gwt.http.HTTP`.
|
|
|
|
To register the GWT module a `GwtPlugin` needs to be bound.
|
|
|
|
If no Guice modules are declared in the manifest, the GWT plugin may
|
|
use auto-registration by using the `@Listen` annotation:
|
|
|
|
[source,java]
|
|
----
|
|
@Listen
|
|
public class MyExtension extends GwtPlugin {
|
|
public MyExtension() {
|
|
super("hello_gwt_plugin");
|
|
}
|
|
}
|
|
----
|
|
|
|
Otherwise the binding must be done in an `HttpModule`:
|
|
|
|
[source,java]
|
|
----
|
|
public class HttpModule extends HttpPluginModule {
|
|
|
|
@Override
|
|
protected void configureServlets() {
|
|
DynamicSet.bind(binder(), WebUiPlugin.class)
|
|
.toInstance(new GwtPlugin("hello_gwt_plugin"));
|
|
}
|
|
}
|
|
----
|
|
|
|
The HTTP module above must be declared in the `pom.xml` for Maven
|
|
driven plugins:
|
|
|
|
[source,xml]
|
|
----
|
|
<manifestEntries>
|
|
<Gerrit-HttpModule>com.googlesource.gerrit.plugins.myplugin.HttpModule</Gerrit-HttpModule>
|
|
</manifestEntries>
|
|
----
|
|
|
|
The name that is provided to the `GwtPlugin` must match the GWT
|
|
module name compiled into the plugin. The name of the GWT module
|
|
can be explicitly set in the GWT module XML file by specifying
|
|
the `rename-to` attribute on the module. It is important that the
|
|
module name be unique across all plugins installed on the server,
|
|
as the module name determines the JavaScript namespace used by the
|
|
compiled plugin code.
|
|
|
|
[source,xml]
|
|
----
|
|
<module rename-to="hello_gwt_plugin">
|
|
----
|
|
|
|
The actual GWT code must be implemented in a class that extends
|
|
`com.google.gerrit.plugin.client.PluginEntryPoint`:
|
|
|
|
[source,java]
|
|
----
|
|
public class HelloPlugin extends PluginEntryPoint {
|
|
|
|
@Override
|
|
public void onPluginLoad() {
|
|
// Create the dialog box
|
|
final DialogBox dialogBox = new DialogBox();
|
|
|
|
// The content of the dialog comes from a User specified Preference
|
|
dialogBox.setText("Hello from GWT Gerrit UI plugin");
|
|
dialogBox.setAnimationEnabled(true);
|
|
Button closeButton = new Button("Close");
|
|
VerticalPanel dialogVPanel = new VerticalPanel();
|
|
dialogVPanel.setWidth("100%");
|
|
dialogVPanel.setHorizontalAlignment(VerticalPanel.ALIGN_CENTER);
|
|
dialogVPanel.add(closeButton);
|
|
|
|
closeButton.addClickHandler(new ClickHandler() {
|
|
public void onClick(ClickEvent event) {
|
|
dialogBox.hide();
|
|
}
|
|
});
|
|
|
|
// Set the contents of the Widget
|
|
dialogBox.setWidget(dialogVPanel);
|
|
|
|
RootPanel rootPanel = RootPanel.get(HelloMenu.MENU_ID);
|
|
rootPanel.getElement().removeAttribute("href");
|
|
rootPanel.addDomHandler(new ClickHandler() {
|
|
@Override
|
|
public void onClick(ClickEvent event) {
|
|
dialogBox.center();
|
|
dialogBox.show();
|
|
}
|
|
}, ClickEvent.getType());
|
|
}
|
|
}
|
|
----
|
|
|
|
This class must be set as entry point in the GWT module:
|
|
|
|
[source,xml]
|
|
----
|
|
<entry-point class="${package}.client.HelloPlugin"/>
|
|
----
|
|
|
|
In addition this class must be defined as module in the `pom.xml` for the
|
|
`gwt-maven-plugin` and the `webappDirectory` option of `gwt-maven-plugin`
|
|
must be set to `${project.build.directory}/classes/static`:
|
|
|
|
[source,xml]
|
|
----
|
|
<plugin>
|
|
<groupId>org.codehaus.mojo</groupId>
|
|
<artifactId>gwt-maven-plugin</artifactId>
|
|
<version>2.7.0</version>
|
|
<configuration>
|
|
<module>com.googlesource.gerrit.plugins.myplugin.HelloPlugin</module>
|
|
<disableClassMetadata>true</disableClassMetadata>
|
|
<disableCastChecking>true</disableCastChecking>
|
|
<webappDirectory>${project.build.directory}/classes/static</webappDirectory>
|
|
</configuration>
|
|
<executions>
|
|
<execution>
|
|
<goals>
|
|
<goal>compile</goal>
|
|
</goals>
|
|
</execution>
|
|
</executions>
|
|
</plugin>
|
|
----
|
|
|
|
To attach a GWT widget defined by the plugin to the Gerrit core UI
|
|
`com.google.gwt.user.client.ui.RootPanel` can be used to manipulate the
|
|
Gerrit core widgets:
|
|
|
|
[source,java]
|
|
----
|
|
RootPanel rootPanel = RootPanel.get(HelloMenu.MENU_ID);
|
|
rootPanel.getElement().removeAttribute("href");
|
|
rootPanel.addDomHandler(new ClickHandler() {
|
|
@Override
|
|
public void onClick(ClickEvent event) {
|
|
dialogBox.center();
|
|
dialogBox.show();
|
|
}
|
|
}, ClickEvent.getType());
|
|
----
|
|
|
|
GWT plugins can come with their own css file. This css file must have a
|
|
unique name and must be registered in the GWT module:
|
|
|
|
[source,xml]
|
|
----
|
|
<stylesheet src="hello.css"/>
|
|
----
|
|
|
|
If a GWT plugin wants to invoke the Gerrit REST API it can use
|
|
`com.google.gerrit.plugin.client.rpc.RestApi` to construct the URL
|
|
path and to trigger the REST calls.
|
|
|
|
Example for invoking a Gerrit core REST endpoint:
|
|
|
|
[source,java]
|
|
----
|
|
new RestApi("projects").id(projectName).view("description")
|
|
.put("new description", new AsyncCallback<JavaScriptObject>() {
|
|
|
|
@Override
|
|
public void onSuccess(JavaScriptObject result) {
|
|
// TODO
|
|
}
|
|
|
|
@Override
|
|
public void onFailure(Throwable caught) {
|
|
// never invoked
|
|
}
|
|
});
|
|
----
|
|
|
|
Example for invoking a REST endpoint defined by a plugin:
|
|
|
|
[source,java]
|
|
----
|
|
new RestApi("projects").id(projectName).view("myplugin", "myview")
|
|
.get(new AsyncCallback<JavaScriptObject>() {
|
|
|
|
@Override
|
|
public void onSuccess(JavaScriptObject result) {
|
|
// TODO
|
|
}
|
|
|
|
@Override
|
|
public void onFailure(Throwable caught) {
|
|
// never invoked
|
|
}
|
|
});
|
|
----
|
|
|
|
The `onFailure(Throwable)` of the provided callback is never invoked.
|
|
If an error occurs, it is shown in an error dialog.
|
|
|
|
In order to be able to do REST calls the GWT module must inherit
|
|
`com.google.gwt.json.JSON`:
|
|
|
|
[source,xml]
|
|
----
|
|
<inherits name="com.google.gwt.json.JSON"/>
|
|
----
|
|
|
|
[[screen]]
|
|
== Add Screen
|
|
A link:#gwt_ui_extension[GWT plugin] can link:#top-menu-extensions[add
|
|
a menu item] that opens a screen that is implemented by the plugin.
|
|
This way plugin screens can be fully integrated into the Gerrit UI.
|
|
|
|
Example menu item:
|
|
[source,java]
|
|
----
|
|
public class MyMenu implements TopMenu {
|
|
private final List<MenuEntry> menuEntries;
|
|
|
|
@Inject
|
|
public MyMenu(@PluginName String name) {
|
|
menuEntries = Lists.newArrayList();
|
|
menuEntries.add(new MenuEntry("My Menu", Collections.singletonList(
|
|
new MenuItem("My Screen", "#/x/" + name + "/my-screen", ""))));
|
|
}
|
|
|
|
@Override
|
|
public List<MenuEntry> getEntries() {
|
|
return menuEntries;
|
|
}
|
|
}
|
|
----
|
|
|
|
Example screen:
|
|
[source,java]
|
|
----
|
|
public class MyPlugin extends PluginEntryPoint {
|
|
@Override
|
|
public void onPluginLoad() {
|
|
Plugin.get().screen("my-screen", new Screen.EntryPoint() {
|
|
@Override
|
|
public void onLoad(Screen screen) {
|
|
screen.add(new InlineLabel("My Screen");
|
|
screen.show();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
----
|
|
|
|
[[settings-screen]]
|
|
== Plugin Settings Screen
|
|
|
|
If a plugin implements a screen for administrating its settings that is
|
|
available under "#/x/<plugin-name>/settings" it is automatically linked
|
|
from the plugin list screen.
|
|
|
|
[[http]]
|
|
== 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:
|
|
|
|
[source,java]
|
|
----
|
|
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");
|
|
}
|
|
}
|
|
----
|
|
|
|
The auto registration only works for standard servlet mappings like
|
|
`/foo` or `+/foo/*+`. Regex style bindings must use a Guice ServletModule
|
|
to register the HTTP servlets and declare it explicitly in the manifest
|
|
with the `Gerrit-HttpModule` attribute:
|
|
|
|
[source,java]
|
|
----
|
|
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
|
|
----
|
|
|
|
[[data-directory]]
|
|
== Data Directory
|
|
|
|
Plugins can request a data directory with a `@PluginData` Path (or File,
|
|
deprecated) dependency. A data directory will be created automatically
|
|
by the server in `$site_path/data/$plugin_name` and passed to the
|
|
plugin.
|
|
|
|
Plugins can use this to store any data they want.
|
|
|
|
[source,java]
|
|
----
|
|
@Inject
|
|
MyType(@PluginData java.nio.file.Path myDir) {
|
|
this.in = Files.newInputStream(myDir.resolve("my.config"));
|
|
}
|
|
----
|
|
|
|
[[secure-store]]
|
|
== SecureStore
|
|
|
|
SecureStore allows to change the way Gerrit stores sensitive data like
|
|
passwords.
|
|
|
|
In order to replace the default SecureStore (no-op) implementation,
|
|
a class that extends `com.google.gerrit.server.securestore.SecureStore`
|
|
needs to be provided (with dependencies) in a separate jar file. Then
|
|
link:pgm-SwitchSecureStore.html[SwitchSecureStore] must be run to
|
|
switch implementations.
|
|
|
|
The SecureStore implementation is instantiated using a Guice injector
|
|
which binds the `File` annotated with the `@SitePath` annotation.
|
|
This means that a SecureStore implementation class can get access to
|
|
the `site_path` like in the following example:
|
|
|
|
[source,java]
|
|
----
|
|
@Inject
|
|
MySecureStore(@SitePath java.io.File sitePath) {
|
|
// your code
|
|
}
|
|
----
|
|
|
|
No Guice bindings or modules are required. Gerrit will automatically
|
|
discover and bind the implementation.
|
|
|
|
[[download-commands]]
|
|
== Download Commands
|
|
|
|
Gerrit offers commands for downloading changes and cloning projects
|
|
using different download schemes (e.g. for downloading via different
|
|
network protocols). Plugins can contribute download schemes, download
|
|
commands and clone commands by implementing
|
|
`com.google.gerrit.extensions.config.DownloadScheme`,
|
|
`com.google.gerrit.extensions.config.DownloadCommand` and
|
|
`com.google.gerrit.extensions.config.CloneCommand`.
|
|
|
|
The download schemes, download commands and clone commands which are
|
|
used most often are provided by the Gerrit core plugin
|
|
`download-commands`.
|
|
|
|
[[included-in]]
|
|
== Included In
|
|
|
|
For merged changes the link:user-review-ui.html#included-in[Included In]
|
|
drop-down panel shows the branches and tags in which the change is
|
|
included.
|
|
|
|
Plugins can add additional systems in which the change can be included
|
|
by implementing `com.google.gerrit.extensions.config.ExternalIncludedIn`,
|
|
e.g. a plugin can provide a list of servers on which the change was
|
|
deployed.
|
|
|
|
[[links-to-external-tools]]
|
|
== Links To External Tools
|
|
|
|
Gerrit has extension points that enables development of a
|
|
light-weight plugin that links commits to external
|
|
tools (GitBlit, CGit, company specific resources etc).
|
|
|
|
PatchSetWebLinks will appear to the right of the commit-SHA1 in the UI.
|
|
|
|
[source, java]
|
|
----
|
|
import com.google.gerrit.extensions.annotations.Listen;
|
|
import com.google.gerrit.extensions.webui.PatchSetWebLink;;
|
|
import com.google.gerrit.extensions.webui.WebLinkTarget;
|
|
|
|
@Listen
|
|
public class MyWeblinkPlugin implements PatchSetWebLink {
|
|
|
|
private String name = "MyLink";
|
|
private String placeHolderUrlProjectCommit = "http://my.tool.com/project=%s/commit=%s";
|
|
private String imageUrl = "http://placehold.it/16x16.gif";
|
|
|
|
@Override
|
|
public WebLinkInfo getPatchSetWebLink(String projectName, String commit) {
|
|
return new WebLinkInfo(name,
|
|
imageUrl,
|
|
String.format(placeHolderUrlProjectCommit, project, commit),
|
|
WebLinkTarget.BLANK);
|
|
}
|
|
}
|
|
----
|
|
|
|
FileWebLinks will appear in the side-by-side diff screen on the right
|
|
side of the patch selection on each side.
|
|
|
|
DiffWebLinks will appear in the side-by-side and unified diff screen in
|
|
the header next to the navigation icons.
|
|
|
|
ProjectWebLinks will appear in the project list in the
|
|
`Repository Browser` column.
|
|
|
|
BranchWebLinks will appear in the branch list in the last column.
|
|
|
|
[[documentation]]
|
|
== 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 the `static/` directory in the JAR will be
|
|
available as `/plugins/helloworld/static/resource`. This prefix is
|
|
configurable by setting the `Gerrit-HttpStaticPrefix` attribute.
|
|
|
|
Documentation files under the `Documentation/` directory in the JAR
|
|
will be available as `/plugins/helloworld/Documentation/resource`. This
|
|
prefix is configurable by setting the `Gerrit-HttpDocumentationPrefix`
|
|
attribute.
|
|
|
|
Documentation may be written in the Markdown flavor
|
|
link:https://github.com/sirthias/pegdown[pegdown]
|
|
if the file name ends with `.md`. Gerrit will automatically convert
|
|
Markdown to HTML if accessed with extension `.html`.
|
|
|
|
[[macros]]
|
|
Within the Markdown documentation files macros can be used that allow
|
|
to write documentation with reasonably accurate examples that adjust
|
|
automatically based on the installation.
|
|
|
|
The following macros are supported:
|
|
|
|
[width="40%",options="header"]
|
|
|===================================================
|
|
|Macro | Replacement
|
|
|@PLUGIN@ | name of the plugin
|
|
|@URL@ | Gerrit Web URL
|
|
|@SSH_HOST@ | SSH Host
|
|
|@SSH_PORT@ | SSH Port
|
|
|===================================================
|
|
|
|
The macros will be replaced when the documentation files are rendered
|
|
from Markdown to HTML.
|
|
|
|
Macros that start with `\` such as `\@KEEP@` will render as `@KEEP@`
|
|
even if there is an expansion for `KEEP` in the future.
|
|
|
|
[[auto-index]]
|
|
=== 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 is named `about.md` or `about.html`, its
|
|
content will be inserted in an 'About' section at the top of the
|
|
auto-generated index page. If both `about.md` and `about.html`
|
|
exist, only the first discovered file will be used.
|
|
|
|
If a discovered file name beings with `cmd-` it will be clustered
|
|
into a 'Commands' section of the generated index page.
|
|
|
|
If a discovered file name beings with `servlet-` it will be clustered
|
|
into a 'Servlets' section of the generated index page.
|
|
|
|
If a discovered file name beings with `rest-api-` it will be clustered
|
|
into a 'REST APIs' 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]]
|
|
== Deployment
|
|
|
|
Compiled plugins and extensions can be deployed to a running Gerrit
|
|
server using the link:cmd-plugin-install.html[plugin install] command.
|
|
|
|
Web UI plugins distributed as single `.js` file can be deployed
|
|
without the overhead of JAR packaging, for more information refer to
|
|
link:cmd-plugin-install.html[plugin install] command.
|
|
|
|
Plugins can also be copied directly into the server's
|
|
directory at `$site_path/plugins/$name.(jar|js)`. The name of
|
|
the JAR file, minus the `.jar` or `.js` 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].
|
|
|
|
For disabling plugins the link:cmd-plugin-remove.html[plugin remove]
|
|
command can be used.
|
|
|
|
Disabled plugins can be re-enabled using the
|
|
link:cmd-plugin-enable.html[plugin enable] command.
|
|
|
|
== Known issues and bugs
|
|
|
|
=== Error handling in UI when using the REST API
|
|
|
|
When a plugin invokes a REST endpoint in the UI, it provides an
|
|
`AsyncCallback` to handle the result. At the moment the
|
|
`onFailure(Throwable)` of the callback is never invoked, even if there
|
|
is an error. Errors are always handled by the Gerrit core UI which
|
|
shows the error dialog. This means currently plugins cannot do any
|
|
error handling and e.g. ignore expected errors.
|
|
|
|
In the following example the REST endpoint would return '404 Not Found'
|
|
if there is no HTTP password and the Gerrit core UI would display an
|
|
error dialog for this. However having no HTTP password is not an error
|
|
and the plugin may like to handle this case.
|
|
|
|
[source,java]
|
|
----
|
|
new RestApi("accounts").id("self").view("password.http")
|
|
.get(new AsyncCallback<NativeString>() {
|
|
|
|
@Override
|
|
public void onSuccess(NativeString httpPassword) {
|
|
// TODO
|
|
}
|
|
|
|
@Override
|
|
public void onFailure(Throwable caught) {
|
|
// never invoked
|
|
}
|
|
});
|
|
----
|
|
|
|
|
|
== SEE ALSO
|
|
|
|
* link:js-api.html[JavaScript API]
|
|
* link:dev-rest-api.html[REST API Developers' Notes]
|
|
|
|
GERRIT
|
|
------
|
|
Part of link:index.html[Gerrit Code Review]
|
|
|
|
SEARCHBOX
|
|
---------
|