Aggregate contributors by branch
If using the --extract-branches or -r flag then for each output line it is
optionally added the set of branches to which the commits belong.
Jira-Id: GERICS-513
Change-Id: I434497b9b6610ce68891465be5e7aac6147834b6
diff --git a/README.md b/README.md
index 647bd92..1146e93 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@
### Contributors
Extract a unordered list of project contributors statistics, including the
-commits data relevant for statistics purposes, such as number of involved files,
+commits data relevant for statistics purposes, such as number of involved files, and optionally also the list of belonging branches,
number of added/deleted lines, timestamp and merge flag.
@@ -65,6 +65,7 @@
- --since -b Starting timestamp to consider
- --until -e Ending timestamp (excluded) to consider
- --aggregate -granularity -g one of email, email_year, email_month, email_day, email_hour defaulting to aggregation by email
+- --extract-branches -r enables branches extraction for each commit
NOTE: Timestamp format is consistent with Gerrit's query syntax, see /Documentation/user-search.html for details.
@@ -73,7 +74,7 @@
```
$ 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}]}
+ {"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","branch1"]}
```
SSH Example:
@@ -81,6 +82,6 @@
```
$ ssh -p 29418 admin@gerrit.mycompany.com analytics contributors myproject --since 2017-08-01 --until 2017-12-31
{"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}]}
+ {"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","branch1"]}
```
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 6e16763..faefac8 100644
--- a/src/main/scala/com/googlesource/gerrit/plugins/analytics/Contributors.scala
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/Contributors.scala
@@ -21,7 +21,6 @@
import com.google.inject.Inject
import com.googlesource.gerrit.plugins.analytics.common.DateConversions._
import com.googlesource.gerrit.plugins.analytics.common._
-import org.eclipse.jgit.lib.{ObjectId, Repository}
import org.kohsuke.args4j.{Option => ArgOption}
@@ -32,6 +31,12 @@
extends SshCommand with ProjectResourceParser {
private var beginDate: Option[Long] = None
+ private var endDate: Option[Long] = None
+ private var granularity: Option[AggregationStrategy] = None
+
+ @ArgOption(name = "--extract-branches", aliases = Array("-r"),
+ usage = "Do extra parsing to extract a list of all branches for each line")
+ private var extractBranches: Boolean = false
@ArgOption(name = "--since", aliases = Array("--after", "-b"),
usage = "(included) begin timestamp. Must be in the format 2006-01-02[ 15:04:05[.890][ -0700]]")
@@ -43,8 +48,6 @@
}
}
- private var endDate: Option[Long] = None
-
@ArgOption(name = "--until", aliases = Array("--before", "-e"),
usage = "(excluded) end timestamp. Must be in the format 2006-01-02[ 15:04:05[.890][ -0700]]")
def setEndDate(date: String) {
@@ -55,8 +58,6 @@
}
}
- private var granularity: Option[AggregationStrategy] = None
-
@ArgOption(name = "--aggregate", aliases = Array("-g"),
usage = "Type of aggregation requested. ")
def setGranularity(value: String) {
@@ -67,10 +68,9 @@
}
}
-
override protected def run =
gsonFmt.format(executor.get(projectRes, beginDate, endDate,
- granularity.getOrElse(AggregationStrategy.EMAIL)), stdout)
+ granularity.getOrElse(AggregationStrategy.EMAIL), extractBranches), stdout)
}
@@ -79,6 +79,8 @@
extends RestReadView[ProjectResource] {
private var beginDate: Option[Long] = None
+ private var endDate: Option[Long] = None
+ private var granularity: Option[AggregationStrategy] = None
@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]]")
@@ -90,8 +92,6 @@
}
}
- private var endDate: Option[Long] = None
-
@ArgOption(name = "--until", aliases = Array("--before", "-e"), metaVar = "QUERY",
usage = "(excluded) end timestamp. Must be in the format 2006-01-02[ 15:04:05[.890][ -0700]]")
def setEndDate(date: String) {
@@ -102,8 +102,6 @@
}
}
- private var granularity: Option[AggregationStrategy] = None
-
@ArgOption(name = "--granularity", aliases = Array("--aggregate", "-g"), metaVar = "QUERY",
usage = "can be one of EMAIL, EMAIL_HOUR, EMAIL_DAY, EMAIL_MONTH, EMAIL_YEAR, defaulting to EMAIL")
def setGranularity(value: String) {
@@ -114,25 +112,33 @@
}
}
+ @ArgOption(name = "--extract-branches", aliases = Array("-r"),
+ usage = "Do extra parsing to extract a list of all branches for each line")
+ private var extractBranches: Boolean = false
+
override def apply(projectRes: ProjectResource) =
Response.ok(
new GsonStreamedResult[UserActivitySummary](gson,
executor.get(projectRes, beginDate, endDate,
- granularity.getOrElse(AggregationStrategy.EMAIL))))
+ granularity.getOrElse(AggregationStrategy.EMAIL), extractBranches)))
}
class ContributorsService @Inject()(repoManager: GitRepositoryManager,
histogram: UserActivityHistogram,
gsonFmt: GsonFormatter) {
-
def get(projectRes: ProjectResource, startDate: Option[Long], stopDate: Option[Long],
- aggregationStrategy: AggregationStrategy): TraversableOnce[UserActivitySummary] = {
+ aggregationStrategy: AggregationStrategy, extractBranches: Boolean)
+ : TraversableOnce[UserActivitySummary] = {
ManagedResource.use(repoManager.openRepository(projectRes.getNameKey)) { repo =>
val stats = new Statistics(repo)
+ import RichBoolean._
+ val commitsBranchesOptionalEnricher = extractBranches.option(
+ new CommitsBranches(repo, startDate, stopDate)
+ )
histogram.get(repo, new AggregatedHistogramFilterByDates(startDate, stopDate,
aggregationStrategy))
.par
- .flatMap(UserActivitySummary.apply(stats))
+ .flatMap(UserActivitySummary.apply(stats, commitsBranchesOptionalEnricher))
.toStream
}
}
@@ -151,25 +157,32 @@
addedLines: Integer,
deletedLines: Integer,
commits: Array[CommitInfo],
+ branches: Array[String],
lastCommitDate: Long,
isMerge: Boolean
)
object UserActivitySummary {
- def apply(statisticsHandler: Statistics)(uca: AggregatedUserCommitActivity): Iterable[UserActivitySummary] = {
+ def apply(statisticsHandler: Statistics,
+ branchesLabeler: Option[CommitsBranches])
+ (uca: AggregatedUserCommitActivity)
+ : Iterable[UserActivitySummary] = {
val INCLUDESEMPTY = -1
implicit def stringToIntOrNull(x: String): Integer = if (x.isEmpty) null else new Integer(x)
uca.key.split("/", INCLUDESEMPTY) match {
case Array(email, year, month, day, hour) =>
+ val branches = branchesLabeler.fold(Set.empty[String]) {
+ labeler => labeler.forCommits(uca.getIds)
+ }
statisticsHandler.forCommits(uca.getIds: _*).map { stat =>
UserActivitySummary(
year, month, day, hour, uca.getName, uca.getEmail, uca.getCount,
- stat.numFiles, stat.addedLines, stat.deletedLines, stat.commits.toArray, uca.getLatest, stat.isForMergeCommits
+ stat.numFiles, stat.addedLines, stat.deletedLines,
+ stat.commits.toArray, branches.toArray, uca.getLatest, stat.isForMergeCommits
)
}
-
case _ => throw new Exception(s"invalid key format found ${uca.key}")
}
}
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/CommitsBranches.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/CommitsBranches.scala
new file mode 100644
index 0000000..46cca17
--- /dev/null
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/CommitsBranches.scala
@@ -0,0 +1,53 @@
+// Copyright (C) 2017 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.analytics.common
+
+import com.googlesource.gerrit.plugins.analytics.common.ManagedResource.use
+import org.eclipse.jgit.api.Git
+import org.eclipse.jgit.lib.{Constants, ObjectId, Repository}
+import org.eclipse.jgit.revwalk.RevWalk
+import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter
+
+import scala.collection.JavaConversions._
+import scala.collection.mutable
+
+class CommitsBranches(repo: Repository, from: Option[Long] = None,
+ to: Option[Long] = None) {
+
+ def forCommits(objectIds: TraversableOnce[ObjectId]): Set[String] = {
+ val commitToBranchesMap = new mutable.HashMap[String, mutable.Set[String]]
+ with mutable.MultiMap[String, String]
+ use(new Git(repo)) { git =>
+ val refs = git.branchList.call
+ for (ref <- refs) {
+ val branchName = ref.getName.drop(Constants.R_HEADS.length)
+ use(new RevWalk(repo)) { rw: RevWalk =>
+ from.foreach(d1 => rw.setRevFilter(CommitTimeRevFilter.after(d1)))
+ to.foreach(d2 => rw.setRevFilter(CommitTimeRevFilter.before(d2)))
+ rw.markStart(rw.parseCommit(ref.getObjectId))
+ rw.foreach { rev =>
+ val sha1 = rev.getName
+ commitToBranchesMap.addBinding(sha1, branchName)
+ }
+ }
+ }
+ objectIds.foldLeft(Set.empty[String]) {
+ (branches, objectId) => {
+ branches ++ commitToBranchesMap(objectId.getName)
+ }
+ }.filter(_.nonEmpty)
+ }
+ }
+}
diff --git a/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/RichBoolean.scala b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/RichBoolean.scala
new file mode 100644
index 0000000..ff4cdcf
--- /dev/null
+++ b/src/main/scala/com/googlesource/gerrit/plugins/analytics/common/RichBoolean.scala
@@ -0,0 +1,22 @@
+// Copyright (C) 2018 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.analytics.common
+
+object RichBoolean {
+ implicit class RichBoolean(val b: Boolean) extends AnyVal {
+ final def option[A](a: => A): Option[A] = if (b) Some(a) else None
+ }
+
+}
diff --git a/src/test/scala/com/googlesource/gerrit/plugins/analytics/common/CommitsBranchesTest.scala b/src/test/scala/com/googlesource/gerrit/plugins/analytics/common/CommitsBranchesTest.scala
new file mode 100644
index 0000000..ad9f712
--- /dev/null
+++ b/src/test/scala/com/googlesource/gerrit/plugins/analytics/common/CommitsBranchesTest.scala
@@ -0,0 +1,27 @@
+package com.googlesource.gerrit.plugins.analytics.common
+
+import com.googlesource.gerrit.plugins.analytics.test.GitTestCase
+import org.eclipse.jgit.internal.storage.file.FileRepository
+import org.scalatest.{FlatSpec, Matchers}
+
+class CommitsBranchesTest extends FlatSpec with Matchers with GitTestCase {
+ def commitsBranches = new CommitsBranches(new FileRepository(testRepo))
+
+ "getAllCommitsLabeledWithBranches" should "label correctly a set of " +
+ "commits" in {
+ val c1 = add("file", "content")
+ val c2 = add("file2", "content")
+ val c3 = add("file3", "content")
+ val c4 = add("file4", "content")
+ branch("feature/branch")
+ val c5 = add("fileOnBranch", "content2")
+ val c6 = add("fileOnBranch2", "content2")
+ val c7 = add("fileOnBranch3", "content2")
+ val c8 = add("fileOnBranch4", "content2")
+
+ commitsBranches.forCommits(Seq(c1, c2, c3, c4)) should be(
+ Set("master", "feature/branch"))
+
+ commitsBranches.forCommits(Seq(c7, c8)) should be(Set("feature/branch"))
+ }
+}