| 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.5-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 helloworld plugin: |
| + |
| This is a Maven project that adds an SSH command to Gerrit to print |
| out a hello world message. It can be taken as an example to develop |
| an own plugin. |
| + |
| ---- |
| $ git clone https://gerrit.googlesource.com/plugins/helloworld |
| ---- |
| + |
| 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.commondatastorage.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 |
| ==== |
| |
| [[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 behaviour: 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 InitStep are executing during the "Gerrit Plugin init" phase, after |
| the extraction of the plugins embedded in Gerrit.war into $GERRIT_SITE/plugins |
| and before the DB Schema initialization or upgrade. |
| Plugins InitStep cannot refer to Gerrit DB Schema or any other Gerrit runtime |
| objects injected at startup. |
| |
| ==== |
| 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. |
| |
| [[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: |
| |
| ==== |
| 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: |
| |
| ==== |
| import com.google.gerrit.extensions.annotations.Export; |
| import com.google.gerrit.sshd.SshCommand; |
| |
| @Export("print") |
| class PrintHello extends SshCommand { |
| protected abstract void run() { |
| stdout.print("Hello\n"); |
| } |
| } |
| ==== |
| |
| If explicit registration is being used, a Guice module must be |
| supplied to register the SSH command and declared in the manifest |
| with the `Gerrit-SshModule` attribute: |
| |
| ==== |
| import com.google.gerrit.sshd.PluginCommandModule; |
| |
| class MyCommands extends PluginCommandModule { |
| protected void configureCommands() { |
| command("print").to(PrintHello.class); |
| } |
| } |
| ==== |
| |
| For a plugin installed as name `helloworld`, the command implemented |
| by PrintHello class will be available to users as: |
| |
| ---- |
| $ ssh -p 29418 review.example.com helloworld print |
| ---- |
| |
| [[http]] |
| HTTP Servlets |
| ------------- |
| |
| Plugins or extensions may register additional HTTP servlets, and |
| wrap them with HTTP filters. |
| |
| Servlets may use auto-registration to declare the URL they handle: |
| |
| ==== |
| import com.google.gerrit.extensions.annotations.Export; |
| import com.google.inject.Singleton; |
| import javax.servlet.http.HttpServlet; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| @Export("/print") |
| @Singleton |
| class HelloServlet extends HttpServlet { |
| protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException { |
| res.setContentType("text/plain"); |
| res.setCharacterEncoding("UTF-8"); |
| res.getWriter().write("Hello"); |
| } |
| } |
| ==== |
| |
| 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: |
| |
| ==== |
| 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. |
| |
| ==== |
| @Inject |
| MyType(@PluginData java.io.File myDir) { |
| new FileInputStream(new File(myDir, "my.config")); |
| } |
| ==== |
| |
| [[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 `static/` directory in the JAR will be |
| available as `/plugins/helloworld/static/resource`. |
| |
| Documentation files under `Documentation/` directory in the JAR |
| will be available as `/plugins/helloworld/Documentation/resource`. |
| |
| Documentation may be written in |
| link:http://daringfireball.net/projects/markdown/[Markdown] style |
| if the file name ends with `.md`. Gerrit will automatically convert |
| Markdown to HTML if accessed with extension `.html`. |
| |
| [[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 name beings with `cmd-` it will be clustered |
| into a 'Commands' section of the generated index page. All other |
| files are clustered under a 'Documentation' section. |
| |
| Some optional information from the manifest is extracted and |
| displayed as part of the index page, if present in the manifest: |
| |
| [width="40%",options="header"] |
| |=================================================== |
| |Field | Source Attribute |
| |Name | Implementation-Title |
| |Vendor | Implementation-Vendor |
| |Version | Implementation-Version |
| |URL | Implementation-URL |
| |API Version | Gerrit-ApiVersion |
| |=================================================== |
| |
| [[deployment]] |
| 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. |
| |
| GERRIT |
| ------ |
| Part of link:index.html[Gerrit Code Review] |