Flag and aggregate commits by BotLike-ness

With this change it is possible to provide a new bot-like-regexps
parameter which allows to pass a comma separated list of regexps to be
used in recognizing bot-like commits.

When *all* files touched by a commit match those regexps, then the commit
is flagged as is_bot_like.

Change-Id: I0f3af74dc223e9f12c9e19592c226d784eceb6f7
diff --git a/README.md b/README.md
index 1b01f3e..e7eb7ab 100644
--- a/README.md
+++ b/README.md
@@ -71,22 +71,76 @@
 - --aggregate -granularity -g one of email, email_year, email_month, email_day, email_hour defaulting to aggregation by email
 - --extract-branches -r enables splitting of aggregation by branch name and expose branch name in the payload
 - --extract-issues -i enables the extraction of issues from commentLink
+- --botlike-filename-regexps -n comma separated list of regexps that identify a bot-like commit, commits that modify only files whose name is a match will be flagged as bot-like
 
 NOTE: Timestamp format is consistent with Gerrit's query syntax, see /Documentation/user-search.html for details.
 
-REST Example:
+### Examples
+
+- REST:
 
 ```
    $ curl http://gerrit.mycompany.com/projects/myproject/analytics~contributors
-   {"name":"John Doe","email":"john.doe@mycompany.com","num_commits":1, "num_files":4,"added_lines":9,"deleted_lines":1, "commits":[{"sha1":"6a1f73738071e299f600017d99f7252d41b96b4b","date":"Apr 28, 2011 5:13:14 AM","merge":false}]}
-   {"name":"Matt Smith","email":"matt.smith@mycompany.com","num_commits":1, "num_files":1,"added_lines":90,"deleted_lines":10,"commits":[{"sha1":"54527e7e3086758a23e3b069f183db6415aca304","date":"Sep 8, 2015 3:11:23 AM","merge":true}],"branches":["master"]}
+   {"name":"John Doe","email":"john.doe@mycompany.com","num_commits":1, "num_files":4,"added_lines":9,"deleted_lines":1, "commits":[{"sha1":"6a1f73738071e299f600017d99f7252d41b96b4b","date":"Apr 28, 2011 5:13:14 AM","merge":false,"bot_like": false}],"is_bot_like": false}
+   {"name":"Matt Smith","email":"matt.smith@mycompany.com","num_commits":1, "num_files":1,"added_lines":90,"deleted_lines":10,"commits":[{"sha1":"54527e7e3086758a23e3b069f183db6415aca304","date":"Sep 8, 2015 3:11:23 AM","merge":true,"bot_like": false}],"branches":["master"],"is_bot_like": false}
 ```
 
-SSH Example:
+- SSH:
 
 ```
    $ ssh -p 29418 admin@gerrit.mycompany.com analytics contributors myproject --since 2017-08-01 --until 2017-12-31 --extract-issues
-   {"name":"John Doe","email":"john.doe@mycompany.com","num_commits":1, "num_files":4,"added_lines":9,"deleted_lines":1, "commits":[{"sha1":"6a1f73738071e299f600017d99f7252d41b96b4b","date":"Apr 28, 2011 5:13:14 AM","merge":false}], "issues_codes":["PRJ-001"],"issues_links":["https://jira.company.org/PRJ-001"]}
-   {"name":"Matt Smith","email":"matt.smith@mycompany.com","num_commits":1, "num_files":1,"added_lines":90,"deleted_lines":10,"commits":[{"sha1":"54527e7e3086758a23e3b069f183db6415aca304","date":"Sep 8, 2015 3:11:23 AM","merge":true}],"branches":["branch1"],"issues_codes":["PRJ-002","PRJ-003"],"issues_links":["https://jira.company.org/PRJ-002","https://jira.company.org/PRJ-003"]}
+   {"name":"John Doe","email":"john.doe@mycompany.com","num_commits":1, "num_files":4,"added_lines":9,"deleted_lines":1, "commits":[{"sha1":"6a1f73738071e299f600017d99f7252d41b96b4b","date":"Apr 28, 2011 5:13:14 AM","merge":false,"bot_like": false}],"is_bot_like": false,"issues_codes":["PRJ-001"],"issues_links":["https://jira.company.org/PRJ-001"]}
+   {"name":"Matt Smith","email":"matt.smith@mycompany.com","num_commits":1, "num_files":1,"added_lines":90,"deleted_lines":10,"commits":[{"sha1":"54527e7e3086758a23e3b069f183db6415aca304","date":"Sep 8, 2015 3:11:23 AM","merge":true,"bot_like": false,}],"is_bot_like": false,"branches":["branch1"],"issues_codes":["PRJ-002","PRJ-003"],"issues_links":["https://jira.company.org/PRJ-002","https://jira.company.org/PRJ-003"]}
+```
+
+- BOT-like:
+Flags the commit as bot-like when *all* files in that commit match any of the following regular expressions: 
+
+    * .+\\.xml
+    * .+\\.bzl
+    * BUILD
+    * WORKSPACE
+    * \\.gitignore
+    * plugins/
+    * \\.settings
+
+```
+curl 'http://gerrit.mycompany.com/projects/myproject/analytics~contributors?botlike-filename-regexps=.%2B%5C.xml%2C.%2B%5C.bzl%2CBUILD%2CWORKSPACE%2C%5C.gitignore%2Cplugins%2F%2C%5C.settings'
+
+{
+  "year": 2018,
+  "month": 3,
+  "day": 21,
+  "hour": 19,
+  "name": "Dave Borowitz",
+  "email": "dborowitz@google.com",
+  "num_commits": 1,
+  "num_files": 6,
+  "num_distinct_files": 6,
+  "added_lines": 6,
+  "deleted_lines": 6,
+  "commits": [
+    {
+      "sha1": "a3ab2e1d07e6745f50b1d9907f6580c6521fd035",
+      "date": 1521661246000,
+      "merge": false,
+      "bot_like": true,
+      "files": [
+        "version.bzl",
+        "gerrit-plugin-gwtui/pom.xml",
+        "gerrit-extension-api/pom.xml",
+        "gerrit-war/pom.xml",
+        "gerrit-plugin-api/pom.xml",
+        "gerrit-acceptance-framework/pom.xml"
+      ]
+    }
+  ],
+  "branches": [],
+  "issues_codes": [],
+  "issues_links": [],
+  "last_commit_date": 1521661246000,
+  "is_merge": false,
+  "is_bot_like": true
+}
 ```
 
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/Contributors.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/Contributors.scala
index 4b6c5e7..c11da56 100644
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/Contributors.scala
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/Contributors.scala
@@ -34,6 +34,7 @@
   private var beginDate: Option[Long] = None
   private var endDate: Option[Long] = None
   private var granularity: Option[AggregationStrategy] = None
+  private var botLikeRegexps: List[String] = List.empty[String]
 
   @ArgOption(name = "--extract-branches", aliases = Array("-r"),
     usage = "Do extra parsing to extract a list of all branches for each line")
@@ -73,9 +74,15 @@
     usage = "Extract a list of issues and links using the Gerrit's commentLink configuration")
   private var extractIssues: Boolean = false
 
+  @ArgOption(name = "--botlike-filename-regexps", aliases = Array("-n"),
+    usage = "comma separated list of regexps that identify a bot-like commit, commits that modify only files whose name is a match will be flagged as bot-like")
+  def setBotLikeRegexps(value: String): Unit = {
+    botLikeRegexps = value.split(",").toList
+  }
+
   override protected def run =
     gsonFmt.format(executor.get(projectRes, beginDate, endDate,
-      granularity.getOrElse(AggregationStrategy.EMAIL), extractBranches, extractIssues), stdout)
+      granularity.getOrElse(AggregationStrategy.EMAIL), extractBranches, extractIssues, botLikeRegexps), stdout)
 
 }
 
@@ -86,6 +93,7 @@
   private var beginDate: Option[Long] = None
   private var endDate: Option[Long] = None
   private var granularity: Option[AggregationStrategy] = None
+  private var botLikeRegexps: List[String] = List.empty[String]
 
   @ArgOption(name = "--since", aliases = Array("--after", "-b"), metaVar = "QUERY",
     usage = "(included) begin timestamp. Must be in the format 2006-01-02[ 15:04:05[.890][ -0700]]")
@@ -125,11 +133,17 @@
     usage = "Extract a list of issues and links using the Gerrit's commentLink configuration")
   private var extractIssues: Boolean = false
 
+  @ArgOption(name = "--botlike-filename-regexps", aliases = Array("-n"),
+    usage = "comma separated list of regexps that identify a bot-like commit, commits that modify only files whose name is a match will be flagged as bot-like")
+  def setBotLikeRegexps(value: String): Unit = {
+    botLikeRegexps = value.split(",").toList
+  }
+
   override def apply(projectRes: ProjectResource) =
     Response.ok(
       new GsonStreamedResult[UserActivitySummary](gson,
         executor.get(projectRes, beginDate, endDate,
-          granularity.getOrElse(AggregationStrategy.EMAIL), extractBranches, extractIssues)))
+          granularity.getOrElse(AggregationStrategy.EMAIL), extractBranches, extractIssues, botLikeRegexps)))
 }
 
 class ContributorsService @Inject()(repoManager: GitRepositoryManager,
@@ -141,7 +155,7 @@
   import scala.collection.JavaConverters._
 
   def get(projectRes: ProjectResource, startDate: Option[Long], stopDate: Option[Long],
-          aggregationStrategy: AggregationStrategy, extractBranches: Boolean, extractIssues: Boolean)
+          aggregationStrategy: AggregationStrategy, extractBranches: Boolean, extractIssues: Boolean, botLikeIdentifiers: List[String])
   : TraversableOnce[UserActivitySummary] = {
     val nameKey = projectRes.getNameKey
     val commentLinks: List[CommentLinkInfo] = extractIssues.option {
@@ -149,9 +163,8 @@
     }.toList.flatten
 
 
-
     ManagedResource.use(repoManager.openRepository(projectRes.getNameKey)) { repo =>
-      val stats = new Statistics(repo, commentLinks.asJava)
+      val stats = new Statistics(repo, new BotLikeExtractorImpl(botLikeIdentifiers), commentLinks.asJava)
       val branchesExtractor = extractBranches.option(new BranchesExtractor(repo))
 
       histogram.get(repo, new AggregatedHistogramFilterByDates(startDate, stopDate, branchesExtractor, aggregationStrategy))
@@ -162,7 +175,7 @@
   }
 }
 
-case class CommitInfo(sha1: String, date: Long, merge: Boolean, files: java.util.Set[String])
+case class CommitInfo(sha1: String, date: Long, merge: Boolean, botLike: Boolean, files: java.util.Set[String])
 
 case class IssueInfo(code: String, link: String)
 
@@ -182,7 +195,8 @@
                                issuesCodes: Array[String],
                                issuesLinks: Array[String],
                                lastCommitDate: Long,
-                               isMerge: Boolean
+                               isMerge: Boolean,
+                               isBotLike: Boolean
                               )
 
 object UserActivitySummary {
@@ -202,7 +216,7 @@
             stat.numFiles, stat.numDistinctFiles, stat.addedLines, stat.deletedLines,
             stat.commits.toArray, maybeBranches, stat.issues.map(_.code)
               .toArray, stat.issues.map(_.link).toArray, uca.getLatest, stat
-              .isForMergeCommits
+              .isForMergeCommits,stat.isForBotLike
           )
         }
       case _ => throw new Exception(s"invalid key format found ${uca.key}")
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/BotLikeExtractor.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/BotLikeExtractor.scala
new file mode 100644
index 0000000..16a8bf2
--- /dev/null
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/BotLikeExtractor.scala
@@ -0,0 +1,26 @@
+// Copyright (C) 2019 GerritForge Ltd
+//
+// 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.analytics.common
+
+import scala.util.matching.Regex
+
+trait BotLikeExtractor {
+  def isBotLike(files: Set[String]): Boolean
+}
+
+class BotLikeExtractorImpl(botLikeIdentifiers: List[String]) extends BotLikeExtractor {
+  private val botRegexps = new Regex(botLikeIdentifiers.mkString("|"))
+  override def isBotLike(files: Set[String]): Boolean = botLikeIdentifiers.nonEmpty && files.forall(botRegexps.findFirstIn(_).isDefined)
+}
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/CommitsStatistics.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/CommitsStatistics.scala
index bb5c0e2..e5f9723 100644
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/CommitsStatistics.scala
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/CommitsStatistics.scala
@@ -35,15 +35,18 @@
   * @param isForMergeCommits    true if the current instance is including stats for merge commits and false if
   *                             calculated for NON merge commits. The current code is not generating stats objects for
   *                             a mixture of merge and non-merge commits
+  * @param isForBotLike         true if the current instance is including BOT-like commits, false otherwise
   * @param commits              list of commits the stats are calculated for
   */
 case class CommitsStatistics(
                               addedLines: Int,
                               deletedLines: Int,
                               isForMergeCommits: Boolean,
+                              isForBotLike: Boolean,
                               commits: List[CommitInfo],
                               issues: List[IssueInfo] = Nil
                             ) {
+  require(commits.forall(_.botLike == isForBotLike), s"Creating a stats object with isForBotLike = $isForBotLike but containing commits of different type")
   require(commits.forall(_.merge == isForMergeCommits), s"Creating a stats object with isMergeCommit = $isForMergeCommits but containing commits of different type")
 
   /**
@@ -73,11 +76,13 @@
 }
 
 object CommitsStatistics {
-  val Empty = CommitsStatistics(0, 0, false, List[CommitInfo](), List[IssueInfo]())
-  val EmptyMerge = Empty.copy(isForMergeCommits = true)
+  val EmptyNonMerge = CommitsStatistics(0, 0, false, false, List[CommitInfo](), List[IssueInfo]())
+  val EmptyBotNonMerge = EmptyNonMerge.copy(isForBotLike = true)
+  val EmptyMerge = EmptyNonMerge.copy(isForMergeCommits = true)
+  val EmptyBotMerge = EmptyMerge.copy(isForBotLike = true)
 }
 
-class Statistics(repo: Repository, commentInfoList: java.util.List[CommentLinkInfo] = Nil) {
+class Statistics(repo: Repository, botLikeExtractor: BotLikeExtractor, commentInfoList: java.util.List[CommentLinkInfo] = Nil) {
 
   val log = LoggerFactory.getLogger(classOf[Statistics])
   val replacers = commentInfoList.map(info =>
@@ -86,7 +91,11 @@
       Option(info.link).getOrElse(info.html)))
 
   /**
-    * Returns up to two different CommitsStatistics object grouping the stats into merge and non-merge commits
+    * Returns up to four different CommitsStatistics object grouping the stats into:
+    * Non Merge - Non Bot
+    * Merge     - Non Bot
+    * Non Merge - Bot
+    * Merge     - Bot
     *
     * @param commits
     * @return
@@ -97,10 +106,16 @@
 
     val (mergeStatsSeq, nonMergeStatsSeq) = stats.partition(_.isForMergeCommits)
 
-    val nonMergeStats = nonMergeStatsSeq.foldLeft(CommitsStatistics.Empty)(_ + _)
-    val mergeStats = mergeStatsSeq.foldLeft(CommitsStatistics.EmptyMerge)(_ + _)
+    val (mergeBotStatsSeq, mergeNonBotStatsSeq) = mergeStatsSeq.partition(_.isForBotLike)
+    val (nonMergeBotStatsSeq, nonMergeNonBotStatsSeq) = nonMergeStatsSeq.partition(_.isForBotLike)
 
-    List(nonMergeStats, mergeStats).filterNot(_.isEmpty)
+    List(
+      nonMergeNonBotStatsSeq.foldLeft(CommitsStatistics.EmptyNonMerge)(_ + _), // Non Merge - Non Bot
+      mergeNonBotStatsSeq.foldLeft(CommitsStatistics.EmptyMerge)(_ + _),       // Merge     - Non Bot
+      nonMergeBotStatsSeq.foldLeft(CommitsStatistics.EmptyBotNonMerge)(_ + _), // Non Merge - Bot
+      mergeBotStatsSeq.foldLeft(CommitsStatistics.EmptyBotMerge)(_ + _)        // Merge     - Bot
+    )
+    .filterNot(_.isEmpty)
   }
 
   protected def forSingleCommit(objectId: ObjectId): CommitsStatistics = {
@@ -138,9 +153,9 @@
 
       val files: Set[String] = diffs.map(df.toFileHeader(_).getNewPath).toSet
 
-      val commitInfo = CommitInfo(objectId.getName, commit.getAuthorIdent.getWhen.getTime, commit.isMerge, files)
+      val commitInfo = CommitInfo(objectId.getName, commit.getAuthorIdent.getWhen.getTime, commit.isMerge, botLikeExtractor.isBotLike(files), files)
 
-      CommitsStatistics(lines.added, lines.deleted, commitInfo.merge, List(commitInfo), extractIssues(commitMessage))
+      CommitsStatistics(lines.added, lines.deleted, commitInfo.merge, commitInfo.botLike, List(commitInfo), extractIssues(commitMessage))
     }
   }
 
diff --git a/src/test/scala/com/googlesource/gerrit/plugins/analytics/common/BotLikeExtractorImplSpec.scala b/src/test/scala/com/googlesource/gerrit/plugins/analytics/common/BotLikeExtractorImplSpec.scala
new file mode 100644
index 0000000..2893a31
--- /dev/null
+++ b/src/test/scala/com/googlesource/gerrit/plugins/analytics/common/BotLikeExtractorImplSpec.scala
@@ -0,0 +1,51 @@
+// Copyright (C) 2019 GerritForge Ltd
+//
+// 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.analytics.common
+
+import com.googlesource.gerrit.plugins.analytics.test.GitTestCase
+import org.scalatest.{FlatSpec, Matchers}
+
+class BotLikeExtractorImplSpec extends FlatSpec with Matchers with GitTestCase {
+
+  behavior of "isBotLike"
+
+  it should "return true when all files match bot-like identifiers" in {
+    val extractor = new BotLikeExtractorImpl(List(""".+\.xml"""))
+
+    extractor.isBotLike(Set(
+      "some/path/AFile.xml",
+      "some/path/AnotherFile.xml"
+    )).shouldBe(true)
+
+  }
+
+  it should "return false when at least one file does not match bot-like identifiers" in {
+    val extractor = new BotLikeExtractorImpl(List(""".+\.xml"""))
+
+    extractor.isBotLike(Set(
+      "some/path/AFile.xml",
+      "some/path/AnotherFile.someExtension"
+    )).shouldBe(false)
+
+  }
+
+  it should "return false when no bot-like identifiers have been provided" in {
+    val extractor = new BotLikeExtractorImpl(List.empty)
+
+    extractor.isBotLike(Set("some/path/anyFile")).shouldBe(false)
+
+  }
+
+}
diff --git a/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/CommitInfoSpec.scala b/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/CommitInfoSpec.scala
index 7de2c03..c8aff83 100644
--- a/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/CommitInfoSpec.scala
+++ b/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/CommitInfoSpec.scala
@@ -8,14 +8,14 @@
 class CommitInfoSpec extends FlatSpec with Matchers {
 
   "CommitInfo" should "be serialised as JSON correctly" in {
-    val commitInfo = CommitInfo(sha1 = "sha", date = 1000l, merge = false, files = newHashSet("file1", "file2"))
+    val commitInfo = CommitInfo(sha1 = "sha", date = 1000l, merge = false, botLike = false, files = newHashSet("file1", "file2"))
 
     val gsonBuilder = OutputFormat.JSON_COMPACT.newGsonBuilder
 
     val actual = gsonBuilder.create().toJson(commitInfo)
     List(actual) should contain oneOf(
-      "{\"sha1\":\"sha\",\"date\":1000,\"merge\":false,\"files\":[\"file1\",\"file2\"]}",
-      "{\"sha1\":\"sha\",\"date\":1000,\"merge\":false,\"files\":[\"file2\",\"file1\"]}"
+      "{\"sha1\":\"sha\",\"date\":1000,\"merge\":false,\"bot_like\":false,\"files\":[\"file1\",\"file2\"]}",
+      "{\"sha1\":\"sha\",\"date\":1000,\"merge\":false,\"bot_like\":false,\"files\":[\"file2\",\"file1\"]}"
     )
   }
 
diff --git a/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/CommitStatisticsCommentLinkSpec.scala b/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/CommitStatisticsCommentLinkSpec.scala
index 95784fd..c9031c6 100644
--- a/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/CommitStatisticsCommentLinkSpec.scala
+++ b/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/CommitStatisticsCommentLinkSpec.scala
@@ -39,7 +39,7 @@
                           createCommentLinkInfo(pattern = "([Bb]ug:\\s+)(\\d+)",
                             html = Some("$1<a href=\"http://trak.example.com/$2\">$2</a>"))).asJava) {
 
-    lazy val stats = new Statistics(repo, commentLinks)
+    lazy val stats = new Statistics(repo, TestBotLikeExtractor, commentLinks)
   }
 
   it should "collect no commentslink if no matching" in new TestEnvironment {
diff --git a/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/CommitStatisticsSpec.scala b/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/CommitStatisticsSpec.scala
index 6db1d00..d5257ef 100644
--- a/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/CommitStatisticsSpec.scala
+++ b/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/CommitStatisticsSpec.scala
@@ -25,7 +25,7 @@
 
   class TestEnvironment {
     val repo = new FileRepository(testRepo)
-    val stats = new Statistics(repo)
+    val stats = new Statistics(repo, TestBotLikeExtractor)
   }
 
   "CommitStatistics" should "stats a single file added" in new TestEnvironment {
@@ -39,19 +39,19 @@
   }
 
   it should "sum to another compatible CommitStatistics generating an aggregated stat" in {
-    val commit1 = CommitInfo("sha_1", 1000l, false, newHashSet("file1"))
-    val commit2 = CommitInfo("sha_2", 2000l, false, newHashSet("file1"))
-    val commit3 = CommitInfo("sha_3", 3000l, false, newHashSet("file2"))
-    val commit4 = CommitInfo("sha_4", 1000l, false, newHashSet("file1"))
+    val commit1 = CommitInfo("sha_1", 1000l, false, botLike = false, newHashSet("file1"))
+    val commit2 = CommitInfo("sha_2", 2000l, false, botLike = false, newHashSet("file1"))
+    val commit3 = CommitInfo("sha_3", 3000l, false, botLike = false, newHashSet("file2"))
+    val commit4 = CommitInfo("sha_4", 1000l, false, botLike = false, newHashSet("file1"))
 
-    val stat1 = CommitsStatistics(3, 4, false, List(commit1, commit2))
-    val stat2 = CommitsStatistics(5, 7, false, List(commit3, commit4))
+    val stat1 = CommitsStatistics(3, 4, false, false, List(commit1, commit2))
+    val stat2 = CommitsStatistics(5, 7, false, false, List(commit3, commit4))
 
-    (stat1 + stat2) shouldBe CommitsStatistics(8, 11, false, List(commit1, commit2, commit3, commit4))
+    (stat1 + stat2) shouldBe CommitsStatistics(8, 11, false, false, List(commit1, commit2, commit3, commit4))
   }
 
   it should "fail if trying to be added to a CommitStatistics object for a different isMerge value" in {
-    an [IllegalArgumentException] should be thrownBy  (CommitsStatistics.EmptyMerge + CommitsStatistics.Empty)
+    an [IllegalArgumentException] should be thrownBy  (CommitsStatistics.EmptyMerge + CommitsStatistics.EmptyNonMerge)
   }
 
   it should "stats multiple files added" in new TestEnvironment {
diff --git a/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/TestBotLikeExtractor.scala b/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/TestBotLikeExtractor.scala
new file mode 100644
index 0000000..3018436
--- /dev/null
+++ b/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/TestBotLikeExtractor.scala
@@ -0,0 +1,21 @@
+// Copyright (C) 2019 GerritForge Ltd
+//
+// 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.analytics.test
+
+import com.googlesource.gerrit.plugins.analytics.common.BotLikeExtractor
+
+case object TestBotLikeExtractor extends BotLikeExtractor {
+  override def isBotLike(files: Set[String]): Boolean = false
+}
diff --git a/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/UserActivitySummarySpec.scala b/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/UserActivitySummarySpec.scala
index 57417cd..de4d2f2 100644
--- a/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/UserActivitySummarySpec.scala
+++ b/src/test/scala/com/googlesource/gerrit/plugins/analytics/test/UserActivitySummarySpec.scala
@@ -18,7 +18,7 @@
     mergeCommit(personEmail, fileName = "anotherFile.txt", content="some other content")
     val aggregatedCommits = aggregateBy(EMAIL)
 
-    val List(nonMergeSummary, mergeSummary) = UserActivitySummary.apply(new Statistics(repo))(aggregatedCommits.head)
+    val List(nonMergeSummary, mergeSummary) = UserActivitySummary.apply(new Statistics(repo, TestBotLikeExtractor))(aggregatedCommits.head)
 
     nonMergeSummary.numCommits should be(2)
     mergeSummary.numCommits should be(1)