Add the configuration for the merge changes grace time

Allow per project configuration of the merge changes grace time (by
default 24h).

Change-Id: Icf28a3cf274b771e1992e694d284df4ee1166a03
diff --git a/README.md b/README.md
index 5b6b123..e0d246a 100644
--- a/README.md
+++ b/README.md
@@ -70,9 +70,9 @@
 has no side effects.
 
 Filtering a closed change refs has the following meaning:
-- Merged changes and all their patch-sets
-- Abandoned changes and all their patch-sets
-- Corrupted changes and all their patch-sets
+- Merged changes and all their patch-sets older than the [grace time](#grace-time-for-closed-changes)
+- Abandoned changes and all their patch-sets older than the [grace time](#grace-time-for-closed-changes)
+- Corrupted changes and all their patch-sets older than the [grace time](#grace-time-for-closed-changes)
 - All '/meta' refs of all changes
 - All non-published edits of any changes
 
@@ -100,3 +100,18 @@
 a READ rule to refs/*). To enable the closed changes filtering you need to disable any global read rule
 for the group that needs refs filtering.
 
+### Grace time for closed changes
+
+The refsfilter allows to define `git-refs-filter: grace time [sec] for closed changes`
+project configuration parameter. This parameter controls the size of the grace
+time window in seconds. All closed changes newer than the grace time will not
+be filtered out. Value can be defined per project or can be inherited from its parents.
+
+Default value: 86400
+
+Example of setting the grace time parameter in `project.config`:
+
+```
+[plugin "gerrit"]
+  gitRefFilterClosedChangesGraceTimeSec = 3600
+```
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/FilterRefsConfig.java b/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/FilterRefsConfig.java
index 6bfafc2..1b29094 100644
--- a/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/FilterRefsConfig.java
+++ b/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/FilterRefsConfig.java
@@ -15,7 +15,10 @@
 package com.googlesource.gerrit.modules.gitrefsfilter;
 
 import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.inject.Inject;
 import java.util.Arrays;
 import java.util.List;
@@ -27,18 +30,20 @@
 public class FilterRefsConfig {
   public static final String SECTION_GIT_REFS_FILTER = "git-refs-filter";
   public static final String KEY_HIDE_REFS = "hideRefs";
+  public static final String PROJECT_CONFIG_CLOSED_CHANGES_GRACE_TIME_SEC =
+      "gitRefFilterClosedChangesGraceTimeSec";
 
-  private static final long CLOSED_CHANGE_GRACE_TIME_SEC_DEFAULT =
+  static final long CLOSED_CHANGES_GRACE_TIME_SEC_DEFAULT =
       TimeUnit.SECONDS.convert(24, TimeUnit.HOURS);
 
-  private long closedChangeGraceTimeSec = CLOSED_CHANGE_GRACE_TIME_SEC_DEFAULT;
-
   private final List<String> hideRefs;
   private final List<String> showRefs;
+  private PluginConfigFactory cfgFactory;
 
   @Inject
-  public FilterRefsConfig(@GerritServerConfig Config gerritConfig) {
+  public FilterRefsConfig(@GerritServerConfig Config gerritConfig, PluginConfigFactory cfgFactory) {
 
+    this.cfgFactory = cfgFactory;
     List<String> hideRefsConfig =
         Arrays.asList(gerritConfig.getStringList(SECTION_GIT_REFS_FILTER, null, KEY_HIDE_REFS));
 
@@ -75,11 +80,12 @@
     return true;
   }
 
-  public long getClosedChangeGraceTimeSec() {
-    return closedChangeGraceTimeSec;
-  }
-
-  public void setClosedChangeGraceTimeSec(long graceTimeSec) {
-    this.closedChangeGraceTimeSec = graceTimeSec;
+  /** performance warning: this call can be expensive, please reuse the value */
+  public long getClosedChangeGraceTimeSec(Project.NameKey projectKey)
+      throws NoSuchProjectException {
+    return cfgFactory
+        .getFromProjectConfigWithInheritance(projectKey, "gerrit")
+        .getLong(
+            PROJECT_CONFIG_CLOSED_CHANGES_GRACE_TIME_SEC, CLOSED_CHANGES_GRACE_TIME_SEC_DEFAULT);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/ForProjectWrapper.java b/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/ForProjectWrapper.java
index 7e30b5f..5347f2d 100644
--- a/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/ForProjectWrapper.java
+++ b/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/ForProjectWrapper.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
 import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.name.Named;
@@ -53,6 +54,7 @@
   private final ForProject defaultForProject;
   private final Project.NameKey project;
   private final FilterRefsConfig config;
+  private long closedChangesGraceTime;
 
   public interface Factory {
     ForProjectWrapper get(ForProject defaultForProject, Project.NameKey project);
@@ -64,12 +66,14 @@
       @Named(OPEN_CHANGES_CACHE) LoadingCache<ChangeCacheKey, Boolean> openChangesCache,
       @Named(CHANGES_CACHE_TS) LoadingCache<ChangeCacheKey, Long> changesTsCache,
       @Assisted ForProject defaultForProject,
-      @Assisted Project.NameKey project) {
+      @Assisted Project.NameKey project)
+      throws NoSuchProjectException {
     this.openChangesCache = openChangesCache;
     this.changesTsCache = changesTsCache;
     this.defaultForProject = defaultForProject;
     this.project = project;
     this.config = config;
+    this.closedChangesGraceTime = config.getClosedChangeGraceTimeSec(project);
   }
 
   @Override
@@ -142,9 +146,7 @@
     try {
       Timestamp cutOffTs =
           Timestamp.from(
-              Instant.now()
-                  .truncatedTo(ChronoUnit.SECONDS)
-                  .minusSeconds(config.getClosedChangeGraceTimeSec()));
+              Instant.now().truncatedTo(ChronoUnit.SECONDS).minusSeconds(closedChangesGraceTime));
       return changesTsCache
               .get(ChangeCacheKey.create(repo, changeId, changeRevision, project))
               .longValue()
diff --git a/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/RefsFilterModule.java b/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/RefsFilterModule.java
index 5d53d22..8b882c9 100644
--- a/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/RefsFilterModule.java
+++ b/src/main/java/com/googlesource/gerrit/modules/gitrefsfilter/RefsFilterModule.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.config.CapabilityDefinition;
+import com.google.gerrit.server.config.ProjectConfigEntry;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.inject.AbstractModule;
 import com.google.inject.Scopes;
@@ -44,6 +45,15 @@
         .to(FilterRefsCapability.class)
         .in(Scopes.SINGLETON);
 
+    bind(ProjectConfigEntry.class)
+        .annotatedWith(Exports.named(FilterRefsConfig.PROJECT_CONFIG_CLOSED_CHANGES_GRACE_TIME_SEC))
+        .toInstance(
+            new ProjectConfigEntry(
+                "git-refs-filter: grace time [sec] for closed changes",
+                FilterRefsConfig.CLOSED_CHANGES_GRACE_TIME_SEC_DEFAULT,
+                true,
+                "Grace time for keeping closed changes from filtering by the git-refs-filter"));
+
     install(OpenChangesCache.module());
     install(ChangesTsCache.module());
   }
diff --git a/src/test/java/com/googlesource/gerrit/libmodule/plugins/test/GitRefsFilterTest.java b/src/test/java/com/googlesource/gerrit/libmodule/plugins/test/GitRefsFilterTest.java
index 44d6cbe..1fd85c1 100644
--- a/src/test/java/com/googlesource/gerrit/libmodule/plugins/test/GitRefsFilterTest.java
+++ b/src/test/java/com/googlesource/gerrit/libmodule/plugins/test/GitRefsFilterTest.java
@@ -29,6 +29,8 @@
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.project.ProjectConfig;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.name.Named;
@@ -60,7 +62,6 @@
 @Sandboxed
 public class GitRefsFilterTest extends AbstractGitDaemonTest {
   @Inject private RequestScopeOperations requestScopeOperations;
-  @Inject private FilterRefsConfig filterConfig;
 
   @Inject
   private @Named(OPEN_CHANGES_CACHE) LoadingCache<ChangeCacheKey, Boolean> changeOpenCache;
@@ -83,7 +84,18 @@
   @Before
   public void setup() throws Exception {
     createFilteredRefsGroup();
-    filterConfig.setClosedChangeGraceTimeSec(CLOSED_CHANGES_GRACE_TIME_SEC);
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+      ProjectConfig projectConfig = projectConfigFactory.create(project);
+      projectConfig.load(md);
+      projectConfig.updatePluginConfig(
+          "gerrit",
+          cfg ->
+              cfg.setLong(
+                  FilterRefsConfig.PROJECT_CONFIG_CLOSED_CHANGES_GRACE_TIME_SEC,
+                  CLOSED_CHANGES_GRACE_TIME_SEC));
+      projectConfig.commit(md);
+      projectCache.evict(project);
+    }
   }
 
   @Test