Support project level plugin configuration
Some plugins need a configuration on project level (e.g. for
'plugins/reviewers-by-blame' the max number of reviewers that should
be automatically added should be configured per project). The idea is
to store this configuration in project.config inside a "plugin"
section having a subsection per plugin. E.g.:
[plugin "reviewers-by-blame"]
maxReviewers = 3
It should be avoided that each plugin needs to parse 'project.config'
on its own. It is also difficult from a plugin to cache the
configuration parameters as the plugin doesn't know when the
'project.config' file is updated.
Extend ProjectConfig so that it automatically parses all 'plugin'
sections on load. The values of these sections are stored internally
and can easily be accessed from a plugin, e.g.:
int maxReviewers = pluginConfigFactory
.get(project, "reviewers-by-blame")
.getInt("maxReviewers", 0);
Change-Id: I802f38a4d41814a51d40d41eb7824637c7e34948
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 64c9bff..c0abf9a 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -366,6 +366,45 @@
.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.
+
+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.get(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
-------------------------
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
index 50a8f33..bc5e914 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfigFactory.java
@@ -14,6 +14,10 @@
package com.google.gerrit.server.config;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -22,13 +26,24 @@
@Singleton
public class PluginConfigFactory {
private final Config cfg;
+ private final ProjectCache projectCache;
@Inject
- PluginConfigFactory(@GerritServerConfig Config cfg) {
+ PluginConfigFactory(@GerritServerConfig Config cfg, ProjectCache projectCache) {
this.cfg = cfg;
+ this.projectCache = projectCache;
}
public PluginConfig get(String pluginName) {
return new PluginConfig(pluginName, cfg);
}
+
+ public PluginConfig get(Project.NameKey projectName, String pluginName)
+ throws NoSuchProjectException {
+ ProjectState projectState = projectCache.get(projectName);
+ if (projectState == null) {
+ throw new NoSuchProjectException(projectName);
+ }
+ return projectState.getConfig().getPluginConfig(pluginName);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 3df559d..9ee8af7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -46,6 +46,7 @@
import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.project.CommentLinkInfo;
@@ -67,6 +68,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
@@ -132,6 +134,8 @@
private static final Set<String> LABEL_FUNCTIONS = ImmutableSet.of(
"MaxWithBlock", "AnyWithBlock", "MaxNoBlock", "NoBlock", "NoOp");
+ private static final String PLUGIN = "plugin";
+
private static final SubmitType defaultSubmitAction =
SubmitType.MERGE_IF_NECESSARY;
private static final State defaultStateValue =
@@ -149,6 +153,7 @@
private List<ValidationError> validationErrors;
private ObjectId rulesId;
private long maxObjectSizeLimit;
+ private Map<String, Config> pluginConfigs;
public static ProjectConfig read(MetaDataUpdate update) throws IOException,
ConfigInvalidException {
@@ -399,6 +404,7 @@
loadNotifySections(rc, groupsByName);
loadLabelSections(rc);
loadCommentLinkSections(rc);
+ loadPluginSections(rc);
maxObjectSizeLimit = rc.getLong(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, 0);
}
@@ -683,6 +689,27 @@
commentLinkSections = ImmutableList.copyOf(commentLinkSections);
}
+ private void loadPluginSections(Config rc) {
+ pluginConfigs = Maps.newHashMap();
+ for (String plugin : rc.getSubsections(PLUGIN)) {
+ Config pluginConfig = new Config();
+ pluginConfigs.put(plugin, pluginConfig);
+ for (String name : rc.getNames(PLUGIN, plugin)) {
+ pluginConfig.setStringList(PLUGIN, plugin, name,
+ Arrays.asList(rc.getStringList(PLUGIN, plugin, name)));
+ }
+ }
+ }
+
+ public PluginConfig getPluginConfig(String pluginName) {
+ Config pluginConfig = pluginConfigs.get(pluginName);
+ if (pluginConfig == null) {
+ pluginConfig = new Config();
+ pluginConfigs.put(pluginName, pluginConfig);
+ }
+ return new PluginConfig(pluginName, pluginConfig);
+ }
+
private Map<String, GroupReference> readGroupList() throws IOException {
groupsByUUID = new HashMap<AccountGroup.UUID, GroupReference>();
Map<String, GroupReference> groupsByName =
@@ -748,6 +775,7 @@
saveNotifySections(rc, keepGroups);
groupsByUUID.keySet().retainAll(keepGroups);
saveLabelSections(rc);
+ savePluginSections(rc);
saveConfig(PROJECT_CONFIG, rc);
saveGroupList();
@@ -997,6 +1025,22 @@
}
}
+ private void savePluginSections(Config rc) {
+ List<String> existing = Lists.newArrayList(rc.getSubsections(PLUGIN));
+ for (String name : existing) {
+ rc.unsetSection(PLUGIN, name);
+ }
+
+ for (Entry<String, Config> e : pluginConfigs.entrySet()) {
+ String plugin = e.getKey();
+ Config pluginConfig = e.getValue();
+ for (String name : pluginConfig.getNames(PLUGIN, plugin)) {
+ rc.setStringList(PLUGIN, plugin, name,
+ Arrays.asList(pluginConfig.getStringList(PLUGIN, plugin, name)));
+ }
+ }
+ }
+
private void saveGroupList() throws IOException {
if (groupsByUUID.isEmpty()) {
saveFile(GROUP_LIST, null);