e178d5ec4c
* stable-2.14: Add documentation for the change report formatter interface CommitValidators: Fix repeated "Change-Id" in error message ChangeReportFormatter: Convert Input to use AutoValue.Builder Introduce Change Report formatter extension point Update elasticsearch-rest-client to 6.3.1 user-review-ui: Fix typo Change-Id: I93b87a5ca9b9ea9f272122c8661780673e533f67
2747 lines
82 KiB
Plaintext
2747 lines
82 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.
|
|
|
|
For PolyGerrit-specific plugin development, consult with
|
|
link:pg-plugin-dev.html[PolyGerrit Plugin Development] guide.
|
|
|
|
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.
|
|
|
|
Plugins may require a specific major.minor.patch server version
|
|
and may need rebuild and revalidation across different
|
|
patch levels. A different patch level may only add new
|
|
API interfaces and never change or extend existing ones.
|
|
|
|
[[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 clone the sample
|
|
plugin:
|
|
|
|
----
|
|
$ git clone https://gerrit.googlesource.com/plugins/cookbook-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.
|
|
|
|
When starting from this example one should take care to adapt the
|
|
`Gerrit-ApiVersion` in the `BUILD` to the version of Gerrit for which
|
|
the plugin is developed.
|
|
|
|
[[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.
|
|
----
|
|
|
|
=== 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 Bazel driven plugins, the following line must be included in the BUILD
|
|
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;
|
|
|
|
@Inject
|
|
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 without user visibility restrictions. 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.common.UserScopedEventListener`:
|
|
+
|
|
Allows to listen to events visible to the specified user. 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
|
|
|
|
* `com.google.gerrit.server.extensions.events.ChangeIndexedListener`:
|
|
+
|
|
Update of the change secondary index
|
|
|
|
* `com.google.gerrit.server.extensions.events.AccountIndexedListener`:
|
|
+
|
|
Update of the account secondary index
|
|
|
|
* `com.google.gerrit.server.extensions.events.GroupIndexedListener`:
|
|
+
|
|
Update of the group secondary index
|
|
|
|
* `com.google.gerrit.httpd.WebLoginListener`:
|
|
+
|
|
User login or logout interactively on the Web user interface.
|
|
|
|
The event listener is under the Gerrit http package to automatically
|
|
inherit the javax.servlet.http dependencies and allowing to influence
|
|
the login or logout flow with additional redirections.
|
|
|
|
[[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 `EventDispatcher` interface, passing an instance of
|
|
its own custom event class derived from
|
|
`com.google.gerrit.server.events.Event`.
|
|
|
|
[source,java]
|
|
----
|
|
import com.google.gerrit.common.EventDispatcher;
|
|
import com.google.gerrit.extensions.registration.DynamicItem;
|
|
import com.google.gwtorm.server.OrmException;
|
|
import com.google.inject.Inject;
|
|
|
|
class MyPlugin {
|
|
private final DynamicItem<EventDispatcher> eventDispatcher;
|
|
|
|
@Inject
|
|
myPlugin(DynamicItem<EventDispatcher> eventDispatcher) {
|
|
this.eventDispatcher = eventDispatcher;
|
|
}
|
|
|
|
private void postEvent(MyPluginEvent event) {
|
|
try {
|
|
eventDispatcher.get().postEvent(event);
|
|
} catch (OrmException e) {
|
|
// error handling
|
|
}
|
|
}
|
|
}
|
|
----
|
|
|
|
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.
|
|
Deserializing events with the
|
|
`com.google.gerrit.server.events.EventDeserializer` class requires
|
|
that the event be registered in EventTypes.
|
|
|
|
== Modifying the Stream Event Flow
|
|
|
|
It is possible to modify the stream event flow from plugins by registering
|
|
an `com.google.gerrit.server.events.EventDispatcher`. A plugin may register
|
|
a Dispatcher class to replace the internal Dispatcher. EventDispatcher is
|
|
a DynamicItem, so Gerrit may only have one copy.
|
|
|
|
[[validation]]
|
|
== Validation Listeners
|
|
|
|
Certain operations in Gerrit can be validated by plugins by
|
|
implementing the corresponding link:config-validation.html[listeners].
|
|
|
|
[[change-message-modifier]]
|
|
== Change Message Modifier
|
|
|
|
`com.google.gerrit.server.git.ChangeMessageModifier`:
|
|
plugins implementing this can modify commit message of the change being
|
|
submitted by Rebase Always and Cherry Pick submit strategies as well as
|
|
change being queried with COMMIT_FOOTERS option.
|
|
|
|
[[receive-pack]]
|
|
== Receive Pack Initializers
|
|
|
|
Plugins may provide ReceivePackInitializer instances, 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.
|
|
|
|
The interactions with the core Gerrit ReceivePack initialization and
|
|
between ReceivePackInitializers can be complex. Please read the
|
|
ReceivePack Javadoc and Gerrit AsyncReceiveCommits implementation
|
|
carefully.
|
|
|
|
[[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.
|
|
|
|
[[upload-pack]]
|
|
== Upload Pack Initializers
|
|
|
|
Plugins may provide UploadPackInitializer instances, which will be
|
|
invoked by Gerrit just before a UploadPack instance will be used.
|
|
Usually, plugins will make use of the setXXX methods on the UploadPack
|
|
to set additional properties on it.
|
|
|
|
The interactions with the core Gerrit UploadPack initialization and
|
|
between UploadPackInitializers can be complex. Please read the
|
|
UploadPack Javadoc and Gerrit Upload/UploadFactory implementations
|
|
carefully.
|
|
|
|
[[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.
|
|
|
|
[[post-upload-hook]]
|
|
== Post Upload-Pack Hooks
|
|
|
|
Plugins may register PostUploadHook instances in order to get notified after
|
|
JGit is done uploading a pack.
|
|
|
|
[[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
|
|
----
|
|
|
|
[[search_operators]]
|
|
== Search Operators
|
|
|
|
Plugins can define new search operators to extend change searching by
|
|
implementing the `ChangeQueryBuilder.ChangeOperatorFactory` interface
|
|
and registering it to an operator name in the plugin module's
|
|
`configure()` method. The search operator name is defined during
|
|
registration via the DynamicMap annotation mechanism. The plugin
|
|
name will get appended to the annotated name, with an underscore
|
|
in between, leading to the final operator name. An example
|
|
registration looks like this:
|
|
|
|
bind(ChangeOperatorFactory.class)
|
|
.annotatedWith(Exports.named("sample"))
|
|
.to(SampleOperator.class);
|
|
|
|
If this is registered in the `myplugin` plugin, then the resulting
|
|
operator will be named `sample_myplugin`.
|
|
|
|
The search operator itself is implemented by ensuring that the
|
|
`create()` method of the class implementing the
|
|
`ChangeQueryBuilder.ChangeOperatorFactory` interface returns a
|
|
`Predicate<ChangeData>`. Here is a sample operator factory
|
|
definition which creates a `MyPredicate`:
|
|
|
|
[source,java]
|
|
----
|
|
@Singleton
|
|
public class SampleOperator
|
|
implements ChangeQueryBuilder.ChangeOperatorFactory {
|
|
public static class MyPredicate extends OperatorChangePredicate<ChangeData> {
|
|
...
|
|
}
|
|
|
|
@Override
|
|
public Predicate<ChangeData> create(ChangeQueryBuilder builder, String value)
|
|
throws QueryParseException {
|
|
return new MyPredicate(value);
|
|
}
|
|
}
|
|
----
|
|
|
|
[[search_operands]]
|
|
=== Search Operands ===
|
|
|
|
Plugins can define new search operands to extend change searching.
|
|
Plugin methods implementing search operands (returning a
|
|
`Predicate<ChangeData>`), must be defined on a class implementing
|
|
one of the `ChangeQueryBuilder.ChangeOperandsFactory` interfaces
|
|
(.e.g., ChangeQueryBuilder.ChangeHasOperandFactory). The specific
|
|
`ChangeOperandFactory` class must also be bound to the `DynamicSet` from
|
|
a module's `configure()` method in the plugin.
|
|
|
|
The new operand, when used in a search would appear as:
|
|
operatorName:operandName_pluginName
|
|
|
|
A sample `ChangeHasOperandFactory` class implementing, and registering, a
|
|
new `has:sample_pluginName` operand is shown below:
|
|
|
|
====
|
|
@Singleton
|
|
public class SampleHasOperand implements ChangeHasOperandFactory {
|
|
public static class Module extends AbstractModule {
|
|
@Override
|
|
protected void configure() {
|
|
bind(ChangeHasOperandFactory.class)
|
|
.annotatedWith(Exports.named("sample")
|
|
.to(SampleHasOperand.class);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Predicate<ChangeData> create(ChangeQueryBuilder builder)
|
|
throws QueryParseException {
|
|
return new HasSamplePredicate();
|
|
}
|
|
====
|
|
|
|
[[command_options]]
|
|
=== Command Options ===
|
|
|
|
Plugins can provide additional options for each of the gerrit ssh and the
|
|
REST API commands by implementing the DynamicBean interface and registering
|
|
it to a command class name in the plugin module's `configure()` method. The
|
|
plugin's name will be prepended to the name of each @Option annotation found
|
|
on the DynamicBean object provided by the plugin. The example below shows a
|
|
plugin that adds an option to log a value from the gerrit 'ban-commits'
|
|
ssh command.
|
|
|
|
[source, java]
|
|
----
|
|
public class SshModule extends AbstractModule {
|
|
private static final Logger log = LoggerFactory.getLogger(SshModule.class);
|
|
|
|
@Override
|
|
protected void configure() {
|
|
bind(DynamicOptions.DynamicBean.class)
|
|
.annotatedWith(Exports.named(
|
|
com.google.gerrit.sshd.commands.BanCommitCommand.class))
|
|
.to(BanOptions.class);
|
|
}
|
|
|
|
public static class BanOptions implements DynamicOptions.DynamicBean {
|
|
@Option(name = "--log", aliases = { "-l" }, usage = "Say Hello in the Log")
|
|
private void parse(String arg) {
|
|
log.error("Say Hello in the Log " + arg);
|
|
}
|
|
}
|
|
----
|
|
|
|
[[query_attributes]]
|
|
=== Query Attributes ===
|
|
|
|
Plugins can provide additional attributes to be returned in Gerrit queries by
|
|
implementing the ChangeAttributeFactory interface and registering it to the
|
|
ChangeQueryProcessor.ChangeAttributeFactory class in the plugin module's
|
|
'configure()' method. The new attribute(s) will be output under a "plugin"
|
|
attribute in the change query output.
|
|
|
|
The example below shows a plugin that adds two attributes ('exampleName' and
|
|
'changeValue'), to the change query output.
|
|
|
|
[source, java]
|
|
----
|
|
public class Module extends AbstractModule {
|
|
@Override
|
|
protected void configure() {
|
|
bind(ChangeAttributeFactory.class)
|
|
.annotatedWith(Exports.named("example"))
|
|
.to(AttributeFactory.class);
|
|
}
|
|
}
|
|
|
|
public class AttributeFactory implements ChangeAttributeFactory {
|
|
|
|
public class PluginAttribute extends PluginDefinedInfo {
|
|
public String exampleName;
|
|
public String changeValue;
|
|
|
|
public PluginAttribute(ChangeData c) {
|
|
this.exampleName = "Attribute Example";
|
|
this.changeValue = Integer.toString(c.getId().get());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public PluginDefinedInfo create(ChangeData c, ChangeQueryProcessor qp, String plugin) {
|
|
return new PluginAttribute(c);
|
|
}
|
|
}
|
|
----
|
|
|
|
Example
|
|
----
|
|
|
|
ssh -p 29418 localhost gerrit query "change:1" --format json
|
|
|
|
Output:
|
|
|
|
{
|
|
"url" : "http://localhost:8080/1",
|
|
"plugins" : [
|
|
{
|
|
"name" : "myplugin-name",
|
|
"exampleName" : "Attribute Example",
|
|
"changeValue" : "1"
|
|
}
|
|
],
|
|
...
|
|
}
|
|
----
|
|
|
|
[[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
|
|
----
|
|
|
|
Plugins that have sensitive configuration settings can store those settings in
|
|
an own secure configuration file. The plugin's secure configuration file must be
|
|
named after the plugin and must be located in the `etc` folder of the review
|
|
site. For example a secure configuration file for a `default-reviewer` plugin
|
|
could look like this:
|
|
|
|
.$site_path/etc/default-reviewer.secure.config
|
|
----
|
|
[auth]
|
|
password = secret
|
|
----
|
|
|
|
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");
|
|
String password = cfg.getGlobalPluginConfig("default-reviewer")
|
|
.getString("auth", null, "password");
|
|
----
|
|
|
|
|
|
[[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.
|
|
|
|
[[configuring-groups]]
|
|
=== Referencing groups in `project.config`
|
|
|
|
Plugins can refer to groups so that when they are renamed, the project
|
|
config will also be updated in this section. The proper format to use is
|
|
the same as for any other group reference in the `project.config`, as shown below.
|
|
|
|
----
|
|
group group_name
|
|
----
|
|
|
|
The file `groups` must also contains the mapping of the group name and its UUID,
|
|
refer to link:config-project-config.html#file-groups[file groups]
|
|
|
|
[[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
|
|
|
|
[[panels]]
|
|
=== Panels
|
|
|
|
GWT plugins can contribute panels to Gerrit screens.
|
|
|
|
Gerrit screens define extension points where plugins can add GWT
|
|
panels with custom controls:
|
|
|
|
* Change Screen:
|
|
** `GerritUiExtensionPoint.CHANGE_SCREEN_HEADER`:
|
|
+
|
|
Panel will be shown in the header bar to the right of the change
|
|
status.
|
|
|
|
** `GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_BUTTONS`:
|
|
+
|
|
Panel will be shown in the header bar on the right side of the buttons.
|
|
|
|
** `GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_POP_DOWNS`:
|
|
+
|
|
Panel will be shown in the header bar on the right side of the pop down
|
|
buttons.
|
|
|
|
** `GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK`:
|
|
+
|
|
Panel will be shown below the commit info block.
|
|
|
|
** `GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK`:
|
|
+
|
|
Panel will be shown below the change info block.
|
|
|
|
** `GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_RELATED_INFO_BLOCK`:
|
|
+
|
|
Panel will be shown below the related info block.
|
|
|
|
** `GerritUiExtensionPoint.CHANGE_SCREEN_HISTORY_RIGHT_OF_BUTTONS`:
|
|
+
|
|
Panel will be shown in the history bar on the right side of the buttons.
|
|
|
|
** The following parameters are provided:
|
|
*** `GerritUiExtensionPoint.Key.CHANGE_INFO`:
|
|
+
|
|
The link:rest-api-changes.html#change-info[ChangeInfo] entity for the
|
|
current change.
|
|
+
|
|
The link:rest-api-changes.html#revision-info[RevisionInfo] entity for
|
|
the current patch set.
|
|
|
|
* Project Info Screen:
|
|
** `GerritUiExtensionPoint.PROJECT_INFO_SCREEN_TOP`:
|
|
+
|
|
Panel will be shown at the top of the screen.
|
|
|
|
** `GerritUiExtensionPoint.PROJECT_INFO_SCREEN_BOTTOM`:
|
|
+
|
|
Panel will be shown at the bottom of the screen.
|
|
|
|
** The following parameters are provided:
|
|
*** `GerritUiExtensionPoint.Key.PROJECT_NAME`:
|
|
+
|
|
The name of the project.
|
|
|
|
* User Password Screen:
|
|
** `GerritUiExtensionPoint.PASSWORD_SCREEN_BOTTOM`:
|
|
+
|
|
Panel will be shown at the bottom of the screen.
|
|
|
|
** The following parameters are provided:
|
|
*** `GerritUiExtensionPoint.Key.ACCOUNT_INFO`:
|
|
+
|
|
The link:rest-api-accounts.html#account-info[AccountInfo] entity for
|
|
the current user.
|
|
|
|
* User Preferences Screen:
|
|
** `GerritUiExtensionPoint.PREFERENCES_SCREEN_BOTTOM`:
|
|
+
|
|
Panel will be shown at the bottom of the screen.
|
|
|
|
** The following parameters are provided:
|
|
*** `GerritUiExtensionPoint.Key.ACCOUNT_INFO`:
|
|
+
|
|
The link:rest-api-accounts.html#account-info[AccountInfo] entity for
|
|
the current user.
|
|
|
|
* User Profile Screen:
|
|
** `GerritUiExtensionPoint.PROFILE_SCREEN_BOTTOM`:
|
|
+
|
|
Panel will be shown at the bottom of the screen below the grid with the
|
|
profile data.
|
|
|
|
** The following parameters are provided:
|
|
*** `GerritUiExtensionPoint.Key.ACCOUNT_INFO`:
|
|
+
|
|
The link:rest-api-accounts.html#account-info[AccountInfo] entity for
|
|
the current user.
|
|
|
|
Example panel:
|
|
[source,java]
|
|
----
|
|
public class MyPlugin extends PluginEntryPoint {
|
|
@Override
|
|
public void onPluginLoad() {
|
|
Plugin.get().panel(GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
|
|
"my_panel_name",
|
|
new Panel.EntryPoint() {
|
|
@Override
|
|
public void onLoad(Panel panel) {
|
|
panel.setWidget(new InlineLabel("My Panel for change "
|
|
+ panel.getInt(GerritUiExtensionPoint.Key.CHANGE_ID, -1));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
----
|
|
|
|
Change Screen panel ordering may be specified in the
|
|
project config. Values may be either "plugin name" or
|
|
"plugin name"."panel name".
|
|
Panels not specified in the config will be added
|
|
to the end in load order. Panels specified in the config that
|
|
are not found will be ignored.
|
|
|
|
Example config:
|
|
----
|
|
[extension-panels "CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK"]
|
|
panel = helloworld.change_id
|
|
panel = myotherplugin
|
|
panel = myplugin.my_panel_name
|
|
----
|
|
|
|
|
|
|
|
[[actions]]
|
|
=== Actions
|
|
|
|
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();
|
|
try (BatchUpdate bu = batchUpdateFactory.create(
|
|
db, project.getNameKey(), user, TimeUtil.nowTs())) {
|
|
bu.addOp(change.getId(), new BatchUpdate.Op() {
|
|
@Override
|
|
public boolean updateChange(ChangeContext ctx) {
|
|
return true;
|
|
}
|
|
});
|
|
bu.execute();
|
|
}
|
|
[...]
|
|
}
|
|
----
|
|
|
|
`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 `BUILD` configuration file for Bazel 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 `BUILD` configuration file for Bazel 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 prerequisites 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}' \
|
|
--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);
|
|
});
|
|
----
|
|
|
|
|
|
[[action-visitor]]
|
|
=== Action Visitors
|
|
|
|
In addition to providing new actions, plugins can have fine-grained control
|
|
over the link:rest-api-changes.html#action-info[ActionInfo] map, modifying or
|
|
removing existing actions, including those contributed by core.
|
|
|
|
Visitors are provided the link:rest-api-changes.html#action-info[ActionInfo],
|
|
which is mutable, along with copies of the
|
|
link:rest-api-changes.html#change-info[ChangeInfo] and
|
|
link:rest-api-changes.html#revision-info[RevisionInfo]. They can modify the
|
|
action, or return `false` to exclude it from the resulting map.
|
|
|
|
These operations only affect the action buttons that are displayed in the UI;
|
|
the underlying REST API endpoints are not affected. Multiple plugins may
|
|
implement the visitor interface, but the order in which they are run is
|
|
undefined.
|
|
|
|
For example, to exclude "Cherry-Pick" only from certain projects, and rename
|
|
"Abandon":
|
|
|
|
[source,java]
|
|
----
|
|
public class MyActionVisitor implements ActionVisitor {
|
|
@Override
|
|
public boolean visit(String name, ActionInfo actionInfo,
|
|
ChangeInfo changeInfo) {
|
|
if (name.equals("abandon")) {
|
|
actionInfo.label = "Drop";
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean visit(String name, ActionInfo actionInfo,
|
|
ChangeInfo changeInfo, RevisionInfo revisionInfo) {
|
|
if (project.startsWith("some-team/") && name.equals("cherrypick")) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
----
|
|
|
|
|
|
[[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.
|
|
|
|
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 = new ArrayList<>();
|
|
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();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
----
|
|
|
|
[[user-settings-screen]]
|
|
== Add User Settings Screen
|
|
|
|
A link:#gwt_ui_extension[GWT plugin] can implement a user settings
|
|
screen that is integrated into the Gerrit user settings menu.
|
|
|
|
Example settings screen:
|
|
[source,java]
|
|
----
|
|
public class MyPlugin extends PluginEntryPoint {
|
|
@Override
|
|
public void onPluginLoad() {
|
|
Plugin.get().settingsScreen("my-preferences", "My Preferences",
|
|
new Screen.EntryPoint() {
|
|
@Override
|
|
public void onLoad(Screen screen) {
|
|
screen.setPageTitle("Settings");
|
|
screen.add(new InlineLabel("My Preferences"));
|
|
screen.show();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
----
|
|
|
|
By defining an link:config-gerrit.html#urlAlias[urlAlias] Gerrit
|
|
administrators can map plugin screens into the Gerrit URL namespace or
|
|
even replace Gerrit screens by plugin screens.
|
|
|
|
Plugins may also programatically add URL aliases in the preferences of
|
|
of a user. This way certain screens can be replaced for certain users.
|
|
E.g. the plugin may offer a user preferences setting for choosing a
|
|
screen that then sets/unsets a URL alias for the user.
|
|
|
|
[[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.
|
|
|
|
[[accountcreation]]
|
|
== Account Creation
|
|
|
|
Plugins can hook into the
|
|
link:rest-api-accounts.html#create-account[account creation] REST API and
|
|
inject additional external identifiers for an account that represents a user
|
|
in some external user store. For that, an implementation of the extension
|
|
point `com.google.gerrit.server.api.accounts.AccountExternalIdCreator`
|
|
must be registered.
|
|
|
|
[source,java]
|
|
----
|
|
class MyExternalIdCreator implements AccountExternalIdCreator {
|
|
@Override
|
|
public List<AccountExternalId> create(Account.Id id, String username,
|
|
String email) {
|
|
// your code
|
|
}
|
|
}
|
|
|
|
bind(AccountExternalIdCreator.class)
|
|
.annotatedWith(UniqueAnnotations.create())
|
|
.to(MyExternalIdCreator.class);
|
|
}
|
|
----
|
|
|
|
[[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.
|
|
|
|
[[change-report-formatting]]
|
|
== Change Report Formatting
|
|
|
|
When a change is pushed for review from the command line, Gerrit reports
|
|
the change(s) received with their URL and subject.
|
|
|
|
By implementing the
|
|
`com.google.gerrit.server.git.ChangeReportFormatter` interface, a plugin
|
|
may change the formatting of the report.
|
|
|
|
[[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);
|
|
}
|
|
}
|
|
----
|
|
|
|
ParentWebLinks will appear to the right of the SHA1 of the parent
|
|
revisions in the UI. The implementation should in most use cases direct
|
|
to the same external service as PatchSetWebLink; it is provided as a
|
|
separate interface because not all users want to have links for the
|
|
parent revisions.
|
|
|
|
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.
|
|
|
|
FileHistoryWebLinks will appear on the access rights screen.
|
|
|
|
TagWebLinks will appear in the tag list in the last column.
|
|
|
|
If a `get*WebLink` implementation returns `null`, the link will be omitted. This
|
|
allows the plugin to selectively "enable" itself on a per-project/branch/file
|
|
basis.
|
|
|
|
[[lfs-extension]]
|
|
== LFS Storage Plugins
|
|
|
|
Gerrit provides an extension point that enables development of
|
|
link:https://github.com/github/git-lfs/blob/master/docs/api/v1/http-v1-batch.md[
|
|
LFS (Large File Storage)] storage plugins. Gerrit core exposes the default LFS
|
|
protocol endpoint `<project-name>/info/lfs/objects/batch` and forwards the requests
|
|
to the configured link:config-gerrit.html#lfs[lfs.plugin] plugin which implements
|
|
the LFS protocol. By exposing the default LFS endpoint, the git-lfs client can be
|
|
used without any configuration.
|
|
|
|
[source, java]
|
|
----
|
|
/** Provide an LFS protocol implementation */
|
|
import org.eclipse.jgit.lfs.server.LargeFileRepository;
|
|
import org.eclipse.jgit.lfs.server.LfsProtocolServlet;
|
|
|
|
@Singleton
|
|
public class LfsApiServlet extends LfsProtocolServlet {
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
private final S3LargeFileRepository repository;
|
|
|
|
@Inject
|
|
LfsApiServlet(S3LargeFileRepository repository) {
|
|
this.repository = repository;
|
|
}
|
|
|
|
@Override
|
|
protected LargeFileRepository getLargeFileRepository() {
|
|
return repository;
|
|
}
|
|
}
|
|
|
|
/** Register the LfsApiServlet to listen on the default LFS protocol endpoint */
|
|
import static com.google.gerrit.httpd.plugins.LfsPluginServlet.URL_REGEX;
|
|
|
|
import com.google.gerrit.httpd.plugins.HttpPluginModule;
|
|
|
|
public class HttpModule extends HttpPluginModule {
|
|
|
|
@Override
|
|
protected void configureServlets() {
|
|
serveRegex(URL_REGEX).with(LfsApiServlet.class);
|
|
}
|
|
}
|
|
|
|
/** Provide an implementation of the LargeFileRepository */
|
|
import org.eclipse.jgit.lfs.server.s3.S3Repository;
|
|
|
|
public class S3LargeFileRepository extends S3Repository {
|
|
...
|
|
}
|
|
----
|
|
|
|
[[metrics]]
|
|
== Metrics
|
|
|
|
=== Metrics Reporting
|
|
|
|
To send Gerrit's metrics data to an external reporting backend, a plugin can
|
|
get a `MetricRegistry` injected and register an instance of a class that
|
|
implements the `Reporter` interface from link:http://metrics.dropwizard.io/[
|
|
DropWizard Metrics].
|
|
|
|
Metric reporting plugin implementations are provided for
|
|
link:https://gerrit.googlesource.com/plugins/metrics-reporter-jmx/[JMX],
|
|
link:https://gerrit.googlesource.com/plugins/metrics-reporter-elasticsearch/[Elastic Search],
|
|
and link:https://gerrit.googlesource.com/plugins/metrics-reporter-graphite/[Graphite].
|
|
|
|
There is also a working example of reporting metrics to the console in the
|
|
link:https://gerrit.googlesource.com/plugins/cookbook-plugin/+/master/src/main/java/com/googlesource/gerrit/plugins/cookbook/ConsoleMetricReporter.java[
|
|
cookbook plugin].
|
|
|
|
=== Providing own metrics
|
|
|
|
Plugins may provide metrics to be dispatched to external reporting services by
|
|
getting a `MetricMaker` injected and creating instances of specific types of
|
|
metric:
|
|
|
|
* Counter
|
|
+
|
|
Metric whose value increments during the life of the process.
|
|
|
|
* Timer
|
|
+
|
|
Metric recording time spent on an operation.
|
|
|
|
* Histogram
|
|
+
|
|
Metric recording statistical distribution (rate) of values.
|
|
|
|
Note that metrics cannot be recorded from plugin init steps that
|
|
are run during site initialization.
|
|
|
|
By default, plugin metrics are recorded under
|
|
`plugins/${plugin-name}/${metric-name}`. This can be changed by
|
|
setting `plugins.${plugin-name}.metricsPrefix` in the `gerrit.config`
|
|
file. For example:
|
|
|
|
----
|
|
[plugin "my-plugin"]
|
|
metricsPrefix = my-metrics
|
|
----
|
|
|
|
will cause the metrics to be recorded under `my-metrics/${metric-name}`.
|
|
|
|
See the replication metrics in the
|
|
link:https://gerrit.googlesource.com/plugins/replication/+/master/src/main/java/com/googlesource/gerrit/plugins/replication/ReplicationMetrics.java[
|
|
replication plugin] for an example of usage.
|
|
|
|
[[account-patch-review-store]]
|
|
== AccountPatchReviewStore
|
|
|
|
The AccountPatchReviewStore is used to store reviewed flags on changes.
|
|
A reviewed flag is a tuple of (patch set ID, file, account ID) and
|
|
records whether the user has reviewed a file in a patch set. Each user
|
|
can easily have thousands of reviewed flags and the number of reviewed
|
|
flags is growing without bound. The store must be able handle this data
|
|
volume efficiently.
|
|
|
|
Gerrit implements this extension point, but plugins may bind another
|
|
implementation, e.g. one that supports multi-master.
|
|
|
|
----
|
|
DynamicItem.bind(binder(), AccountPatchReviewStore.class)
|
|
.to(MultiMasterAccountPatchReviewStore.class);
|
|
|
|
...
|
|
|
|
public class MultiMasterAccountPatchReviewStore
|
|
implements AccountPatchReviewStore {
|
|
...
|
|
}
|
|
----
|
|
|
|
|
|
[[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 a single `.js` file (or `.html` file for
|
|
Polygerrit) 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|html)`. For Web UI plugins, the name
|
|
of the file, minus the `.js` or `.html` extension, will be used as the
|
|
plugin name. For JAR plugins, the value of the `Gerrit-PluginName`
|
|
manifest attribute will be used, if provided, otherwise the name of
|
|
the file, minus the `.jar` extension, will be used.
|
|
|
|
For Web UI plugins, the plugin version is derived from the filename.
|
|
If the filename contains one or more hyphens, the version is taken
|
|
from the portion following the last hyphen. For example if the plugin
|
|
filename is `my-plugin-1.0.js` the version will be `1.0`. For JAR
|
|
plugins, the version is taken from the `Version` attribute in the
|
|
manifest.
|
|
|
|
Unless disabled, servers periodically scan the `$site_path/plugins`
|
|
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 the user has no username and the Gerrit core UI would
|
|
display an error dialog for this. However having no username is
|
|
not an error and the plugin may like to handle this case.
|
|
|
|
[source,java]
|
|
----
|
|
new RestApi("accounts").id("self").view("username")
|
|
.get(new AsyncCallback<NativeString>() {
|
|
|
|
@Override
|
|
public void onSuccess(NativeString username) {
|
|
// TODO
|
|
}
|
|
|
|
@Override
|
|
public void onFailure(Throwable caught) {
|
|
// never invoked
|
|
}
|
|
});
|
|
----
|
|
|
|
|
|
[[reviewer-suggestion]]
|
|
== Reviewer Suggestion Plugins
|
|
|
|
Gerrit provides an extension point that enables Plugins to rank
|
|
the list of reviewer suggestion a user receives upon clicking "Add Reviewer" on
|
|
the change screen.
|
|
|
|
Gerrit supports both a default suggestion that appears when the user has not yet
|
|
typed anything and a filtered suggestion that is shown as the user starts
|
|
typing.
|
|
|
|
Plugins receive a candidate list and can return a `Set` of suggested reviewers
|
|
containing the `Account.Id` and a score for each reviewer. The candidate list is
|
|
non-binding and plugins can choose to return reviewers not initially contained in
|
|
the candidate list.
|
|
|
|
Server administrators can configure the overall weight of each plugin by setting
|
|
the `addreviewer.pluginName-exportName.weight` value in `gerrit.config`.
|
|
|
|
[source, java]
|
|
----
|
|
import com.google.gerrit.common.Nullable;
|
|
import com.google.gerrit.extensions.annotations.ExtensionPoint;
|
|
import com.google.gerrit.reviewdb.client.Account;
|
|
import com.google.gerrit.reviewdb.client.Change;
|
|
import com.google.gerrit.reviewdb.client.Project;
|
|
|
|
import java.util.Set;
|
|
|
|
public class MyPlugin implements ReviewerSuggestion {
|
|
public Set<SuggestedReviewer> suggestReviewers(Project.NameKey project,
|
|
@Nullable Change.Id changeId, @Nullable String query,
|
|
Set<Account.Id> candidates) {
|
|
Set<SuggestedReviewer> suggestions = new HashSet<>();
|
|
// Implement your ranking logic here
|
|
return suggestions;
|
|
}
|
|
}
|
|
----
|
|
|
|
|
|
[[mail-filter]]
|
|
== Mail Filter Plugins
|
|
|
|
Gerrit provides an extension point that enables Plugins to discard incoming
|
|
messages and prevent further processing by Gerrit.
|
|
|
|
This can be used to implement spam checks, signature validations or organization
|
|
specific checks like IP filters.
|
|
|
|
[source, java]
|
|
----
|
|
import com.google.gerrit.extensions.annotations.ExtensionPoint;
|
|
import com.google.gerrit.server.mail.receive.MailMessage;
|
|
|
|
public class MyPlugin implements MailFilter {
|
|
boolean shouldProcessMessage(MailMessage message) {
|
|
// Implement your filter logic here
|
|
return true;
|
|
}
|
|
}
|
|
----
|
|
|
|
[[ssh-command-interception]]
|
|
== SSH Command Interception
|
|
|
|
Gerrit provides an extension point that allows a plugin to intercept
|
|
creation of SSH commands and override the functionality with its own
|
|
implementation.
|
|
|
|
[source, java]
|
|
----
|
|
import com.google.gerrit.sshd.SshCreateCommandInterceptor;
|
|
|
|
class MyCommandInterceptor implements SshCreateCommandInterceptor {
|
|
@Override
|
|
public String intercept(String in) {
|
|
return pluginName + " mycommand";
|
|
}
|
|
}
|
|
----
|
|
|
|
|
|
== 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
|
|
---------
|