diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index f9138a9e1e..98ac07ade6 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -741,6 +741,11 @@ how the replacement is displayed to the user.
html = $1$2
----
+Comment links can also be specified in `project.config` and sections in
+children override those in parents. The only restriction is that to
+avoid injecting arbitrary user-supplied HTML in the page, comment links
+defined in `project.config` may only supply `link`, not `html`.
+
[[commentlink.name.match]]commentlink..match::
+
A JavaScript regular expression to match positions to be replaced
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 bd4fafb335..43413eacb7 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
@@ -14,12 +14,14 @@
package com.google.gerrit.server.git;
+import static com.google.common.base.Preconditions.checkArgument;
import static com.google.gerrit.common.data.Permission.isPermission;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
@@ -66,6 +68,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
public class ProjectConfig extends VersionedMetaData {
public static final String COMMENTLINK = "commentlink";
@@ -138,6 +141,7 @@ public class ProjectConfig extends VersionedMetaData {
private Map contributorAgreements;
private Map notifySections;
private Map labelSections;
+ private List commentLinkSections;
private List validationErrors;
private ObjectId rulesId;
@@ -155,8 +159,8 @@ public class ProjectConfig extends VersionedMetaData {
return r;
}
- public static CommentLinkInfo buildCommentLink(Config cfg, String name)
- throws IllegalArgumentException {
+ public static CommentLinkInfo buildCommentLink(Config cfg, String name,
+ boolean allowRaw) throws IllegalArgumentException {
String match = cfg.getString(COMMENTLINK, name, KEY_MATCH);
// Unfortunately this validation isn't entirely complete. Clients
@@ -168,6 +172,8 @@ public class ProjectConfig extends VersionedMetaData {
String link = cfg.getString(COMMENTLINK, name, KEY_LINK);
String html = cfg.getString(COMMENTLINK, name, KEY_HTML);
+ checkArgument(allowRaw || Strings.isNullOrEmpty(html),
+ "Raw html replacement not allowed");
return new CommentLinkInfo(name, match, link, html);
}
@@ -256,6 +262,10 @@ public class ProjectConfig extends VersionedMetaData {
return labelSections;
}
+ public Collection getCommentLinkSections() {
+ return commentLinkSections;
+ }
+
public GroupReference resolve(AccountGroup group) {
return resolve(GroupReference.forGroup(group));
}
@@ -356,6 +366,7 @@ public class ProjectConfig extends VersionedMetaData {
loadAccessSections(rc, groupsByName);
loadNotifySections(rc, groupsByName);
loadLabelSections(rc);
+ loadCommentLinkSections(rc);
}
private void loadAccountsSection(
@@ -613,6 +624,25 @@ public class ProjectConfig extends VersionedMetaData {
}
}
+ private void loadCommentLinkSections(Config rc) {
+ Set subsections = rc.getSubsections(COMMENTLINK);
+ commentLinkSections = Lists.newArrayListWithCapacity(subsections.size());
+ for (String name : subsections) {
+ try {
+ commentLinkSections.add(buildCommentLink(rc, name, false));
+ } catch (PatternSyntaxException e) {
+ error(new ValidationError(PROJECT_CONFIG, String.format(
+ "Invalid pattern \"%s\" in commentlink.%s.match: %s",
+ rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage())));
+ } catch (IllegalArgumentException e) {
+ error(new ValidationError(PROJECT_CONFIG, String.format(
+ "Error in pattern \"%s\" in commentlink.%s.match: %s",
+ rc.getString(COMMENTLINK, name, KEY_MATCH), name, e.getMessage())));
+ }
+ }
+ commentLinkSections = ImmutableList.copyOf(commentLinkSections);
+ }
+
private Map readGroupList() throws IOException {
groupsByUUID = new HashMap();
Map groupsByName =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
index 747ccfe420..3d54003ca3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CommentLinkProvider.java
@@ -40,7 +40,7 @@ public class CommentLinkProvider implements Provider> {
List cls =
Lists.newArrayListWithCapacity(subsections.size());
for (String name : subsections) {
- cls.add(ProjectConfig.buildCommentLink(cfg, name));
+ cls.add(ProjectConfig.buildCommentLink(cfg, name, true));
}
return ImmutableList.copyOf(cls);
}
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 ec00e3fd31..3d5e1b8db2 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
@@ -16,6 +16,7 @@ package com.google.gerrit.server.project;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -295,6 +296,16 @@ public class ProjectState {
};
}
+ /**
+ * @return an iterable that walks in-order from All-Projects through the
+ * project hierarchy to this project.
+ */
+ public Iterable treeInOrder() {
+ List projects = Lists.newArrayList(tree());
+ Collections.reverse(projects);
+ return projects;
+ }
+
/**
* @return an iterable that walks through the parents of this project. Starts
* from the immediate parent of this project and progresses up the
@@ -346,9 +357,7 @@ public class ProjectState {
public LabelTypes getLabelTypes() {
Map types = Maps.newLinkedHashMap();
- List projects = Lists.newArrayList(tree());
- Collections.reverse(projects);
- for (ProjectState s : projects) {
+ for (ProjectState s : treeInOrder()) {
for (LabelType type : s.getConfig().getLabelSections().values()) {
String lower = type.getName().toLowerCase();
LabelType old = types.get(lower);
@@ -367,7 +376,16 @@ public class ProjectState {
}
public List getCommentLinks() {
- return commentLinks;
+ Map cls = Maps.newLinkedHashMap();
+ for (CommentLinkInfo cl : commentLinks) {
+ cls.put(cl.name.toLowerCase(), cl);
+ }
+ for (ProjectState s : treeInOrder()) {
+ for (CommentLinkInfo cl : s.getConfig().getCommentLinkSections()) {
+ cls.put(cl.name.toLowerCase(), cl);
+ }
+ }
+ return ImmutableList.copyOf(cls.values());
}
private boolean getInheritableBoolean(Function func) {