Merge "Fix link to plugin specific actions in documentation"
diff --git a/BUCK b/BUCK
index c5d6009..29197f0 100644
--- a/BUCK
+++ b/BUCK
@@ -6,7 +6,9 @@
   '//lib/guice:guice',
   '//lib/jgit:jgit',
   '//lib/log:api',
-  '//lib:parboiled-core',
+  '//lib:grappa', # used for it's parboiled part. See
+  # core's 0db7612e092b37c6ea04883a5a45f51c6d9ae433
+  # for details
   '//lib:velocity',
 ]
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java
index 2504937..c2f08ab 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/ItsHookModule.java
@@ -29,10 +29,6 @@
 import com.googlesource.gerrit.plugins.hooks.workflow.ActionController;
 import com.googlesource.gerrit.plugins.hooks.workflow.ActionRequest;
 import com.googlesource.gerrit.plugins.hooks.workflow.Condition;
-import com.googlesource.gerrit.plugins.hooks.workflow.GerritHookFilterAddComment;
-import com.googlesource.gerrit.plugins.hooks.workflow.GerritHookFilterAddRelatedLinkToChangeId;
-import com.googlesource.gerrit.plugins.hooks.workflow.GerritHookFilterAddRelatedLinkToGitWeb;
-import com.googlesource.gerrit.plugins.hooks.workflow.GerritHookFilterChangeState;
 import com.googlesource.gerrit.plugins.hooks.workflow.Property;
 import com.googlesource.gerrit.plugins.hooks.workflow.Rule;
 import com.googlesource.gerrit.plugins.hooks.workflow.action.AddComment;
@@ -57,14 +53,6 @@
         .annotatedWith(Exports.named("enabled"))
         .toInstance(new ItsHookEnabledConfigEntry(pluginName, pluginCfgFactory));
     bind(ItsConfig.class);
-    DynamicSet.bind(binder(), EventListener.class).to(
-        GerritHookFilterAddRelatedLinkToChangeId.class);
-    DynamicSet.bind(binder(), EventListener.class).to(
-        GerritHookFilterAddComment.class);
-    DynamicSet.bind(binder(), EventListener.class).to(
-        GerritHookFilterChangeState.class);
-    DynamicSet.bind(binder(), EventListener.class).to(
-        GerritHookFilterAddRelatedLinkToGitWeb.class);
     DynamicSet.bind(binder(), CommitValidationListener.class).to(
         ItsValidateComment.class);
     DynamicSet.bind(binder(), EventListener.class).to(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/its/InitIts.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/its/InitIts.java
index 85ab85f..ddb0e62 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/its/InitIts.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/its/InitIts.java
@@ -29,7 +29,7 @@
 
 public class InitIts implements InitStep {
 
-  public static String COMMENT_LINK_SECTION = "commentLink";
+  public static String COMMENT_LINK_SECTION = "commentlink";
 
   public static enum TrueFalseEnum {
     TRUE, FALSE;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/its/ItsConfig.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/its/ItsConfig.java
index 89bccf4..cdca3e8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/its/ItsConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/its/ItsConfig.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.events.ChangeAbandonedEvent;
@@ -31,24 +32,35 @@
 import com.google.gerrit.server.project.RefPatternMatcher;
 import com.google.inject.Inject;
 
+import com.googlesource.gerrit.plugins.hooks.validation.ItsAssociationPolicy;
+
+import java.util.regex.Pattern;
+
+import org.eclipse.jgit.lib.Config;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+
 public class ItsConfig {
   private static final Logger log = LoggerFactory.getLogger(ItsConfig.class);
 
   private final String pluginName;
   private final ProjectCache projectCache;
   private final PluginConfigFactory pluginCfgFactory;
+  private final Config gerritConfig;
 
   @Inject
   public ItsConfig(@PluginName String pluginName, ProjectCache projectCache,
-      PluginConfigFactory pluginCfgFactory) {
+      PluginConfigFactory pluginCfgFactory, @GerritServerConfig Config gerritConfig) {
     this.pluginName = pluginName;
     this.projectCache = projectCache;
     this.pluginCfgFactory = pluginCfgFactory;
+    this.gerritConfig = gerritConfig;
   }
 
+  // Plugin enablement --------------------------------------------------------
+
   public boolean isEnabled(Event event) {
     if (event instanceof PatchSetCreatedEvent) {
       PatchSetCreatedEvent e = (PatchSetCreatedEvent) event;
@@ -114,4 +126,68 @@
   private boolean match(String refName, String refPattern) {
     return RefPatternMatcher.getMatcher(refPattern).match(refName, null);
   }
+
+  // Issue association --------------------------------------------------------
+
+  /**
+   * Gets the name of the comment link that should be used
+   *
+   * @return name of the comment link that should be used
+   */
+  public String getCommentLinkName() {
+    String ret;
+
+    ret = gerritConfig.getString(pluginName, null, "commentlink");
+    if (ret == null) {
+      ret = pluginName;
+    }
+
+    return ret;
+  }
+
+  /**
+   * Gets the regular expression used to identify issue ids.
+   * <p>
+   * The index of the group that holds the issue id is
+   * {@link #getIssuePatternGroupIndex()}.
+   *
+   * @return the regular expression, or {@code null}, if there is no pattern
+   *    to match issue ids.
+   */
+  public Pattern getIssuePattern() {
+    Pattern ret = null;
+    String match = gerritConfig.getString("commentlink",
+        getCommentLinkName(), "match");
+    if (match != null) {
+      ret = Pattern.compile(match);
+    }
+    return ret;
+  }
+
+  /**
+   * Gets the index of the group in the issue pattern that holds the issue id.
+   * <p>
+   * The corresponding issue pattern is {@link #getIssuePattern()}
+   *
+   * @return the group index for {@link #getIssuePattern()} that holds the
+   *     issue id. The group index is guaranteed to be a valid group index.
+   */
+  public int getIssuePatternGroupIndex() {
+    Pattern pattern = getIssuePattern();
+    int groupCount = pattern.matcher("").groupCount();
+    int index = gerritConfig.getInt(pluginName, "commentlinkGroupIndex", 1);
+    if (index < 0 || index > groupCount) {
+      index = (groupCount == 0 ? 0 : 1);
+    }
+    return index;
+  }
+
+  /**
+   * Gets how necessary it is to associate commits with issues
+   * @return policy on how necessary association with issues is
+   */
+  public ItsAssociationPolicy getItsAssociationPolicy() {
+    return gerritConfig.getEnum("commentlink", getCommentLinkName(),
+        "association", ItsAssociationPolicy.OPTIONAL);
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractor.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractor.java
index 1d57c2a..45892d2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractor.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractor.java
@@ -1,16 +1,16 @@
 package com.googlesource.gerrit.plugins.hooks.util;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
+import com.googlesource.gerrit.plugins.hooks.its.ItsConfig;
+
 import org.apache.commons.lang.StringUtils;
-import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -23,19 +23,17 @@
   private static final Logger log = LoggerFactory.getLogger(
       IssueExtractor.class);
 
-  private final Config gerritConfig;
-  private final String pluginName;
   private final CommitMessageFetcher commitMessageFetcher;
   private final ReviewDb db;
+  private final ItsConfig itsConfig;
 
   @Inject
-  IssueExtractor(@GerritServerConfig Config gerritConfig,
-      @PluginName String pluginName, CommitMessageFetcher commitMessageFetcher,
+  IssueExtractor(ItsConfig itsConfig,
+      CommitMessageFetcher commitMessageFetcher,
       ReviewDb db) {
-    this.gerritConfig = gerritConfig;
-    this.pluginName = pluginName;
     this.commitMessageFetcher = commitMessageFetcher;
     this.db = db;
+    this.itsConfig = itsConfig;
   }
 
   /**
@@ -45,7 +43,7 @@
    * @return array of {@link String}. Each String being a found issue id.
    */
   public String[] getIssueIds(String haystack) {
-    Pattern pattern = getPattern();
+    Pattern pattern = itsConfig.getIssuePattern();
     if (pattern == null) return new String[] {};
 
     log.debug("Matching '" + haystack + "' against " + pattern.pattern());
@@ -53,29 +51,18 @@
     Set<String> issues = Sets.newHashSet();
     Matcher matcher = pattern.matcher(haystack);
 
+    int groupIdx = itsConfig.getIssuePatternGroupIndex();
     while (matcher.find()) {
-      int groupIdx = Math.min(matcher.groupCount(), 1);
-      issues.add(matcher.group(groupIdx));
+      String issueId = matcher.group(groupIdx);
+      if (!Strings.isNullOrEmpty(issueId)) {
+        issues.add(issueId);
+      }
     }
 
     return issues.toArray(new String[issues.size()]);
   }
 
   /**
-   * Gets the regular expression used to identify issue ids.
-   * @return the regular expression, or {@code null}, if there is no pattern
-   *    to match issue ids.
-   */
-  public Pattern getPattern() {
-    Pattern ret = null;
-    String match = gerritConfig.getString("commentLink", pluginName, "match");
-    if (match != null) {
-      ret = Pattern.compile(match);
-    }
-    return ret;
-  }
-
-  /**
    * Helper funcion for {@link #getIssueIds(String, String)}.
    * <p>
    * Adds a text's issues for a given occurrence to the map returned by
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/validation/ItsValidateComment.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/validation/ItsValidateComment.java
index a396ed9..190f061 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/validation/ItsValidateComment.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/hooks/validation/ItsValidateComment.java
@@ -16,7 +16,6 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.git.validators.CommitValidationException;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
@@ -27,7 +26,6 @@
 import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
 import com.googlesource.gerrit.plugins.hooks.util.IssueExtractor;
 
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.slf4j.Logger;
@@ -45,10 +43,6 @@
   @Inject
   private ItsFacade client;
 
-  @Inject
-  @GerritServerConfig
-  private Config gerritConfig;
-
   @Inject @PluginName
   private String pluginName;
 
@@ -60,7 +54,7 @@
 
   private List<CommitValidationMessage> validCommit(ReceiveCommand cmd, RevCommit commit) throws CommitValidationException {
     List<CommitValidationMessage> ret = Lists.newArrayList();
-    ItsAssociationPolicy associationPolicy = getItsAssociationPolicy();
+    ItsAssociationPolicy associationPolicy = itsConfig.getItsAssociationPolicy();
 
     switch (associationPolicy) {
       case MANDATORY:
@@ -118,7 +112,7 @@
           sb.append("Hint: insert one or more issue-id anywhere in the ");
           sb.append("commit message.\n");
           sb.append("      Issue-ids are strings matching ");
-          sb.append(issueExtractor.getPattern().pattern());
+          sb.append(itsConfig.getIssuePattern().pattern());
           sb.append("\n");
           sb.append("      and are pointing to existing tickets on ");
           sb.append(pluginName);
@@ -135,16 +129,11 @@
     return ret;
   }
 
-  private ItsAssociationPolicy getItsAssociationPolicy() {
-    return gerritConfig.getEnum("commentLink", pluginName, "association",
-        ItsAssociationPolicy.OPTIONAL);
-  }
-
   private CommitValidationMessage commitValidationFailure(
       String synopsis, String details) throws CommitValidationException {
     CommitValidationMessage ret =
         new CommitValidationMessage(synopsis + "\n" + details, false);
-    if (getItsAssociationPolicy() == ItsAssociationPolicy.MANDATORY) {
+    if (itsConfig.getItsAssociationPolicy() == ItsAssociationPolicy.MANDATORY) {
       throw new CommitValidationException(synopsis,
           Collections.singletonList(ret));
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilter.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilter.java
deleted file mode 100644
index d559bc3..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilter.java
+++ /dev/null
@@ -1,132 +0,0 @@
-// 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.googlesource.gerrit.plugins.hooks.workflow;
-
-import com.google.gerrit.common.EventListener;
-import com.google.gerrit.server.events.ChangeAbandonedEvent;
-import com.google.gerrit.server.events.ChangeMergedEvent;
-import com.google.gerrit.server.events.ChangeRestoredEvent;
-import com.google.gerrit.server.events.CommentAddedEvent;
-import com.google.gerrit.server.events.Event;
-import com.google.gerrit.server.events.PatchSetCreatedEvent;
-import com.google.gerrit.server.events.RefUpdatedEvent;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-
-import com.googlesource.gerrit.plugins.hooks.its.ItsConfig;
-import com.googlesource.gerrit.plugins.hooks.util.CommitMessageFetcher;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-
-public class GerritHookFilter implements EventListener {
-  private static final Logger log = LoggerFactory.getLogger(GerritHookFilter.class);
-
-  @Inject
-  private CommitMessageFetcher commitMessageFetcher;
-
-  @Inject
-  private ItsConfig itsConfig;
-
-  public String getComment(String projectName, String commitId)
-      throws IOException {
-    return commitMessageFetcher.fetch(projectName, commitId);
-  }
-
-  /**
-   * Filter patch set created event.
-   * @param hook
-   * @throws IOException
-   * @throws OrmException
-   */
-  public void doFilter(PatchSetCreatedEvent hook) throws IOException,
-      OrmException {
-  }
-
-  /**
-   * Filter comment added event.
-   * @param hook
-   * @throws IOException
-   */
-  public void doFilter(CommentAddedEvent hook) throws IOException {
-  }
-
-  /**
-   * Filter change merged event.
-   * @param hook
-   * @throws IOException
-   */
-  public void doFilter(ChangeMergedEvent hook) throws IOException {
-  }
-
-  /**
-   * Filter change abandoned event.
-   * @param changeAbandonedHook
-   * @throws IOException
-   */
-  public void doFilter(ChangeAbandonedEvent changeAbandonedHook)
-      throws IOException {
-  }
-
-  /**
-   * Filter change restored event.
-   * @param changeRestoredHook
-   * @throws IOException
-   */
-  public void doFilter(ChangeRestoredEvent changeRestoredHook)
-      throws IOException {
-  }
-
-  /**
-   * Filter ref updated event.
-   * @param refUpdatedHook
-   * @throws IOException
-   */
-  public void doFilter(RefUpdatedEvent refUpdatedHook) throws IOException {
-  }
-
-  @Override
-  public void onEvent(Event event) {
-    if (!itsConfig.isEnabled(event)) {
-      return;
-    }
-
-    try {
-      if (event instanceof PatchSetCreatedEvent) {
-        doFilter((PatchSetCreatedEvent) event);
-      } else if (event instanceof CommentAddedEvent) {
-        doFilter((CommentAddedEvent) event);
-      } else if (event instanceof ChangeMergedEvent) {
-        doFilter((ChangeMergedEvent) event);
-      } else if (event instanceof ChangeAbandonedEvent) {
-        doFilter((ChangeAbandonedEvent) event);
-      } else if (event instanceof ChangeRestoredEvent) {
-        doFilter((ChangeRestoredEvent) event);
-      } else if (event instanceof RefUpdatedEvent) {
-        doFilter((RefUpdatedEvent) event);
-      } else {
-        log.debug("Event " + event + " not recognised and ignored");
-      }
-    } catch (Throwable e) {
-      log.error("Event " + event + " processing failed", e);
-    }
-  }
-
-  public String getUrl(PatchSetCreatedEvent hook) {
-    return null;
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddComment.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddComment.java
deleted file mode 100644
index 8f2d873..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddComment.java
+++ /dev/null
@@ -1,180 +0,0 @@
-// 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.googlesource.gerrit.plugins.hooks.workflow;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.server.config.AnonymousCowardName;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.data.AccountAttribute;
-import com.google.gerrit.server.data.ApprovalAttribute;
-import com.google.gerrit.server.data.ChangeAttribute;
-import com.google.gerrit.server.events.ChangeAbandonedEvent;
-import com.google.gerrit.server.events.ChangeEvent;
-import com.google.gerrit.server.events.ChangeMergedEvent;
-import com.google.gerrit.server.events.ChangeRestoredEvent;
-import com.google.gerrit.server.events.CommentAddedEvent;
-import com.google.inject.Inject;
-
-import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
-import com.googlesource.gerrit.plugins.hooks.util.IssueExtractor;
-
-import org.eclipse.jgit.lib.Config;
-
-import java.io.IOException;
-
-public class GerritHookFilterAddComment extends GerritHookFilter  {
-
-  @Inject
-  private ItsFacade its;
-
-  @Inject @AnonymousCowardName
-  private String anonymousCowardName;
-
-  @Inject
-  @GerritServerConfig
-  private Config gerritConfig;
-
-  @Inject
-  private IssueExtractor issueExtractor;
-
-  @Inject @PluginName
-  private String pluginName;
-
-  @Override
-  public void doFilter(CommentAddedEvent hook) throws IOException {
-    if (!(gerritConfig.getBoolean(pluginName, null, "commentOnCommentAdded",
-        true))) {
-      return;
-    }
-
-    String comment = getComment(hook);
-    addComment(hook.change, comment);
-  }
-
-  @Override
-  public void doFilter(ChangeMergedEvent hook) throws IOException {
-    if (!(gerritConfig.getBoolean(pluginName, null, "commentOnChangeMerged",
-        true))) {
-      return;
-    }
-
-    String comment = getComment(hook);
-    addComment(hook.change, comment);
-  }
-
-  @Override
-  public void doFilter(ChangeAbandonedEvent hook) throws IOException {
-    if (!(gerritConfig.getBoolean(pluginName, null, "commentOnChangeAbandoned",
-        true))) {
-      return;
-    }
-    String comment = getComment(hook);
-    addComment(hook.change, comment);
-  }
-
-  @Override
-  public void doFilter(ChangeRestoredEvent hook) throws IOException {
-    if (!(gerritConfig.getBoolean(pluginName, null, "commentOnChangeRestored",
-        true))) {
-      return;
-    }
-    String comment = getComment(hook);
-    addComment(hook.change, comment);
-  }
-
-  private String getCommentPrefix(ChangeAttribute change) {
-    return getChangeIdUrl(change) + " | ";
-  }
-
-  private String formatAccountAttribute(AccountAttribute who) {
-    if (who != null && !Strings.isNullOrEmpty(who.name)) {
-      return who.name;
-    }
-    return anonymousCowardName;
-  }
-
-  private String getComment(ChangeAttribute change, ChangeEvent hook, AccountAttribute who, String what) {
-    return getCommentPrefix(change) + "change " + what + " [by "
-        + formatAccountAttribute(who) + "]";
-  }
-
-  private String getComment(ChangeRestoredEvent hook) {
-    return getComment(hook.change, hook, hook.restorer, "RESTORED");
-  }
-
-  private String getComment(ChangeAbandonedEvent hook) {
-    return getComment(hook.change, hook, hook.abandoner, "ABANDONED");
-  }
-
-  private String getComment(ChangeMergedEvent hook) {
-    return getComment(hook.change, hook, hook.submitter, "APPROVED and MERGED");
-  }
-
-  private String getChangeIdUrl(ChangeAttribute change) {
-    final String url = change.url;
-    String changeId = change.id;
-    return its.createLinkForWebui(url, "Gerrit Change " + changeId);
-  }
-
-  private String getComment(CommentAddedEvent commentAdded) {
-    StringBuilder comment = new StringBuilder(getCommentPrefix(commentAdded.change));
-
-    if (commentAdded.approvals != null && commentAdded.approvals.length > 0) {
-      comment.append("Code-Review: ");
-      for (ApprovalAttribute approval : commentAdded.approvals) {
-        String value = getApprovalValue(approval);
-        if (value != null) {
-          comment.append(getApprovalType(approval) + ":" + value + " ");
-        }
-      }
-    }
-
-    comment.append(commentAdded.comment + " ");
-    comment.append("[by " + formatAccountAttribute(commentAdded.author) + "]");
-    return comment.toString();
-  }
-
-  private String getApprovalValue(ApprovalAttribute approval) {
-    if (approval.value.equals("0")) {
-      return null;
-    }
-
-    if (approval.value.charAt(0) != '-') {
-      return "+" + approval.value;
-    } else {
-      return approval.value;
-    }
-  }
-
-  private String getApprovalType(ApprovalAttribute approval) {
-    if (approval.type.equalsIgnoreCase("CRVW")) {
-      return "Reviewed";
-    } else if (approval.type.equalsIgnoreCase("VRIF")) {
-      return "Verified";
-    } else
-      return approval.type;
-  }
-
-  private void addComment(ChangeAttribute change, String comment)
-      throws IOException {
-    String gitComment = change.subject;
-    String[] issues = issueExtractor.getIssueIds(gitComment);
-
-    for (String issue : issues) {
-      its.addComment(issue, comment);
-    }
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToChangeId.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToChangeId.java
deleted file mode 100644
index 19ba344..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToChangeId.java
+++ /dev/null
@@ -1,148 +0,0 @@
-// 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.googlesource.gerrit.plugins.hooks.workflow;
-
-import com.google.common.collect.Lists;
-import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.events.PatchSetCreatedEvent;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import com.google.inject.Inject;
-
-import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
-import com.googlesource.gerrit.plugins.hooks.util.IssueExtractor;
-
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.net.URL;
-import java.util.Iterator;
-import java.util.List;
-
-public class GerritHookFilterAddRelatedLinkToChangeId extends
-    GerritHookFilter  {
-
-  Logger log = LoggerFactory
-      .getLogger(GerritHookFilterAddRelatedLinkToChangeId.class);
-
-  @Inject
-  private ItsFacade its;
-
-  @Inject
-  @GerritServerConfig
-  private Config gerritConfig;
-
-  @Inject
-  private IssueExtractor issueExtractor;
-
-  @Inject
-  private ReviewDb db;
-
-  @Inject @PluginName
-  private String pluginName;
-
-
-  /**
-   * Filter issues to those that occur for the first time in a change
-   *
-   * @param issues The issues to filter.
-   * @param patchSet Filter for this patch set.
-   * @return the issues that occur for the first time.
-   * @throws IOException
-   * @throws OrmException
-   */
-  private List<String> filterForFirstLinkedIssues(String[] issues,
-      PatchSetCreatedEvent patchSet) throws IOException, OrmException {
-    List<String> ret = Lists.newArrayList(issues);
-    int patchSetNumberCurrent = Integer.parseInt(patchSet.patchSet.number);
-
-    if (patchSetNumberCurrent > 1) {
-      String project = patchSet.change.project;
-      int changeNumber = Integer.parseInt(patchSet.change.number);
-      Change.Id changeId = new Change.Id(changeNumber);
-
-      // It would be nice to get patch sets directly via
-      //   patchSetCreated.change.patchSets
-      // but it turns out that it's null for our events. So we fetch the patch
-      // sets from the db instead.
-      ResultSet<PatchSet> patchSets = db.patchSets().byChange(changeId);
-      Iterator<PatchSet> patchSetIter = patchSets.iterator();
-
-      while (!ret.isEmpty() && patchSetIter.hasNext()) {
-        PatchSet previousPatchSet = patchSetIter.next();
-        if (previousPatchSet.getPatchSetId() < patchSetNumberCurrent) {
-          String commitMessage = getComment(project,
-              previousPatchSet.getRevision().get());
-          for (String issue : issueExtractor.getIssueIds(commitMessage)) {
-            ret.remove(issue);
-          }
-        }
-      }
-    }
-    return ret;
-  }
-
-  @Override
-  public void doFilter(PatchSetCreatedEvent patchsetCreated)
-      throws IOException, OrmException {
-    boolean addPatchSetComment = gerritConfig.getBoolean(pluginName, null,
-        "commentOnPatchSetCreated", true);
-
-    boolean addChangeComment = "1".equals(patchsetCreated.patchSet.number) &&
-        gerritConfig.getBoolean(pluginName, null, "commentOnChangeCreated",
-            false);
-
-    boolean addFirstLinkedPatchSetComment = gerritConfig.getBoolean(pluginName,
-        null, "commentOnFirstLinkedPatchSetCreated", false);
-
-    if (addPatchSetComment || addFirstLinkedPatchSetComment || addChangeComment) {
-      String gitComment =
-          getComment(patchsetCreated.change.project,
-              patchsetCreated.patchSet.revision);
-
-      String[] issues = issueExtractor.getIssueIds(gitComment);
-
-      List<String> firstLinkedIssues = null;
-      if (addFirstLinkedPatchSetComment) {
-        firstLinkedIssues = filterForFirstLinkedIssues(issues, patchsetCreated);
-      }
-
-      for (String issue : issues) {
-        if (addChangeComment) {
-          its.addRelatedLink(issue, new URL(patchsetCreated.change.url),
-              "Gerrit Change " + patchsetCreated.change.id);
-        }
-
-        if (addPatchSetComment) {
-          its.addRelatedLink(issue, new URL(patchsetCreated.change.url),
-              "Gerrit Patch-Set " + patchsetCreated.change.id + "/"
-                  + patchsetCreated.patchSet.number);
-        }
-
-        if (addFirstLinkedPatchSetComment && firstLinkedIssues.contains(issue)) {
-          its.addRelatedLink(issue, new URL(patchsetCreated.change.url),
-              "Gerrit Patch-Set " + patchsetCreated.change.id + "/"
-                  + patchsetCreated.patchSet.number);
-        }
-      }
-    }
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToGitWeb.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToGitWeb.java
deleted file mode 100644
index 9e806d2..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterAddRelatedLinkToGitWeb.java
+++ /dev/null
@@ -1,124 +0,0 @@
-// 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.googlesource.gerrit.plugins.hooks.workflow;
-
-import com.google.gerrit.common.data.GitWebType;
-import com.google.gerrit.common.data.ParameterizedString;
-import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.httpd.GitWebConfig;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.events.RefUpdatedEvent;
-import com.google.inject.Inject;
-
-import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
-import com.googlesource.gerrit.plugins.hooks.util.IssueExtractor;
-
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLEncoder;
-import java.util.HashMap;
-import java.util.Map;
-
-public class GerritHookFilterAddRelatedLinkToGitWeb extends GerritHookFilter {
-
-  Logger log = LoggerFactory
-      .getLogger(GerritHookFilterAddRelatedLinkToGitWeb.class);
-
-  @Inject
-  @GerritServerConfig
-  private Config gerritConfig;
-
-  @Inject
-  private ItsFacade its;
-
-  @Inject
-  private GitWebConfig gitWebConfig;
-
-  @Inject
-  private IssueExtractor issueExtractor;
-
-  @Inject @PluginName
-  private String pluginName;
-
-  @Override
-  public void doFilter(RefUpdatedEvent hook) throws IOException {
-    if (!(gerritConfig.getBoolean(pluginName, null, "commentOnRefUpdatedGitWeb",
-        true))) {
-      return;
-    }
-
-    String gitComment = getComment(hook.refUpdate.project,  hook.refUpdate.newRev);
-    log.debug("Git commit " + hook.refUpdate.newRev + ": " + gitComment);
-
-    URL gitUrl = getGitUrl(hook);
-    if (gitUrl == null) {
-      return;
-    }
-    String[] issues = issueExtractor.getIssueIds(gitComment);
-
-    for (String issue : issues) {
-      log.debug("Adding GitWeb URL " + gitUrl + " to issue " + issue);
-
-      its.addRelatedLink(issue, gitUrl, "Git: "
-          + hook.refUpdate.newRev);
-    }
-  }
-
-
-  /**
-   * generates the URL to GitWeb for the event
-   *
-   * @return if null is returned, the configuration does not allow to come up
-   * with a GitWeb url. In that case, a message describing the problematic
-   * setting has been logged.
-   */
-  private URL getGitUrl(RefUpdatedEvent hook) throws MalformedURLException,
-      UnsupportedEncodingException {
-    String gerritCanonicalUrl =
-        gerritConfig.getString("gerrit", null, "canonicalWebUrl");
-    if (gerritCanonicalUrl == null) {
-      log.info( "No canonicalWebUrl configured. Skipping GitWeb link generation");
-      return null;
-    }
-    if(!gerritCanonicalUrl.endsWith("/")) {
-      gerritCanonicalUrl += "/";
-    }
-
-    String gitWebUrl = gitWebConfig.getUrl();
-    if (gitWebUrl == null) {
-      log.info( "No url for GitWeb found. Skipping GitWeb link generation");
-      return null;
-    }
-    if (!gitWebUrl.startsWith("http")) {
-      gitWebUrl = gerritCanonicalUrl + gitWebUrl;
-    }
-
-    GitWebType gitWebType = gitWebConfig.getGitWebType();
-    String revUrl = gitWebType.getRevision();
-
-    ParameterizedString pattern = new ParameterizedString(revUrl);
-    final Map<String, String> p = new HashMap<>();
-    p.put("project", URLEncoder.encode(
-        gitWebType.replacePathSeparator(hook.refUpdate.project), "US-ASCII"));
-    p.put("commit", hook.refUpdate.newRev);
-    return new URL(gitWebUrl + pattern.replace(p));
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterChangeState.java b/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterChangeState.java
deleted file mode 100644
index d14278c..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/hooks/workflow/GerritHookFilterChangeState.java
+++ /dev/null
@@ -1,271 +0,0 @@
-// 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.googlesource.gerrit.plugins.hooks.workflow;
-
-
-import com.google.gerrit.server.config.SitePath;
-import com.google.gerrit.server.data.ApprovalAttribute;
-import com.google.gerrit.server.data.ChangeAttribute;
-import com.google.gerrit.server.events.ChangeAbandonedEvent;
-import com.google.gerrit.server.events.ChangeMergedEvent;
-import com.google.gerrit.server.events.ChangeRestoredEvent;
-import com.google.gerrit.server.events.CommentAddedEvent;
-import com.google.gerrit.server.events.PatchSetCreatedEvent;
-import com.google.inject.Inject;
-
-import com.googlesource.gerrit.plugins.hooks.its.InvalidTransitionException;
-import com.googlesource.gerrit.plugins.hooks.its.ItsFacade;
-import com.googlesource.gerrit.plugins.hooks.util.IssueExtractor;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.util.FS;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
-public class GerritHookFilterChangeState extends GerritHookFilter {
-  private static final Logger log = LoggerFactory
-      .getLogger(GerritHookFilterChangeState.class);
-
-  @Inject
-  private ItsFacade its;
-
-  @Inject
-  @SitePath
-  private Path sitePath;
-
-  @Inject
-  private IssueExtractor issueExtractor;
-
-  @Override
-  public void doFilter(PatchSetCreatedEvent hook) throws IOException {
-    performAction(hook.change, new Condition("change", "created"));
-  }
-
-  @Override
-  public void doFilter(CommentAddedEvent hook) throws IOException {
-    try {
-      List<Condition> conditions = new ArrayList<>();
-      conditions.add(new Condition("change", "commented"));
-
-      if (hook.approvals != null) {
-        for (ApprovalAttribute approval : hook.approvals) {
-          addApprovalCategoryCondition(conditions, approval.type, approval.value);
-        }
-      }
-
-      performAction(hook.change,
-          conditions.toArray(new Condition[conditions.size()]));
-    } catch (InvalidTransitionException ex) {
-      log.warn(ex.getMessage());
-    }
-  }
-
-  @Override
-  public void doFilter(ChangeMergedEvent hook) throws IOException {
-    performAction(hook.change, new Condition("change", "merged"));
-  }
-
-  @Override
-  public void doFilter(ChangeAbandonedEvent hook) throws IOException {
-    performAction(hook.change, new Condition("change", "abandoned"));
-  }
-
-  @Override
-  public void doFilter(ChangeRestoredEvent hook) throws IOException {
-    performAction(hook.change, new Condition("change", "restored"));
-  }
-
-  private void addApprovalCategoryCondition(List<Condition> conditions,
-      String name, String value) {
-    value = toConditionValue(value);
-    if (value == null) return;
-
-    conditions.add(new Condition(name, value));
-  }
-
-  private String toConditionValue(String text) {
-    if (text == null) return null;
-
-    try {
-      int val = Integer.parseInt(text);
-      if (val > 0)
-        return "+" + val;
-      else
-        return text;
-    } catch (Exception any) {
-      return null;
-    }
-  }
-
-  private void performAction(ChangeAttribute change, Condition... conditionArgs)
-      throws IOException {
-
-    List<Condition> conditions = Arrays.asList(conditionArgs);
-
-    log.debug("Checking suitable transition for: " + conditions);
-
-    Transition transition = null;
-    List<Transition> transitions = loadTransitions();
-    for (Transition tx : transitions) {
-
-      log.debug("Checking transition: " + tx);
-      if (tx.matches(conditions)) {
-        log.debug("Transition FOUND > " + tx.getAction());
-        transition = tx;
-        break;
-      }
-    }
-
-    if (transition == null) {
-      log.debug("Nothing to perform, transition not found for conditions "
-          + conditions);
-      return;
-    }
-
-    String gitComment = change.subject;
-    String[] issues = issueExtractor.getIssueIds(gitComment);
-
-    for (String issue : issues) {
-      its.performAction(issue, transition.getAction());
-    }
-  }
-
-  private List<Transition> loadTransitions() {
-    File configFile = new File(sitePath.toFile(), "etc/issue-state-transition.config");
-    FileBasedConfig cfg = new FileBasedConfig(configFile, FS.DETECTED);
-    try {
-      cfg.load();
-    } catch (IOException e) {
-      log.error("Cannot load transitions configuration file " + cfg, e);
-      return Collections.emptyList();
-    } catch (ConfigInvalidException e) {
-      log.error("Invalid transitions configuration file" + cfg, e);
-      return Collections.emptyList();
-    }
-
-    List<Transition> transitions = new ArrayList<>();
-    Set<String> sections = cfg.getSubsections("action");
-    for (String section : sections) {
-      List<Condition> conditions = new ArrayList<>();
-      Set<String> keys = cfg.getNames("action", section);
-      for (String key : keys) {
-        String val = cfg.getString("action", section, key);
-        conditions.add(new Condition(key.trim(), val.trim().split(",")));
-      }
-      transitions.add(new Transition(toAction(section), conditions));
-    }
-    return transitions;
-  }
-
-  private String toAction(String name) {
-    name = name.trim();
-    try {
-      int i = name.lastIndexOf(' ');
-      Integer.parseInt(name.substring(i + 1));
-      name = name.substring(0, i);
-    } catch (Exception ignore) {
-    }
-    return name;
-  }
-
-  public class Condition {
-    private String key;
-    private String[] val;
-
-    public Condition(String key, String[] values) {
-      super();
-      this.key = key.toLowerCase();
-      this.val = values;
-    }
-
-    public Condition(String key, String value) {
-      this(key, new String[] {value});
-    }
-
-    public String getKey() {
-      return key;
-    }
-
-    public String[] getVal() {
-      return val;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      try {
-        Condition other = (Condition) o;
-        if (!(key.equals(other.key))) return false;
-
-        boolean valMatch = false;
-        List<String> otherVals = Arrays.asList(other.val);
-        for (String value : val) {
-          if (otherVals.contains(value)) valMatch = true;
-        }
-
-        return valMatch;
-      } catch (Exception any) {
-        return false;
-      }
-    }
-
-    @Override
-    public String toString() {
-      return key + "=" + Arrays.asList(val);
-    }
-  }
-
-  public class Transition {
-    private String action;
-    private List<Condition> conditions;
-
-    public Transition(String action, List<Condition> conditions) {
-      super();
-      this.action = action;
-      this.conditions = conditions;
-    }
-
-    public String getAction() {
-      return action;
-    }
-
-    public List<Condition> getCondition() {
-      return conditions;
-    }
-
-    public boolean matches(List<Condition> eventConditions) {
-
-      for (Condition condition : conditions) {
-        if (!eventConditions.contains(condition)) return false;
-      }
-
-      return true;
-    }
-
-    @Override
-    public String toString() {
-      return "action=\"" + action + "\", conditions=" + conditions;
-    }
-  }
-}
diff --git a/src/main/resources/Documentation/config-common.md b/src/main/resources/Documentation/config-common.md
index 13720b4..9e2c033 100644
--- a/src/main/resources/Documentation/config-common.md
+++ b/src/main/resources/Documentation/config-common.md
@@ -5,7 +5,7 @@
 * [Identifying ITS ids][identifying-its-ids]
 * [Enabling ITS integration][enabling-its-integration]
 * [Configuring rules of when to take which actions in the ITS][configure-rules]
-* [Legacy configuration][legacy-configuration]
+* [Further common configuration details][config-common-detail]
 
 
 
@@ -14,15 +14,16 @@
 -----------------------------------------------------
 
 In order to extract ITS ids from commit messages, @PLUGIN@ uses
-[commentlink][upstream-comment-link-doc]s of name "`@PLUGIN@`".
+[commentlink][upstream-comment-link-doc]s of
+([per default][common-config-commentlink]) name "`@PLUGIN@`".
 
-The first group of `commentlink.@PLUGIN@.match` is considered the
-issue id.
+The ([per default][common-config-commentlinkGroupIndex]) first group of
+`commentlink.@PLUGIN@.match` is considered the issue id.
 
 So for example having
 
 ```
-[commentLink "@PLUGIN@"]
+[commentlink "@PLUGIN@"]
     match = [Bb][Uu][Gg][ ]*([1-9][0-9]*)
     html = "<a href=\"http://my.issure.tracker.example.org/show_bug.cgi?id=$1\">(bug $1)</a>"
     association = SUGGESTED
@@ -51,6 +52,8 @@
 :	 Bug-ids are liked when found in the git commit message, no warning is
 	 displayed otherwise.
 
+[upstream-comment-link-doc]: ../../../Documentation/config-gerrit.html#commentlink
+
 
 
 [enabling-its-integration]: #enabling-its-integration
@@ -142,66 +145,29 @@
 
 
 
+[config-common-detail]: #config-common-detail
+<a name="config-common-detail">Further common configuration details</a>
+-----------------------------------------------------------------------
 
-[legacy-configuration]: #legacy-configuration
-<a name="legacy-configuration">Legacy configuration</a>
--------------------------------------------------------
+[common-config-commentlink]: #common-config-commentlink
+[common-config-commentlinkGroupIndex]: #common-config-commentlinkGroupIndex
 
-In this section we present the legacy configuration that uses
-`etc/gerrit.config` directly. As this legacy part will be removed at
-some point, please upgrade to the rule [rule based
-approach][rule-base].
+<a name="common-config-commentlink">`@PLUGIN@.commentlink`
+:   The name of the comment link to use to extract issue ids.
 
-The following configuration settings are available:
+    This setting is useful to reuse the same comment link from different Its
+    plugins. For example, if you set `@PLUGIN@.commentlink` to `foo`, then the
+    comment link `foo` is used (instead of the comment link `@PLUGIN@`) to
+    extract issue ids.
 
-`@PLUGIN@.commentOnChangeAbandoned`
-:	If true, abandoning a change adds an ITS comment to the change's
-	associated issue.
+    Default is `@PLUGIN@`
 
-	Default is `true`.
+<a name="common-config-commentlinkGroupIndex">`@PLUGIN@.commentlinkGroupIndex`
+:   The group index within `@PLUGIN@.commentlink` that holds the issue id.
 
-`@PLUGIN@.commentOnChangeCreated`
-:	If true, creating a change adds an ITS comment to the change's
-	associated issue.
-
-	Default is `false`.
-
-`@PLUGIN@.commentOnChangeMerged`
-:	If true, merging a change's patch set adds an ITS comment to
-	the change's associated issue.
-
-	Default is `true`.
-
-`@PLUGIN@.commentOnChangeRestored`
-:	If true, restoring an abandoned change adds an ITS comment to
-	the change's associated issue.
-
-	Default is `true`.
-
-`@PLUGIN@.commentOnCommentAdded`
-:	If true, adding a comment and/or review to a change in Gerrit
-	adds an ITS comment to the change's associated issue.
-
-	Default is `true`.
-
-`@PLUGIN@.commentOnFirstLinkedPatchSetCreated`
-:	If true, creating a patch set for a change adds an ITS comment
-	to the change's associated issue, if the issue has not been
-	mentioned in previous patch sets of the same change.
-
-	Default is `false`.
-
-`@PLUGIN@.commentOnPatchSetCreated`
-:	If true, creating a patch set for a change adds an ITS comment
-	to the change's associated issue.
-
-	Default is `true`.
-
-`@PLUGIN@.commentOnRefUpdatedGitWeb`
-:	If true, updating a ref adds a GitWeb link to the associated
-	issue.
-
-	Default is `true`.
+    Default is `1`, if there are are groups within the regular expression for
+    the `@PLUGIN@.commentlink` comment link, and the default is `0`, if there
+    are no such groups.
 
 [Back to @PLUGIN@ documentation index][index]
 
diff --git a/src/test/java/com/googlesource/gerrit/plugins/hooks/its/ItsConfigTest.java b/src/test/java/com/googlesource/gerrit/plugins/hooks/its/ItsConfigTest.java
index 72d229c..70d3480 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/hooks/its/ItsConfigTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/hooks/its/ItsConfigTest.java
@@ -15,131 +15,673 @@
 package com.googlesource.gerrit.plugins.hooks.its;
 
 import static org.easymock.EasyMock.expect;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.PluginConfig;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
 
-import org.easymock.EasyMockSupport;
-import org.junit.Before;
-import org.junit.Test;
+import com.googlesource.gerrit.plugins.hooks.testutil.LoggingMockingTestCase;
+import com.googlesource.gerrit.plugins.hooks.validation.ItsAssociationPolicy;
 
-import java.util.ArrayList;
+import org.eclipse.jgit.lib.Config;
 
-public class ItsConfigTest {
-  private EasyMockSupport easyMock;
-  private String pluginName = "its-base";
-  private PluginConfig pluginConfig;
-  private PluginConfig pluginConfigWithInheritance;
-  private ItsConfig itsConfig;
-  private PatchSetCreatedEvent pscEvent;
+import java.util.Arrays;
 
-  @Before
-  public void setup() {
-    easyMock = new EasyMockSupport();
-    ProjectCache projectCache = easyMock.createNiceMock(ProjectCache.class);
-    PluginConfigFactory pluginCfgFactory = easyMock.createNiceMock(PluginConfigFactory.class);
-    ProjectState projectState = easyMock.createNiceMock(ProjectState.class);
-    pluginConfig = easyMock.createNiceMock(PluginConfig.class);
-    pluginConfigWithInheritance = easyMock.createNiceMock(PluginConfig.class);
-    itsConfig = new ItsConfig(pluginName, projectCache, pluginCfgFactory);
+public class ItsConfigTest extends LoggingMockingTestCase {
+  private Injector injector;
 
-    pscEvent = new PatchSetCreatedEvent();
-    pscEvent.change = new ChangeAttribute();
-    pscEvent.change.project = "testProject";
-    pscEvent.change.branch = "testBranch";
-    ArrayList<ProjectState> listProjectState = new ArrayList<>(1);
-    listProjectState.add(projectState);
+  private ProjectCache projectCache;
+  private PluginConfigFactory pluginConfigFactory;
+  private Config serverConfig;
 
-    expect(projectCache.get(new Project.NameKey("testProject"))).andReturn(
-        projectState).once();
-    expect(projectState.treeInOrder()).andReturn(listProjectState);
-    expect(pluginCfgFactory.getFromProjectConfig(projectState, pluginName))
-    .andStubReturn(pluginConfig);
-    expect(
-        pluginCfgFactory.getFromProjectConfigWithInheritance(projectState,
-            pluginName)).andStubReturn(pluginConfigWithInheritance);
+  public void setupIsEnabled(String enabled, String parentEnabled,
+      String[] branches) {
+    ProjectState projectState = createMock(ProjectState.class);
+
+    expect(projectCache.get(new Project.NameKey("testProject")))
+        .andReturn(projectState).anyTimes();
+    expect(projectCache.get(new Project.NameKey("parentProject")))
+        .andReturn(projectState).anyTimes();
+
+    Iterable<ProjectState> parents;
+    if (parentEnabled == null) {
+      parents = Arrays.asList(projectState);
+    } else {
+      ProjectState parentProjectState = createMock(ProjectState.class);
+
+      PluginConfig parentPluginConfig = createMock(PluginConfig.class);
+
+      expect(pluginConfigFactory.getFromProjectConfig(
+          parentProjectState, "ItsTestName")).andReturn(parentPluginConfig);
+
+      expect(parentPluginConfig.getString("enabled")).andReturn(parentEnabled)
+          .anyTimes();
+
+      PluginConfig parentPluginConfigWI = createMock(PluginConfig.class);
+
+      expect(pluginConfigFactory.getFromProjectConfigWithInheritance(
+          parentProjectState, "ItsTestName")).andReturn(parentPluginConfigWI)
+          .anyTimes();
+
+      String[] parentBranches = { "refs/heads/testBranch" };
+      expect(parentPluginConfigWI.getStringList("branch"))
+          .andReturn(parentBranches).anyTimes();
+
+      parents = Arrays.asList(parentProjectState, projectState);
+    }
+    expect(projectState.treeInOrder()).andReturn(parents);
+
+    PluginConfig pluginConfig = createMock(PluginConfig.class);
+
+    expect(pluginConfigFactory.getFromProjectConfig(
+        projectState, "ItsTestName")).andReturn(pluginConfig).anyTimes();
+
+    expect(pluginConfig.getString("enabled")).andReturn(enabled).anyTimes();
+
+    PluginConfig pluginConfigWI = createMock(PluginConfig.class);
+
+    expect(pluginConfigFactory.getFromProjectConfigWithInheritance(
+        projectState, "ItsTestName")).andReturn(pluginConfigWI).anyTimes();
+
+    expect(pluginConfigWI.getBoolean("enabled", false))
+        .andReturn("true".equals(enabled)).anyTimes();
+
+    expect(pluginConfigWI.getStringList("branch")).andReturn(branches)
+        .anyTimes();
   }
 
-  @Test
-  public void testEnforcedWithRegex() {
-    String[] refPatterns = {"^refs/heads/test.*"};
-    setUpEnforced(refPatterns);
+  public void testIsEnabledRefNoParentNoBranchEnabled() {
+    String[] branches = {};
+    setupIsEnabled("true", null, branches);
 
-    assertTrue(itsConfig.isEnabled(pscEvent));
-    easyMock.verifyAll();
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertTrue(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
   }
 
-  @Test
-  public void testEnforcedWithExact() {
-    String[] refPatterns = {"refs/heads/testBranch"};
-    setUpEnforced(refPatterns);
+  public void testIsEnabledRefNoParentNoBranchDisabled() {
+    String[] branches = {};
+    setupIsEnabled("false", null, branches);
 
-    assertTrue(itsConfig.isEnabled(pscEvent));
-    easyMock.verifyAll();
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertFalse(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
   }
 
-  @Test
-  public void testEnforcedForAll() {
-    String[] refPatterns = new String[0];
-    setUpEnforced(refPatterns);
+  public void testIsEnabledRefNoParentNoBranchEnforced() {
+    String[] branches = {};
+    setupIsEnabled("enforced", null, branches);
 
-    assertTrue(itsConfig.isEnabled(pscEvent));
-    easyMock.verifyAll();
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertTrue(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
   }
 
-  @Test
-  public void testEnabledWithRegex() {
-    String[] refPatterns = {"^refs/heads/test.*"};
-    setUpEnabled(refPatterns);
+  public void testIsEnabledRefNoParentMatchingBranchEnabled() {
+    String[] branches = {"^refs/heads/test.*"};
+    setupIsEnabled("true", null, branches);
 
-    assertTrue(itsConfig.isEnabled(pscEvent));
-    easyMock.verifyAll();
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertTrue(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
   }
 
-  @Test
-  public void testEnabledWithExact() {
-    String[] refPatterns = {"refs/heads/testBranch"};
-    setUpEnabled(refPatterns);
+  public void testIsEnabledRefNoParentMatchingBranchDisabled() {
+    String[] branches = {"^refs/heads/test.*"};
+    setupIsEnabled("false", null, branches);
 
-    assertTrue(itsConfig.isEnabled(pscEvent));
-    easyMock.verifyAll();
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertFalse(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
   }
 
-  @Test
-  public void testDisabled() {
-    String[] refPatterns = {"refs/heads/testBranch1"};
-    setUpEnabled(refPatterns);
+  public void testIsEnabledRefNoParentMatchingBranchEnforced() {
+    String[] branches = {"^refs/heads/test.*"};
+    setupIsEnabled("enforced", null, branches);
 
-    assertFalse(itsConfig.isEnabled(pscEvent));
-    easyMock.verifyAll();
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertTrue(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
   }
 
-  @Test
-  public void testInvalidPattern() {
-    String[] refPatterns = {"testBranch"};
-    setUpEnabled(refPatterns);
+  public void testIsEnabledRefNoParentNonMatchingBranchEnabled() {
+    String[] branches = {"^refs/heads/foo.*"};
+    setupIsEnabled("true", null, branches);
 
-    assertFalse(itsConfig.isEnabled(pscEvent));
-    easyMock.verifyAll();
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertFalse(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
   }
 
-  private void setUpEnforced(String[] refPatterns) {
-    expect(pluginConfig.getString("enabled")).andReturn("enforced").once();
-    expect(pluginConfigWithInheritance.getStringList("branch")).andReturn(refPatterns);
-    easyMock.replayAll();
+  public void testIsEnabledRefNoParentNonMatchingBranchDisabled() {
+    String[] branches = {"^refs/heads/foo.*"};
+    setupIsEnabled("false", null, branches);
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertFalse(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
   }
 
-  private void setUpEnabled(String[] refPatterns) {
-    expect(pluginConfig.getString("enabled")).andReturn("true");
-    expect(pluginConfigWithInheritance.getBoolean("enabled", false)).andReturn(true);
-    expect(pluginConfigWithInheritance.getStringList("branch")).andReturn(refPatterns);
-    easyMock.replayAll();
+  public void testIsEnabledRefNoParentNonMatchingBranchEnforced() {
+    String[] branches = {"^refs/heads/foo.*"};
+    setupIsEnabled("enforced", null, branches);
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertFalse(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
+  }
+
+  public void testIsEnabledRefNoParentMatchingBranchMiddleEnabled() {
+    String[] branches = {"^refs/heads/foo.*", "^refs/heads/test.*", "^refs/heads/baz.*"};
+    setupIsEnabled("true", null, branches);
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertTrue(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
+  }
+
+  public void testIsEnabledRefNoParentMatchingBranchMiddleDisabled() {
+    String[] branches = {"^refs/heads/foo.*", "^refs/heads/test.*", "^refs/heads/baz.*"};
+    setupIsEnabled("false", null, branches);
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertFalse(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
+  }
+
+  public void testIsEnabledRefNoParentMatchingBranchMiddleEnforced() {
+    String[] branches = {"^refs/heads/foo.*", "^refs/heads/test.*", "^refs/heads/baz.*"};
+    setupIsEnabled("enforced", null, branches);
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertTrue(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
+  }
+
+  public void testIsEnabledRefParentNoBranchEnabled() {
+    String[] branches = {};
+    setupIsEnabled("false", "true", branches);
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertFalse(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
+  }
+
+  public void testIsEnabledRefParentNoBranchDisabled() {
+    String[] branches = {};
+    setupIsEnabled("false", "false", branches);
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertFalse(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
+  }
+
+  public void testIsEnabledRefParentNoBranchEnforced() {
+    String[] branches = {};
+    setupIsEnabled("false", "enforced", branches);
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertTrue(itsConfig.isEnabled("testProject", "refs/heads/testBranch"));
+  }
+
+  public void testIsEnabledEventNoBranches() {
+    String[] branches = {};
+    setupIsEnabled("true", null, branches);
+
+    PatchSetCreatedEvent event = new PatchSetCreatedEvent();
+    event.change = new ChangeAttribute();
+    event.change.project = "testProject";
+    event.change.branch = "testBranch";
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertTrue(itsConfig.isEnabled(event));
+  }
+
+  public void testIsEnabledEventSingleBranchExact() {
+    String[] branches = {"refs/heads/testBranch"};
+    setupIsEnabled("true", null, branches);
+
+    PatchSetCreatedEvent event = new PatchSetCreatedEvent();
+    event.change = new ChangeAttribute();
+    event.change.project = "testProject";
+    event.change.branch = "testBranch";
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertTrue(itsConfig.isEnabled(event));
+  }
+
+  public void testIsEnabledEventSingleBranchRegExp() {
+    String[] branches = {"^refs/heads/test.*"};
+    setupIsEnabled("true", null, branches);
+
+    PatchSetCreatedEvent event = new PatchSetCreatedEvent();
+    event.change = new ChangeAttribute();
+    event.change.project = "testProject";
+    event.change.branch = "testBranch";
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertTrue(itsConfig.isEnabled(event));
+  }
+
+  public void testIsEnabledEventSingleBranchNonMatchingRegExp() {
+    String[] branches = {"^refs/heads/foo.*"};
+    setupIsEnabled("true", null, branches);
+
+    PatchSetCreatedEvent event = new PatchSetCreatedEvent();
+    event.change = new ChangeAttribute();
+    event.change.project = "testProject";
+    event.change.branch = "testBranch";
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertFalse(itsConfig.isEnabled(event));
+  }
+
+  public void testIsEnabledEventMultiBranchExact() {
+    String[] branches = {"refs/heads/foo", "refs/heads/testBranch"};
+    setupIsEnabled("true", null, branches);
+
+    PatchSetCreatedEvent event = new PatchSetCreatedEvent();
+    event.change = new ChangeAttribute();
+    event.change.project = "testProject";
+    event.change.branch = "testBranch";
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertTrue(itsConfig.isEnabled(event));
+  }
+
+  public void testIsEnabledEventMultiBranchRegExp() {
+    String[] branches = {"^refs/heads/foo.*", "^refs/heads/test.*"};
+    setupIsEnabled("true", null, branches);
+
+    PatchSetCreatedEvent event = new PatchSetCreatedEvent();
+    event.change = new ChangeAttribute();
+    event.change.project = "testProject";
+    event.change.branch = "testBranch";
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertTrue(itsConfig.isEnabled(event));
+  }
+
+  public void testIsEnabledEventMultiBranchMixedMatchExact() {
+    String[] branches = {"refs/heads/testBranch", "refs/heads/foo.*"};
+    setupIsEnabled("true", null, branches);
+
+    PatchSetCreatedEvent event = new PatchSetCreatedEvent();
+    event.change = new ChangeAttribute();
+    event.change.project = "testProject";
+    event.change.branch = "testBranch";
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertTrue(itsConfig.isEnabled(event));
+  }
+
+public void testIsEnabledEventMultiBranchMixedMatchRegExp() {
+    String[] branches = {"refs/heads/foo", "^refs/heads/test.*"};
+    setupIsEnabled("true", null, branches);
+
+    PatchSetCreatedEvent event = new PatchSetCreatedEvent();
+    event.change = new ChangeAttribute();
+    event.change.project = "testProject";
+    event.change.branch = "testBranch";
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertTrue(itsConfig.isEnabled(event));
+  }
+
+  public void testIsEnabledEventDisabled() {
+    String[] branches = {"^refs/heads/testBranch"};
+    setupIsEnabled("false", null, branches);
+
+    PatchSetCreatedEvent event = new PatchSetCreatedEvent();
+    event.change = new ChangeAttribute();
+    event.change.project = "testProject";
+    event.change.branch = "testBranch";
+
+    ItsConfig itsConfig = createItsConfig();
+
+    replayMocks();
+
+    assertFalse(itsConfig.isEnabled(event));
+  }
+
+  public void testGetIssuePatternNullMatch() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn(null).atLeastOnce();
+    expect(serverConfig.getString("commentlink", "ItsTestName", "match"))
+        .andReturn(null).atLeastOnce();
+
+    replayMocks();
+
+    assertNull("Pattern for null match is not null",
+        itsConfig.getIssuePattern());
+  }
+
+  public void testGetIssuePatternNullMatchWCommentLink() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn("foo").atLeastOnce();
+    expect(serverConfig.getString("commentlink", "foo", "match"))
+        .andReturn(null).atLeastOnce();
+
+    replayMocks();
+
+    assertNull("Pattern for null match is not null",
+        itsConfig.getIssuePattern());
+  }
+
+  public void testGetIssuePattern() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn(null).atLeastOnce();
+    expect(serverConfig.getString("commentlink", "ItsTestName", "match"))
+        .andReturn("TestPattern").atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and generated pattern are not equal",
+        "TestPattern", itsConfig.getIssuePattern().pattern());
+  }
+
+  public void testGetIssuePatternWCommentLink() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn("foo").atLeastOnce();
+    expect(serverConfig.getString("commentlink", "foo", "match"))
+        .andReturn("TestPattern").atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and generated pattern are not equal",
+        "TestPattern", itsConfig.getIssuePattern().pattern());
+
+  }
+
+  public void testGetIssuePatternGroupIndexGroupDefault() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn(null).atLeastOnce();
+    expect(serverConfig.getString("commentlink", "ItsTestName", "match"))
+        .andReturn("(foo)(bar)(baz)").atLeastOnce();
+    expect(serverConfig.getInt("ItsTestName", "commentlinkGroupIndex", 1))
+        .andReturn(1).atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and actual group index do not match",
+        1, itsConfig.getIssuePatternGroupIndex());
+  }
+
+  public void testGetIssuePatternGroupIndexGroupDefaultGroupless() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn(null).atLeastOnce();
+    expect(serverConfig.getString("commentlink", "ItsTestName", "match"))
+        .andReturn("foo").atLeastOnce();
+    expect(serverConfig.getInt("ItsTestName", "commentlinkGroupIndex", 1))
+        .andReturn(1).atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and actual group index do not match",
+        0, itsConfig.getIssuePatternGroupIndex());
+  }
+
+  public void testGetIssuePatternGroupIndexGroup1() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn(null).atLeastOnce();
+    expect(serverConfig.getString("commentlink", "ItsTestName", "match"))
+        .andReturn("(foo)(bar)(baz)").atLeastOnce();
+    expect(serverConfig.getInt("ItsTestName", "commentlinkGroupIndex", 1))
+        .andReturn(1).atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and actual group index do not match",
+        1, itsConfig.getIssuePatternGroupIndex());
+  }
+
+  public void testGetIssuePatternGroupIndexGroup3() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn(null).atLeastOnce();
+    expect(serverConfig.getString("commentlink", "ItsTestName", "match"))
+        .andReturn("(foo)(bar)(baz)").atLeastOnce();
+    expect(serverConfig.getInt("ItsTestName", "commentlinkGroupIndex", 1))
+        .andReturn(3).atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and actual group index do not match",
+        3, itsConfig.getIssuePatternGroupIndex());
+  }
+
+  public void testGetIssuePatternGroupIndexGroupTooHigh() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn(null).atLeastOnce();
+    expect(serverConfig.getString("commentlink", "ItsTestName", "match"))
+        .andReturn("(foo)(bar)(baz)").atLeastOnce();
+    expect(serverConfig.getInt("ItsTestName", "commentlinkGroupIndex", 1))
+        .andReturn(5).atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and actual group index do not match",
+        1, itsConfig.getIssuePatternGroupIndex());
+  }
+
+  public void testGetIssuePatternGroupIndexGroupTooHighGroupless() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn(null).atLeastOnce();
+    expect(serverConfig.getString("commentlink", "ItsTestName", "match"))
+        .andReturn("foo").atLeastOnce();
+    expect(serverConfig.getInt("ItsTestName", "commentlinkGroupIndex", 1))
+        .andReturn(5).atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and actual group index do not match",
+        0, itsConfig.getIssuePatternGroupIndex());
+  }
+
+  public void testGetItsAssociationPolicyOptional() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn(null).atLeastOnce();
+    expect(serverConfig.getEnum("commentlink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL))
+        .andReturn(ItsAssociationPolicy.OPTIONAL)
+        .atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and generated associated policy do not match",
+        ItsAssociationPolicy.OPTIONAL, itsConfig.getItsAssociationPolicy());
+  }
+
+  public void testGetItsAssociationPolicyOptionalWCommentLink() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn("foo").atLeastOnce();
+    expect(serverConfig.getEnum("commentlink", "foo", "association",
+        ItsAssociationPolicy.OPTIONAL))
+        .andReturn(ItsAssociationPolicy.OPTIONAL)
+        .atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and generated associated policy do not match",
+        ItsAssociationPolicy.OPTIONAL, itsConfig.getItsAssociationPolicy());
+  }
+
+  public void testGetItsAssociationPolicySuggested() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn(null).atLeastOnce();
+    expect(serverConfig.getEnum("commentlink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL))
+        .andReturn(ItsAssociationPolicy.SUGGESTED)
+        .atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and generated associated policy do not match",
+        ItsAssociationPolicy.SUGGESTED, itsConfig.getItsAssociationPolicy());
+  }
+
+  public void testGetItsAssociationPolicySuggestedWCommentLink() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn("foo").atLeastOnce();
+    expect(serverConfig.getEnum("commentlink", "foo", "association",
+        ItsAssociationPolicy.OPTIONAL))
+        .andReturn(ItsAssociationPolicy.SUGGESTED)
+        .atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and generated associated policy do not match",
+        ItsAssociationPolicy.SUGGESTED, itsConfig.getItsAssociationPolicy());
+  }
+
+  public void testGetItsAssociationPolicyMandatory() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn(null).atLeastOnce();
+    expect(serverConfig.getEnum("commentlink", "ItsTestName", "association",
+        ItsAssociationPolicy.OPTIONAL))
+        .andReturn(ItsAssociationPolicy.MANDATORY)
+        .atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and generated associated policy do not match",
+        ItsAssociationPolicy.MANDATORY, itsConfig.getItsAssociationPolicy());
+  }
+
+  public void testGetItsAssociationPolicyMandatoryWCommentLink() {
+    ItsConfig itsConfig = createItsConfig();
+
+    expect(serverConfig.getString("ItsTestName", null, "commentlink"))
+        .andReturn("foo").atLeastOnce();
+    expect(serverConfig.getEnum("commentlink", "foo", "association",
+        ItsAssociationPolicy.OPTIONAL))
+        .andReturn(ItsAssociationPolicy.MANDATORY)
+        .atLeastOnce();
+
+    replayMocks();
+
+    assertEquals("Expected and generated associated policy do not match",
+        ItsAssociationPolicy.MANDATORY, itsConfig.getItsAssociationPolicy());
+  }
+
+  private ItsConfig createItsConfig() {
+    return injector.getInstance(ItsConfig.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    injector = Guice.createInjector(new TestModule());
+  }
+
+  private class TestModule extends FactoryModule {
+    @Override
+    protected void configure() {
+      projectCache = createMock(ProjectCache.class);
+      bind(ProjectCache.class).toInstance(projectCache);
+
+      pluginConfigFactory = createMock(PluginConfigFactory.class);
+      bind(PluginConfigFactory.class).toInstance(pluginConfigFactory);
+
+      bind(String.class).annotatedWith(PluginName.class)
+        .toInstance("ItsTestName");
+
+      serverConfig = createMock(Config.class);
+      bind(Config.class).annotatedWith(GerritServerConfig.class)
+          .toInstance(serverConfig);
+    }
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractorTest.java b/src/test/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractorTest.java
index a18a1ef..7a1530d 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractorTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/hooks/util/IssueExtractorTest.java
@@ -17,21 +17,19 @@
 
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.PatchSetAccess;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.FactoryModule;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 
+import com.googlesource.gerrit.plugins.hooks.its.ItsConfig;
 import com.googlesource.gerrit.plugins.hooks.testutil.LoggingMockingTestCase;
 
-import org.eclipse.jgit.lib.Config;
 import org.junit.runner.RunWith;
 import org.powermock.core.classloader.annotations.PrepareForTest;
 import org.powermock.modules.junit4.PowerMockRunner;
@@ -40,44 +38,21 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 @RunWith(PowerMockRunner.class)
 @PrepareForTest({PatchSet.class,RevId.class})
 public class IssueExtractorTest extends LoggingMockingTestCase {
   private Injector injector;
-  private Config serverConfig;
+  private ItsConfig itsConfig;
   private CommitMessageFetcher commitMessageFetcher;
   private ReviewDb db;
 
-  public void testPatternNullMatch() {
-    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
-
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-        .andReturn(null).atLeastOnce();
-
-    replayMocks();
-
-    assertNull("Pattern for null match is not null",
-        issueExtractor.getPattern());
-  }
-
-  public void testPattern() {
-    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
-
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-        .andReturn("TestPattern").atLeastOnce();
-
-    replayMocks();
-
-    assertEquals("Expected and generated pattern are not equal",
-        "TestPattern", issueExtractor.getPattern().pattern());
-  }
-
   public void testIssueIdsNullPattern() {
     IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
 
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-        .andReturn(null).atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(null)
+        .atLeastOnce();
 
     replayMocks();
 
@@ -87,8 +62,10 @@
 
   public void testIssueIdsNoMatch() {
     IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-        .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     replayMocks();
 
@@ -98,10 +75,27 @@
     assertLogMessageContains("Matching");
   }
 
+  public void testIssueIdsEmptyGroup() {
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(X*)(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
+
+    replayMocks();
+
+    String ret[] = issueExtractor.getIssueIds("bug#4711");
+    assertEquals("Number of found ids do not match", 0, ret.length);
+
+    assertLogMessageContains("Matching");
+  }
+
   public void testIssueIdsFullMatch() {
     IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-        .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     replayMocks();
 
@@ -114,8 +108,10 @@
 
   public void testIssueIdsMatch() {
     IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-        .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     replayMocks();
 
@@ -128,8 +124,10 @@
 
   public void testIssueIdsGrouplessMatch() {
     IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-        .andReturn("bug#\\d+").atLeastOnce();
+
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#\\d+"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(0).atLeastOnce();
 
     replayMocks();
 
@@ -140,10 +138,12 @@
     assertLogMessageContains("Matching");
   }
 
-  public void testIssueIdsMultiGroupMatch() {
+  public void testIssueIdsMultiGroupMatchGroup1() {
     IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-        .andReturn("bug#(\\d)(\\d+)").atLeastOnce();
+
+    expect(itsConfig.getIssuePattern())
+        .andReturn(Pattern.compile("bug#(\\d)(\\d+)")).atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     replayMocks();
 
@@ -154,10 +154,28 @@
     assertLogMessageContains("Matching");
   }
 
+  public void testIssueIdsMultiGroupMatchGroup2() {
+    IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
+
+    expect(itsConfig.getIssuePattern())
+        .andReturn(Pattern.compile("bug#(\\d)(\\d+)")).atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(2).atLeastOnce();
+
+    replayMocks();
+
+    String ret[] = issueExtractor.getIssueIds("Foo bug#4711 bar");
+    assertEquals("Number of found ids do not match", 1, ret.length);
+    assertEquals("Found issue id does not match", "711", ret[0]);
+
+    assertLogMessageContains("Matching");
+  }
+
   public void testIssueIdsMulipleMatches() {
     IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-        .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     replayMocks();
 
@@ -173,8 +191,10 @@
 
   public void testIssueIdsMulipleMatchesWithDuplicates() {
     IssueExtractor issueExtractor = injector.getInstance(IssueExtractor.class);
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-        .andReturn("bug#(\\d+)").atLeastOnce();
+
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     replayMocks();
 
@@ -190,8 +210,9 @@
   }
 
   public void testIssueIdsCommitSingleIssue() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn(
@@ -217,8 +238,9 @@
   }
 
   public void testIssueIdsCommitMultipleIssues() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn(
@@ -245,8 +267,9 @@
   }
 
   public void testIssueIdsCommitMultipleIssuesMultipleTimes() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn(
@@ -273,8 +296,9 @@
   }
 
   public void testIssueIdsCommitSingleIssueBody() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn(
@@ -303,8 +327,9 @@
   }
 
   public void testIssueIdsCommitSingleIssueFooter() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn(
@@ -334,8 +359,9 @@
   }
 
   public void testIssueIdsCommitMultipleIssuesFooter() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn(
@@ -374,8 +400,9 @@
   }
 
   public void testIssueIdsCommitDifferentParts() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn(
@@ -408,8 +435,9 @@
   }
 
   public void testIssueIdsCommitDifferentPartsEmptySubject() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn(
@@ -441,8 +469,9 @@
   }
 
   public void testIssueIdsCommitDifferentPartsLinePastFooter() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn(
@@ -475,8 +504,9 @@
   }
 
   public void testIssueIdsCommitDifferentPartsLinesPastFooter() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn(
@@ -510,8 +540,9 @@
   }
 
   public void testIssueIdsCommitDifferentPartsNoFooter() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn(
@@ -537,8 +568,9 @@
   }
 
   public void testIssueIdsCommitDifferentPartsNoFooterTrailingLine() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn(
@@ -564,8 +596,9 @@
   }
 
   public void testIssueIdsCommitDifferentPartsNoFooterTrailingLines() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn(
@@ -592,8 +625,9 @@
   }
 
   public void testIssueIdsCommitEmpty() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn("");
@@ -613,8 +647,9 @@
   }
 
   public void testIssueIdsCommitBlankLine() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn("\n");
@@ -632,8 +667,9 @@
   }
 
   public void testIssueIdsCommitBlankLines() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn("\n\n");
@@ -651,8 +687,9 @@
   }
 
   public void testIssueIdsCommitMoreBlankLines() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn("\n\n\n");
@@ -670,8 +707,9 @@
   }
 
   public void testIssueIdsCommitMixed() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn(
@@ -710,8 +748,9 @@
   }
 
   public void testIssueIdsCommitWAddedEmptyFirst() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     expect(commitMessageFetcher.fetchGuarded("testProject",
         "1234567891123456789212345678931234567894")).andReturn("");
@@ -732,8 +771,9 @@
   }
 
   public void testIssueIdsCommitWAddedSingleSubjectIssueFirst() {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     Change.Id changeId = createMock(Change.Id.class);
 
@@ -768,8 +808,9 @@
 
   public void testIssueIdsCommitWAddedSingleSubjectIssueSecondEmpty()
       throws OrmException {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     Change.Id changeId = createMock(Change.Id.class);
 
@@ -830,8 +871,9 @@
 
   public void testIssueIdsCommitWAddedSingleSubjectIssueSecondSame()
       throws OrmException {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     Change.Id changeId = createMock(Change.Id.class);
 
@@ -891,8 +933,9 @@
 
   public void testIssueIdsCommitWAddedSingleSubjectIssueSecondBody()
       throws OrmException {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     Change.Id changeId = createMock(Change.Id.class);
 
@@ -953,8 +996,9 @@
 
   public void testIssueIdsCommitWAddedSingleSubjectIssueSecondFooter()
       throws OrmException {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     Change.Id changeId = createMock(Change.Id.class);
 
@@ -1017,8 +1061,9 @@
 
   public void testIssueIdsCommitWAddedSubjectFooter()
       throws OrmException {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     Change.Id changeId = createMock(Change.Id.class);
 
@@ -1085,8 +1130,9 @@
 
   public void testIssueIdsCommitWAddedMultiple()
       throws OrmException {
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-    .andReturn("bug#(\\d+)").atLeastOnce();
+    expect(itsConfig.getIssuePattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+        .atLeastOnce();
+    expect(itsConfig.getIssuePatternGroupIndex()).andReturn(1).atLeastOnce();
 
     Change.Id changeId = createMock(Change.Id.class);
 
@@ -1164,12 +1210,8 @@
   private class TestModule extends FactoryModule {
     @Override
     protected void configure() {
-      bind(String.class).annotatedWith(PluginName.class)
-          .toInstance("ItsTestName");
-
-      serverConfig = createMock(Config.class);
-      bind(Config.class).annotatedWith(GerritServerConfig.class)
-          .toInstance(serverConfig);
+      itsConfig = createMock(ItsConfig.class);
+      bind(ItsConfig.class).toInstance(itsConfig);
 
       commitMessageFetcher = createMock(CommitMessageFetcher.class);
       bind(CommitMessageFetcher.class).toInstance(commitMessageFetcher);
diff --git a/src/test/java/com/googlesource/gerrit/plugins/hooks/validation/ItsValidateCommentTest.java b/src/test/java/com/googlesource/gerrit/plugins/hooks/validation/ItsValidateCommentTest.java
index 9fc9d79..fd679a6 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/hooks/validation/ItsValidateCommentTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/hooks/validation/ItsValidateCommentTest.java
@@ -45,9 +45,10 @@
 @PrepareForTest({RevCommit.class})
 public class ItsValidateCommentTest extends LoggingMockingTestCase {
   private Injector injector;
-  private Config serverConfig;
   private IssueExtractor issueExtractor;
   private ItsFacade itsFacade;
+  private ItsConfig itsConfig;
+
   private Project project = new Project(new Project.NameKey("myProject"));
 
   public void testOptional() throws CommitValidationException {
@@ -57,9 +58,10 @@
     RevCommit commit = createMock(RevCommit.class);
     CommitReceivedEvent event = new CommitReceivedEvent(command, project, null,
         commit, null);
-    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
-        ItsAssociationPolicy.OPTIONAL)).andReturn(
-            ItsAssociationPolicy.OPTIONAL).atLeastOnce();
+
+    expect(itsConfig.getItsAssociationPolicy())
+        .andReturn(ItsAssociationPolicy.OPTIONAL).atLeastOnce();
+
     replayMocks();
 
     ret = ivc.onCommitReceived(event);
@@ -74,11 +76,9 @@
     RevCommit commit = createMock(RevCommit.class);
     CommitReceivedEvent event = new CommitReceivedEvent(command, project, null,
         commit, null);
-    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
-        ItsAssociationPolicy.OPTIONAL)).andReturn(
-            ItsAssociationPolicy.SUGGESTED).atLeastOnce();
-    expect(serverConfig.getString("commentLink", "ItsTestName", "match"))
-        .andReturn("TestPattern").anyTimes();
+
+    expect(itsConfig.getItsAssociationPolicy())
+        .andReturn(ItsAssociationPolicy.SUGGESTED).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("TestMessage").atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
     expect(commit.getName()).andReturn("TestCommit").anyTimes();
@@ -101,9 +101,9 @@
     RevCommit commit = createMock(RevCommit.class);
     CommitReceivedEvent event = new CommitReceivedEvent(command, project, null,
         commit, null);
-    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
-        ItsAssociationPolicy.OPTIONAL)).andReturn(
-            ItsAssociationPolicy.MANDATORY).atLeastOnce();
+
+    expect(itsConfig.getItsAssociationPolicy())
+        .andReturn(ItsAssociationPolicy.MANDATORY).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("TestMessage").atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
     expect(commit.getName()).andReturn("TestCommit").anyTimes();
@@ -130,9 +130,8 @@
     RevCommit commit = createMock(RevCommit.class);
     CommitReceivedEvent event = new CommitReceivedEvent(command, project, null,
         commit, null);
-    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
-        ItsAssociationPolicy.OPTIONAL)).andReturn(
-            ItsAssociationPolicy.SUGGESTED).atLeastOnce();
+    expect(itsConfig.getItsAssociationPolicy())
+        .andReturn(ItsAssociationPolicy.SUGGESTED).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("bug#4711").atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
     expect(commit.getName()).andReturn("TestCommit").anyTimes();
@@ -155,9 +154,9 @@
     RevCommit commit = createMock(RevCommit.class);
     CommitReceivedEvent event = new CommitReceivedEvent(command, project, null,
         commit, null);
-    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
-        ItsAssociationPolicy.OPTIONAL)).andReturn(
-            ItsAssociationPolicy.MANDATORY).atLeastOnce();
+
+    expect(itsConfig.getItsAssociationPolicy())
+      .andReturn(ItsAssociationPolicy.MANDATORY).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("bug#4711").atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
     expect(commit.getName()).andReturn("TestCommit").anyTimes();
@@ -180,9 +179,9 @@
     RevCommit commit = createMock(RevCommit.class);
     CommitReceivedEvent event = new CommitReceivedEvent(command, project, null,
         commit, null);
-    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
-        ItsAssociationPolicy.OPTIONAL)).andReturn(
-            ItsAssociationPolicy.SUGGESTED).atLeastOnce();
+
+    expect(itsConfig.getItsAssociationPolicy())
+        .andReturn(ItsAssociationPolicy.SUGGESTED).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("bug#4711").atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
     expect(commit.getName()).andReturn("TestCommit").anyTimes();
@@ -209,9 +208,9 @@
     RevCommit commit = createMock(RevCommit.class);
     CommitReceivedEvent event = new CommitReceivedEvent(command, project, null,
         commit, null);
-    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
-        ItsAssociationPolicy.OPTIONAL)).andReturn(
-            ItsAssociationPolicy.MANDATORY).atLeastOnce();
+
+    expect(itsConfig.getItsAssociationPolicy())
+        .andReturn(ItsAssociationPolicy.MANDATORY).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("bug#4711").atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
     expect(commit.getName()).andReturn("TestCommit").anyTimes();
@@ -238,9 +237,9 @@
     RevCommit commit = createMock(RevCommit.class);
     CommitReceivedEvent event = new CommitReceivedEvent(command, project, null,
         commit, null);
-    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
-        ItsAssociationPolicy.OPTIONAL)).andReturn(
-            ItsAssociationPolicy.SUGGESTED).atLeastOnce();
+
+    expect(itsConfig.getItsAssociationPolicy())
+        .andReturn(ItsAssociationPolicy.SUGGESTED).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("bug#4711, bug#42")
         .atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
@@ -265,9 +264,9 @@
     RevCommit commit = createMock(RevCommit.class);
     CommitReceivedEvent event = new CommitReceivedEvent(command, project, null,
         commit, null);
-    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
-        ItsAssociationPolicy.OPTIONAL)).andReturn(
-            ItsAssociationPolicy.MANDATORY).atLeastOnce();
+
+    expect(itsConfig.getItsAssociationPolicy())
+        .andReturn(ItsAssociationPolicy.MANDATORY).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("bug#4711, bug#42")
         .atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
@@ -292,9 +291,9 @@
     RevCommit commit = createMock(RevCommit.class);
     CommitReceivedEvent event = new CommitReceivedEvent(command, project, null,
         commit, null);
-    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
-        ItsAssociationPolicy.OPTIONAL)).andReturn(
-            ItsAssociationPolicy.SUGGESTED).atLeastOnce();
+
+    expect(itsConfig.getItsAssociationPolicy())
+        .andReturn(ItsAssociationPolicy.SUGGESTED).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("bug#4711, bug#42")
         .atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
@@ -325,9 +324,9 @@
     RevCommit commit = createMock(RevCommit.class);
     CommitReceivedEvent event = new CommitReceivedEvent(command, project, null,
         commit, null);
-    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
-        ItsAssociationPolicy.OPTIONAL)).andReturn(
-            ItsAssociationPolicy.MANDATORY).atLeastOnce();
+
+    expect(itsConfig.getItsAssociationPolicy())
+        .andReturn(ItsAssociationPolicy.MANDATORY).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("bug#4711, bug#42")
         .atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
@@ -356,9 +355,9 @@
     RevCommit commit = createMock(RevCommit.class);
     CommitReceivedEvent event = new CommitReceivedEvent(command, project, null,
         commit, null);
-    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
-        ItsAssociationPolicy.OPTIONAL)).andReturn(
-            ItsAssociationPolicy.SUGGESTED).atLeastOnce();
+
+    expect(itsConfig.getItsAssociationPolicy())
+        .andReturn(ItsAssociationPolicy.SUGGESTED).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("bug#4711, bug#42")
         .atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
@@ -389,9 +388,9 @@
     RevCommit commit = createMock(RevCommit.class);
     CommitReceivedEvent event = new CommitReceivedEvent(command, project, null,
         commit, null);
-    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
-        ItsAssociationPolicy.OPTIONAL)).andReturn(
-            ItsAssociationPolicy.MANDATORY).atLeastOnce();
+
+    expect(itsConfig.getItsAssociationPolicy())
+        .andReturn(ItsAssociationPolicy.MANDATORY).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("bug#4711, bug#42")
         .atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
@@ -420,9 +419,9 @@
     RevCommit commit = createMock(RevCommit.class);
     CommitReceivedEvent event = new CommitReceivedEvent(command, project, null,
         commit, null);
-    expect(serverConfig.getEnum("commentLink", "ItsTestName", "association",
-        ItsAssociationPolicy.OPTIONAL)).andReturn(
-            ItsAssociationPolicy.SUGGESTED).atLeastOnce();
+
+    expect(itsConfig.getItsAssociationPolicy())
+        .andReturn(ItsAssociationPolicy.SUGGESTED).atLeastOnce();
     expect(commit.getFullMessage()).andReturn("bug#4711, bug#42")
         .atLeastOnce();
     expect(commit.getId()).andReturn(commit).anyTimes();
@@ -468,7 +467,9 @@
   }
 
   private void setupCommonMocks() {
-    expect(issueExtractor.getPattern()).andReturn(Pattern.compile("bug#(\\d+)"))
+    expect(itsConfig.getIssuePattern())
+        .andReturn(Pattern.compile("bug#(\\d+)")).anyTimes();
+    expect(itsConfig.isEnabled("myProject", null)).andReturn(true)
         .anyTimes();
   }
 
@@ -487,22 +488,14 @@
       bind(String.class).annotatedWith(PluginName.class)
           .toInstance("ItsTestName");
 
-      serverConfig = createMock(Config.class);
-      bind(Config.class).annotatedWith(GerritServerConfig.class)
-          .toInstance(serverConfig);
-
       issueExtractor = createMock(IssueExtractor.class);
       bind(IssueExtractor.class).toInstance(issueExtractor);
 
       itsFacade = createMock(ItsFacade.class);
       bind(ItsFacade.class).toInstance(itsFacade);
 
-      bind(ItsConfig.class).toInstance(new ItsConfig(null, null, null) {
-        @Override
-        public boolean isEnabled(String project, String branch) {
-          return true;
-        }
-      });
+      itsConfig = createMock(ItsConfig.class);
+      bind(ItsConfig.class).toInstance(itsConfig);
     }
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionControllerTest.java b/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionControllerTest.java
index eea768c..df122d5 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionControllerTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/hooks/workflow/ActionControllerTest.java
@@ -13,6 +13,7 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.hooks.workflow;
 
+import static org.easymock.EasyMock.anyObject;
 import static org.easymock.EasyMock.expect;
 
 import com.google.common.collect.Lists;
@@ -37,6 +38,7 @@
   private PropertyExtractor propertyExtractor;
   private RuleBase ruleBase;
   private ActionExecutor actionExecutor;
+  private ItsConfig itsConfig;
 
   public void testNoPropertySets() {
     ActionController actionController = createActionController();
@@ -182,10 +184,17 @@
     return injector.getInstance(ActionController.class);
   }
 
+  private void setupCommonMocks() {
+    expect(itsConfig.isEnabled(anyObject(Event.class))).andReturn(true)
+        .anyTimes();
+  }
+
   @Override
   public void setUp() throws Exception {
     super.setUp();
     injector = Guice.createInjector(new TestModule());
+
+    setupCommonMocks();
   }
 
   private class TestModule extends FactoryModule {
@@ -200,12 +209,8 @@
       actionExecutor = createMock(ActionExecutor.class);
       bind(ActionExecutor.class).toInstance(actionExecutor);
 
-      bind(ItsConfig.class).toInstance(new ItsConfig(null, null, null) {
-        @Override
-        public boolean isEnabled(Event event) {
-          return true;
-        }
-      });
+      itsConfig = createMock(ItsConfig.class);
+      bind(ItsConfig.class).toInstance(itsConfig);
     }
   }
 }
\ No newline at end of file