From 705f2848636e13849157dc2d0e1b5add38990134 Mon Sep 17 00:00:00 2001 From: Edwin Kempin Date: Wed, 30 Oct 2013 14:25:31 +0100 Subject: [PATCH] Support project specific plugin configuration in own config file For some plugins the project specific configuration is too complex to be stored in a single 'plugin' subsection in 'project.config'. If a plugin needs subsections in its project specific configuration then the project specific plugin configuration can be stored in an own configuration file in the project's 'refs/meta/config' branch. This change adds a method to PluginConfigFactory to easily access the project specific plugin configuration that is stored in such a plugin configuration file. The project specific plugin configuration is cached within ProjectState (which is cached in the project cache). This avoids reloading and reparsing of the config file on every access. After a project is evicted from the cache the project specific plugin configuration is reloaded. This means that any update that is done to the plugins configuration file is immediately active since any push to the `refs/meta/config` branch evicts the project from the cache and hence triggers a reloading of the project specific plugin configuration. Change-Id: Id5d371a22041bd7381f92e75fb021af5318249b4 Signed-off-by: Edwin Kempin --- Documentation/dev-plugins.txt | 61 +++++++++-- .../google/gerrit/acceptance/git/GitUtil.java | 7 ++ .../rest/project/ProjectLevelConfigIT.java | 102 ++++++++++++++++++ .../server/config/PluginConfigFactory.java | 23 ++++ .../gerrit/server/git/ProjectLevelConfig.java | 57 ++++++++++ .../gerrit/server/project/ProjectState.java | 27 +++++ 6 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java create mode 100644 gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectLevelConfig.java diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt index b529217db4..95560e2f36 100644 --- a/Documentation/dev-plugins.txt +++ b/Documentation/dev-plugins.txt @@ -552,9 +552,9 @@ The plugin configuration is loaded only once and is then cached. Similar to changes in 'gerrit.config', changes to the plugin configuration file will only become effective after a Gerrit restart. -[[project-specific-configuration]] -Project Specific Configuration ------------------------------- +[[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 @@ -566,12 +566,12 @@ 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. +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 @@ -616,6 +616,49 @@ Project owners can edit the project configuration by fetching the `refs/meta/config` branch, editing the `project.config` file and pushing the commit back. +[[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"); +---- + +Project owners can edit the project configuration by fetching the +`refs/meta/config` branch, editing the `.config` file and +pushing the commit back. + [[capabilities]] Plugin Owned Capabilities ------------------------- diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java index c17598f2cd..6e795f862f 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java @@ -28,6 +28,7 @@ import org.eclipse.jgit.api.AddCommand; import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.CommitCommand; +import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.PushCommand; import org.eclipse.jgit.api.errors.GitAPIException; @@ -178,6 +179,12 @@ public class GitUtil { } } + public static void fetch(Git git, String spec) throws GitAPIException { + FetchCommand fetch = git.fetch(); + fetch.setRefSpecs(new RefSpec(spec)); + fetch.call(); + } + public static void checkout(Git git, String name) throws GitAPIException { CheckoutCommand checkout = git.checkout(); checkout.setName(name); diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java new file mode 100644 index 0000000000..2afc3e49f4 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectLevelConfigIT.java @@ -0,0 +1,102 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.acceptance.rest.project; + +import static com.google.gerrit.acceptance.git.GitUtil.checkout; +import static com.google.gerrit.acceptance.git.GitUtil.cloneProject; +import static com.google.gerrit.acceptance.git.GitUtil.createProject; +import static com.google.gerrit.acceptance.git.GitUtil.fetch; +import static com.google.gerrit.acceptance.git.GitUtil.initSsh; +import static org.junit.Assert.assertEquals; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.AccountCreator; +import com.google.gerrit.acceptance.SshSession; +import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.acceptance.git.PushOneCommit; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.project.ProjectState; +import com.google.gwtorm.server.SchemaFactory; +import com.google.inject.Inject; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Config; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; + +public class ProjectLevelConfigIT extends AbstractDaemonTest { + + @Inject + private AccountCreator accounts; + + @Inject + private SchemaFactory reviewDbProvider; + + @Inject + private ProjectCache projectCache; + + private ReviewDb db; + private TestAccount admin; + private String project; + private Git git; + + @Before + public void setUp() throws Exception { + admin = accounts.admin(); + initSsh(admin); + SshSession sshSession = new SshSession(server, admin); + + project = "p"; + createProject(sshSession, project, null, true); + git = cloneProject(sshSession.getUrl() + "/" + project); + fetch(git, GitRepositoryManager.REF_CONFIG + ":refs/heads/config"); + checkout(git, "refs/heads/config"); + + db = reviewDbProvider.open(); + } + + @After + public void cleanup() { + db.close(); + } + + @Test + public void accessProjectSpecificConfig() throws GitAPIException, IOException { + String configName = "test.config"; + Config cfg = new Config(); + cfg.setString("s1", null, "k1", "v1"); + cfg.setString("s2", "ss", "k2", "v2"); + PushOneCommit push = + new PushOneCommit(db, admin.getIdent(), "Create Project Level Config", + configName, cfg.toText()); + push.to(git, GitRepositoryManager.REF_CONFIG); + + ProjectState state = projectCache.get(new Project.NameKey(project)); + assertEquals(cfg.toText(), state.getConfig(configName).get().toText()); + } + + @Test + public void nonExistingConfig() { + ProjectState state = projectCache.get(new Project.NameKey(project)); + assertEquals("", state.getConfig("test.config").get().toText()); + } +} 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 e222bcf599..1bb8679cae 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 @@ -179,4 +179,27 @@ public class PluginConfigFactory { return cfg; } + + /** + * Returns the configuration for the specified plugin that is stored in the + * '.config' file in the 'refs/meta/config' branch of the + * specified project. + * + * @param projectName the name of the project for which the plugin + * configuration should be returned + * @param pluginName the name of the plugin for which the configuration should + * be returned + * @return the plugin configuration from the '.config' file of + * the specified project + * @throws NoSuchProjectException thrown if the specified project does not + * exist + */ + public Config getProjectPluginConfig(Project.NameKey projectName, + String pluginName) throws NoSuchProjectException { + ProjectState projectState = projectCache.get(projectName); + if (projectState == null) { + throw new NoSuchProjectException(projectName); + } + return projectState.getConfig(pluginName + ".config").get(); + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectLevelConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectLevelConfig.java new file mode 100644 index 0000000000..902f025a19 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectLevelConfig.java @@ -0,0 +1,57 @@ +// Copyright (C) 2013 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server.git; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Config; + +import java.io.IOException; + +/** Configuration file in the projects refs/meta/config branch. */ +public class ProjectLevelConfig extends VersionedMetaData { + private final String fileName; + private Config cfg; + + public ProjectLevelConfig(String fileName) { + this.fileName = fileName; + } + + @Override + protected String getRefName() { + return GitRepositoryManager.REF_CONFIG; + } + + @Override + protected void onLoad() throws IOException, ConfigInvalidException { + cfg = readConfig(fileName); + } + + public Config get() { + if (cfg == null) { + cfg = new Config(); + } + return cfg; + } + + @Override + protected void onSave(CommitBuilder commit) throws IOException, + ConfigInvalidException { + if (commit.getMessage() == null || "".equals(commit.getMessage())) { + commit.setMessage("Updated configuration\n"); + } + saveConfig(fileName, cfg); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java index 800fa6ea2c..c3cee65272 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java @@ -40,12 +40,14 @@ import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.git.ProjectLevelConfig; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import com.googlecode.prolog_cafe.compiler.CompileException; import com.googlecode.prolog_cafe.lang.PrologMachineCopy; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.slf4j.Logger; @@ -83,6 +85,7 @@ public class ProjectState { private final List commentLinks; private final ProjectConfig config; + private final Map configs; private final Set localOwners; /** Prolog rule state. */ @@ -121,6 +124,7 @@ public class ProjectState { this.rulesCache = rulesCache; this.commentLinks = commentLinks; this.config = config; + this.configs = Maps.newHashMap(); this.capabilities = isAllProjects ? new CapabilityCollection(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES)) : null; @@ -216,6 +220,29 @@ public class ProjectState { return config; } + public ProjectLevelConfig getConfig(String fileName) { + if (configs.containsKey(fileName)) { + return configs.get(fileName); + } + + ProjectLevelConfig cfg = new ProjectLevelConfig(fileName); + try { + Repository git = gitMgr.openRepository(getProject().getNameKey()); + try { + cfg.load(git); + } finally { + git.close(); + } + } catch (IOException e) { + log.warn("Failed to load " + fileName + " for " + getProject().getName(), e); + } catch (ConfigInvalidException e) { + log.warn("Failed to load " + fileName + " for " + getProject().getName(), e); + } + + configs.put(fileName, cfg); + return cfg; + } + public long getMaxObjectSizeLimit() { return config.getMaxObjectSizeLimit(); }