| Gerrit Code Review - Plugin Development |
| ======================================= |
| |
| The Gerrit server functionality can be extended by installing plugins. |
| This page describes how plugins for Gerrit can be developed. |
| |
| Depending on how tightly the extension code is coupled with the Gerrit |
| server code, there is a distinction between `plugins` and `extensions`. |
| |
| [[plugin]] |
| A `plugin` in Gerrit is tightly coupled code that runs in the same |
| JVM as Gerrit. It has full access to all server internals. Plugins |
| are tightly coupled to a specific major.minor server version and |
| may require source code changes to compile against a different |
| server version. |
| |
| [[extension]] |
| An `extension` in Gerrit runs inside of the same JVM as Gerrit |
| in the same way as a plugin, but has limited visibility to the |
| server's internals. The limited visibility reduces the extension's |
| dependencies, enabling it to be compatible across a wider range |
| of server versions. |
| |
| Most of this documentation refers to either type as a plugin. |
| |
| [[getting-started]] |
| Getting started |
| --------------- |
| |
| To get started with the development of a plugin there are two |
| recommended ways: |
| |
| . use the Gerrit Plugin Maven archetype to create a new plugin project: |
| + |
| With the Gerrit Plugin Maven archetype you can create a skeleton for a |
| plugin project. |
| + |
| ---- |
| mvn archetype:generate -DarchetypeGroupId=com.google.gerrit \ |
| -DarchetypeArtifactId=gerrit-plugin-archetype \ |
| -DarchetypeVersion=2.8-SNAPSHOT \ |
| -DgroupId=com.google.gerrit \ |
| -DartifactId=testPlugin |
| ---- |
| + |
| Maven will ask for additional properties and then create the plugin in |
| the current directory. To change the default property values answer 'n' |
| when Maven asks to confirm the properties configuration. It will then |
| ask again for all properties including those with predefined default |
| values. |
| |
| . clone the sample plugin: |
| + |
| This is a project that demonstrates the various features of the |
| plugin API. It can be taken as an example to develop an own plugin. |
| + |
| ---- |
| $ git clone https://gerrit.googlesource.com/plugins/cookbook-plugin |
| ---- |
| + |
| When starting from this example one should take care to adapt the |
| `Gerrit-ApiVersion` in the `pom.xml` to the version of Gerrit for which |
| the plugin is developed. If the plugin is developed for a released |
| Gerrit version (no `SNAPSHOT` version) then the URL for the |
| `gerrit-api-repository` in the `pom.xml` needs to be changed to |
| `https://gerrit-api.storage.googleapis.com/release/`. |
| |
| [[API]] |
| API |
| --- |
| |
| There are two different API formats offered against which plugins can |
| be developed: |
| |
| gerrit-extension-api.jar:: |
| A stable but thin interface. Suitable for extensions that need |
| to be notified of events, but do not require tight coupling to |
| the internals of Gerrit. Extensions built against this API can |
| expect to be binary compatible across a wide range of server |
| versions. |
| |
| gerrit-plugin-api.jar:: |
| The complete internals of the Gerrit server, permitting a |
| plugin to tightly couple itself and provide additional |
| functionality that is not possible as an extension. Plugins |
| built against this API are expected to break at the source |
| code level between every major.minor Gerrit release. A plugin |
| that compiles against 2.5 will probably need source code level |
| changes to work with 2.6, 2.7, and so on. |
| |
| Manifest |
| -------- |
| |
| Plugins may provide optional description information with standard |
| manifest fields: |
| |
| ==== |
| Implementation-Title: Example plugin showing examples |
| Implementation-Version: 1.0 |
| Implementation-Vendor: Example, Inc. |
| Implementation-URL: http://example.com/opensource/plugin-foo/ |
| ==== |
| |
| ApiType |
| ~~~~~~~ |
| |
| Plugins using the tightly coupled `gerrit-plugin-api.jar` must |
| declare this API dependency in the manifest to gain access to server |
| internals. If no `Gerrit-ApiType` is specified the stable `extension` |
| API will be assumed. This may cause ClassNotFoundExceptions when |
| loading a plugin that needs the plugin API. |
| |
| ==== |
| Gerrit-ApiType: plugin |
| ==== |
| |
| Explicit Registration |
| ~~~~~~~~~~~~~~~~~~~~~ |
| |
| Plugins that use explicit Guice registration must name the Guice |
| modules in the manifest. Up to three modules can be named in the |
| manifest. `Gerrit-Module` supplies bindings to the core server; |
| `Gerrit-SshModule` supplies SSH commands to the SSH server (if |
| enabled); `Gerrit-HttpModule` supplies servlets and filters to the HTTP |
| server (if enabled). If no modules are named automatic registration |
| will be performed by scanning all classes in the plugin JAR for |
| `@Listen` and `@Export("")` annotations. |
| |
| ==== |
| Gerrit-Module: tld.example.project.CoreModuleClassName |
| Gerrit-SshModule: tld.example.project.SshModuleClassName |
| Gerrit-HttpModule: tld.example.project.HttpModuleClassName |
| ==== |
| |
| [[plugin_name]] |
| Plugin Name |
| ~~~~~~~~~~~ |
| |
| A plugin can optionally provide its own plugin name. |
| |
| ==== |
| Gerrit-PluginName: replication |
| ==== |
| |
| This is useful for plugins that contribute plugin-owned capabilities that |
| are stored in the `project.config` file. Another use case is to be able to put |
| project specific plugin configuration section in `project.config`. In this |
| case it is advantageous to reserve the plugin name to access the configuration |
| section in the `project.config` file. |
| |
| If `Gerrit-PluginName` is omitted, then the plugin's name is determined from |
| the plugin file name. |
| |
| If a plugin provides its own name, then that plugin cannot be deployed |
| multiple times under different file names on one Gerrit site. |
| |
| For Maven driven plugins, the following line must be included in the pom.xml |
| file: |
| |
| [source,xml] |
| ---- |
| <manifestEntries> |
| <Gerrit-PluginName>name</Gerrit-PluginName> |
| </manifestEntries> |
| ---- |
| |
| For Buck driven plugins, the following line must be included in the BUCK |
| configuration file: |
| |
| [source,python] |
| ---- |
| manifest_entries = [ |
| 'Gerrit-PluginName: name', |
| ] |
| ---- |
| |
| A plugin can get its own name injected at runtime: |
| |
| [source,java] |
| ---- |
| public class MyClass { |
| |
| private final String pluginName; |
| |
| @Inject |
| public MyClass(@PluginName String pluginName) { |
| this.pluginName = pluginName; |
| } |
| |
| [...] |
| } |
| ---- |
| |
| [[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 propertly. |
| |
| 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. |
| |
| 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"); |
| } |
| } |
| ---- |
| |
| [[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.ChangeListener`: |
| + |
| Allows to listen to change events. These are the same |
| link:cmd-stream-events.html#events[events] that are also streamed by |
| the link:cmd-stream-events.html[gerrit stream-events] command. |
| |
| * `com.google.gerrit.extensions.events.LifecycleListener`: |
| + |
| Gerrit server startup and shutdown |
| |
| * `com.google.gerrit.extensions.events.NewProjectCreatedListener`: |
| + |
| Project creation |
| |
| * `com.google.gerrit.extensions.events.ProjectDeletedListener`: |
| + |
| Project deletion |
| |
| [[stream-events]] |
| Sending Events to the Events Stream |
| ----------------------------------- |
| |
| Plugins may send events to the events stream where consumers of |
| Gerrit's `stream-events` ssh command will receive them. |
| |
| To send an event, the plugin must invoke one of the `postEvent` |
| methods in the `ChangeHookRunner` class, passing an instance of |
| its own custom event class derived from `ChangeEvent`. |
| |
| [[validation]] |
| Validation Listeners |
| -------------------- |
| |
| Certain operations in Gerrit can be validated by plugins by |
| implementing the corresponding link:config-validation.html[listeners]. |
| |
| [[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; |
| |
| class PrintHello extends SshCommand { |
| protected abstract void run() { |
| stdout.print("Hello\n"); |
| } |
| } |
| ---- |
| |
| If no Guice modules are declared in the manifest, SSH commands may |
| use auto-registration by providing an `@Export` annotation: |
| |
| [source,java] |
| ---- |
| import com.google.gerrit.extensions.annotations.Export; |
| import com.google.gerrit.sshd.SshCommand; |
| |
| @Export("print") |
| class PrintHello extends SshCommand { |
| protected abstract void run() { |
| stdout.print("Hello\n"); |
| } |
| } |
| ---- |
| |
| If explicit registration is being used, a Guice module must be |
| supplied to register the SSH command and declared in the manifest |
| with the `Gerrit-SshModule` attribute: |
| |
| [source,java] |
| ---- |
| import com.google.gerrit.sshd.PluginCommandModule; |
| |
| class MyCommands extends PluginCommandModule { |
| protected void configureCommands() { |
| command("print").to(PrintHello.class); |
| } |
| } |
| ---- |
| |
| For a plugin installed as name `helloworld`, the command implemented |
| by PrintHello class will be available to users as: |
| |
| ---- |
| $ ssh -p 29418 review.example.com helloworld print |
| ---- |
| |
| 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 |
| ---- |
| |
| [[configuration]] |
| Configuration |
| ------------- |
| |
| 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 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"); |
| ---- |
| |
| [[project-specific-configuration]] |
| Project Specific Configuration |
| ------------------------------ |
| |
| 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 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. |
| |
| [[capabilities]] |
| Plugin Owned Capabilities |
| ------------------------- |
| |
| Plugins may provide their own capabilities and restrict usage of SSH |
| commands 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, UI actions may |
| use auto-registration by providing an `@Export` annotation: |
| |
| [source,java] |
| ---- |
| @Export("printHello") |
| public class PrintHelloCapability extends CapabilityDefinition { |
| [...] |
| } |
| ---- |
| |
| Otherwise the capability must be bound in a plugin module: |
| |
| [source,java] |
| ---- |
| public class HelloWorldModule extends AbstractModule { |
| @Override |
| protected void configure() { |
| bind(CapabilityDefinition.class) |
| .annotatedWith(Exports.named("printHello")) |
| .to(PrintHelloCapability.class); |
| } |
| } |
| ---- |
| |
| With a plugin-owned capability defined in this way, it is possible to restrict |
| usage of an SSH command or `UiAction` to members of the group that were granted |
| this capability in the usual way, using the `RequiresCapability` annotation: |
| |
| [source,java] |
| ---- |
| @RequiresCapability("printHello") |
| @CommandMetaData(name="print", description="Print greeting in different languages") |
| public final class PrintHelloWorldCommand extends SshCommand { |
| [...] |
| } |
| ---- |
| |
| Or with `UiAction`: |
| |
| [source,java] |
| ---- |
| @RequiresCapability("printHello") |
| public class SayHelloAction extends UiAction<RevisionResource> |
| implements RestModifyView<RevisionResource, SayHelloAction.Input> { |
| [...] |
| } |
| ---- |
| |
| Capability scope was introduced to differentiate between plugin-owned |
| capabilities and core capabilities. Per default the scope of the |
| `@RequiresCapability` annotation is `CapabilityScope.CONTEXT`, that means: |
| |
| * when `@RequiresCapability` is used within a plugin the scope of the |
| capability is assumed to be that plugin. |
| |
| * If `@RequiresCapability` is used within the core Gerrit Code Review server |
| (and thus is outside of a plugin) the scope is the core server and will use |
| the `GlobalCapability` known to Gerrit Code Review server. |
| |
| If a plugin needs to use a core capability name (e.g. "administrateServer") |
| this can be specified by setting `scope = CapabilityScope.CORE`: |
| |
| [source,java] |
| ---- |
| @RequiresCapability(value = "administrateServer", scope = |
| CapabilityScope.CORE) |
| [...] |
| ---- |
| |
| [[ui_extension]] |
| UI Extension |
| ------------ |
| |
| Plugins can contribute UI actions on core Gerrit pages. This is useful |
| for workflow customization or exposing plugin functionality through the |
| UI in addition to SSH commands and the REST API. |
| |
| For instance a plugin to integrate Jira with Gerrit changes may |
| contribute a "File bug" button to allow filing a bug from the change |
| page or plugins to integrate continuous integration systems may |
| contribute a "Schedule" button to allow a CI build to be scheduled |
| manually from the patch set panel. |
| |
| Two different places on core Gerrit pages are supported: |
| |
| * Change screen |
| * Project info screen |
| |
| Plugins contribute UI actions by implementing the `UiAction` interface: |
| |
| [source,java] |
| ---- |
| @RequiresCapability("printHello") |
| class HelloWorldAction implements UiAction<RevisionResource>, |
| RestModifyView<RevisionResource, HelloWorldAction.Input> { |
| static class Input { |
| boolean french; |
| String message; |
| } |
| |
| private Provider<CurrentUser> user; |
| |
| @Inject |
| HelloWorldAction(Provider<CurrentUser> user) { |
| this.user = user; |
| } |
| |
| @Override |
| public String apply(RevisionResource rev, Input input) { |
| final String greeting = input.french |
| ? "Bonjour" |
| : "Hello"; |
| return String.format("%s %s from change %s, patch set %d!", |
| greeting, |
| Strings.isNullOrEmpty(input.message) |
| ? Objects.firstNonNull(user.get().getUserName(), "world") |
| : input.message, |
| rev.getChange().getId().toString(), |
| rev.getPatchSet().getPatchSetId()); |
| } |
| |
| @Override |
| public Description getDescription( |
| RevisionResource resource) { |
| return new Description() |
| .setLabel("Say hello") |
| .setTitle("Say hello in different languages"); |
| } |
| } |
| ---- |
| |
| Sometimes plugins may want to be able to change the state of a patch set or |
| change in the `UiAction.apply()` method and reflect these changes on the core |
| UI. For example a buildbot plugin which exposes a 'Schedule' button on the |
| patch set panel may want to disable that button after the build was scheduled |
| and update the tooltip of that button. But because of Gerrit's caching |
| strategy the following must be taken into consideration. |
| |
| The browser is allowed to cache the `UiAction` information until something on |
| the change is modified. More accurately the change row needs to be modified in |
| the database to have a more recent `lastUpdatedOn` or a new `rowVersion`, or |
| the +refs/meta/config+ of the project or any parents needs to change to a new |
| SHA-1. The ETag SHA-1 computation code can be found in the |
| `ChangeResource.getETag()` method. |
| |
| The easiest way to accomplish this is to update `lastUpdatedOn` of the change: |
| |
| [source,java] |
| ---- |
| @Override |
| public Object apply(RevisionResource rcrs, Input in) { |
| // schedule a build |
| [...] |
| // update change |
| ReviewDb db = dbProvider.get(); |
| db.changes().beginTransaction(change.getId()); |
| try { |
| change = db.changes().atomicUpdate( |
| change.getId(), |
| new AtomicUpdate<Change>() { |
| @Override |
| public Change update(Change change) { |
| ChangeUtil.updated(change); |
| return change; |
| } |
| }); |
| db.commit(); |
| } finally { |
| db.rollback(); |
| } |
| [...] |
| } |
| ---- |
| |
| `UiAction` must be bound in a plugin module: |
| |
| [source,java] |
| ---- |
| public class Module extends AbstractModule { |
| @Override |
| protected void configure() { |
| install(new RestApiModule() { |
| @Override |
| protected void configure() { |
| post(REVISION_KIND, "say-hello") |
| .to(HelloWorldAction.class); |
| } |
| }); |
| } |
| } |
| ---- |
| |
| The module above must be declared in the `pom.xml` for Maven driven |
| plugins: |
| |
| [source,xml] |
| ---- |
| <manifestEntries> |
| <Gerrit-Module>com.googlesource.gerrit.plugins.cookbook.Module</Gerrit-Module> |
| </manifestEntries> |
| ---- |
| |
| or in the `BUCK` configuration file for Buck driven plugins: |
| |
| [source,python] |
| ---- |
| manifest_entries = [ |
| 'Gerrit-Module: com.googlesource.gerrit.plugins.cookbook.Module', |
| ] |
| ---- |
| |
| In some use cases more user input must be gathered, for that `UiAction` can be |
| combined with the JavaScript API. This would display a small popup near the |
| activation button to gather additional input from the user. The JS file is |
| typically put in the `static` folder within the plugin's directory: |
| |
| [source,javascript] |
| ---- |
| Gerrit.install(function(self) { |
| function onSayHello(c) { |
| var f = c.textfield(); |
| var t = c.checkbox(); |
| var b = c.button('Say hello', {onclick: function(){ |
| c.call( |
| {message: f.value, french: t.checked}, |
| function(r) { |
| c.hide(); |
| window.alert(r); |
| c.refresh(); |
| }); |
| }}); |
| c.popup(c.div( |
| c.prependLabel('Greeting message', f), |
| c.br(), |
| c.label(t, 'french'), |
| c.br(), |
| b)); |
| f.focus(); |
| } |
| self.onAction('revision', 'say-hello', onSayHello); |
| }); |
| ---- |
| |
| The JS module must be exposed as a `WebUiPlugin` and bound as |
| an HTTP Module: |
| |
| [source,java] |
| ---- |
| public class HttpModule extends HttpPluginModule { |
| @Override |
| protected void configureServlets() { |
| DynamicSet.bind(binder(), WebUiPlugin.class) |
| .toInstance(new JavaScriptPlugin("hello.js")); |
| } |
| } |
| ---- |
| |
| The HTTP module above must be declared in the `pom.xml` for Maven |
| driven plugins: |
| |
| [source,xml] |
| ---- |
| <manifestEntries> |
| <Gerrit-HttpModule>com.googlesource.gerrit.plugins.cookbook.HttpModule</Gerrit-HttpModule> |
| </manifestEntries> |
| ---- |
| |
| or in the `BUCK` configuration file for Buck driven plugins |
| |
| [source,python] |
| ---- |
| manifest_entries = [ |
| 'Gerrit-HttpModule: com.googlesource.gerrit.plugins.cookbook.HttpModule', |
| ] |
| ---- |
| |
| If `UiAction` is annotated with the `@RequiresCapability` annotation, then the |
| capability check is done during the `UiAction` gathering, so the plugin author |
| doesn't have to set `UiAction.Description.setVisible()` explicitly in this |
| case. |
| |
| The following prerequisities must be met, to satisfy the capability check: |
| |
| * user is authenticated |
| * user is a member of a group which has the `Administrate Server` capability, or |
| * user is a member of a group which has the required capability |
| |
| The `apply` method is called when the button is clicked. If `UiAction` is |
| combined with JavaScript API (its own JavaScript function is provided), |
| then a popup dialog is normally opened to gather additional user input. |
| A new button is placed on the popup dialog to actually send the request. |
| |
| Every `UiAction` exposes a REST API endpoint. The endpoint from the example above |
| can be accessed from any REST client, i. e.: |
| |
| ==== |
| curl -X POST -H "Content-Type: application/json" \ |
| -d '{message: "François", french: true}' \ |
| --digest --user joe:secret \ |
| http://host:port/a/changes/1/revisions/1/cookbook~say-hello |
| "Bonjour François from change 1, patch set 1!" |
| ==== |
| |
| A special case is to bind an endpoint without a view name. This is |
| particularly useful for `DELETE` requests: |
| |
| [source,java] |
| ---- |
| public class Module extends AbstractModule { |
| @Override |
| protected void configure() { |
| install(new RestApiModule() { |
| @Override |
| protected void configure() { |
| delete(PROJECT_KIND) |
| .to(DeleteProject.class); |
| } |
| }); |
| } |
| } |
| ---- |
| |
| For a `UiAction` bound this way, a JS API function can be provided. |
| |
| Currently only one restriction exists: per plugin only one `UiAction` |
| can be bound per resource without view name. To define a JS function |
| for the `UiAction`, "/" must be used as the name: |
| |
| [source,javascript] |
| ---- |
| Gerrit.install(function(self) { |
| function onDeleteProject(c) { |
| [...] |
| } |
| self.onAction('project', '/', onDeleteProject); |
| }); |
| ---- |
| |
| [[top-menu-extensions]] |
| Top Menu Extensions |
| ------------------- |
| |
| Plugins can contribute items to Gerrit's top menu. |
| |
| A single top menu extension can have multiple elements and will be put as |
| the last element in Gerrit's top menu. |
| |
| Plugins define the top menu entries by implementing `TopMenu` interface: |
| |
| [source,java] |
| ---- |
| public class MyTopMenuExtension implements TopMenu { |
| |
| @Override |
| public List<MenuEntry> getEntries() { |
| return Lists.newArrayList( |
| new MenuEntry("Top Menu Entry", Lists.newArrayList( |
| new MenuItem("Gerrit", "http://gerrit.googlecode.com/")))); |
| } |
| } |
| ---- |
| |
| Plugins can also add additional menu items to Gerrit's top menu entries |
| by defining a `MenuEntry` that has the same name as a Gerrit top menu |
| entry: |
| |
| [source,java] |
| ---- |
| public class MyTopMenuExtension implements TopMenu { |
| |
| @Override |
| public List<MenuEntry> getEntries() { |
| return Lists.newArrayList( |
| new MenuEntry(GerritTopMenu.PROJECTS, Lists.newArrayList( |
| new MenuItem("Browse Repositories", "https://gerrit.googlesource.com/")))); |
| } |
| } |
| ---- |
| |
| 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 |
| ---- |
| |
| [[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` File |
| 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.io.File myDir) { |
| new FileInputStream(new File(myDir, "my.config")); |
| } |
| ---- |
| |
| [[download-commands]] |
| Download Commands |
| ----------------- |
| |
| Gerrit offers commands for downloading changes using different |
| download schemes (e.g. for downloading via different network |
| protocols). Plugins can contribute download schemes and download |
| commands by implementing |
| `com.google.gerrit.extensions.config.DownloadScheme` and |
| `com.google.gerrit.extensions.config.DownloadCommand`. |
| |
| The download schemes and download commands which are used most often |
| are provided by the Gerrit core plugin `download-commands`. |
| |
| [[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 |
| link:http://daringfireball.net/projects/markdown/[Markdown] style |
| if the file name ends with `.md`. Gerrit will automatically convert |
| Markdown to HTML if accessed with extension `.html`. |
| |
| [[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. |
| |
| Plugins can also be copied directly into the server's |
| directory at `$site_path/plugins/$name.jar`. The name of |
| the JAR file, minus the `.jar` extension, will be used as the |
| plugin name. Unless disabled, servers periodically scan this |
| directory for updated plugins. The time can be adjusted by |
| link:config-gerrit.html#plugins.checkFrequency[plugins.checkFrequency]. |
| |
| 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. |
| |
| 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] |