blob: 34cae77dad0af6b18eb8bc46209094272fa8dd60 [file] [log] [blame]
// 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
import com.google.gerrit.extensions.api.projects.CommentLinkInfo
import com.google.gerrit.extensions.restapi.{BadRequestException, Response, RestReadView}
import com.google.gerrit.server.git.GitRepositoryManager
import com.google.gerrit.server.project.{ProjectCache, ProjectResource}
import com.google.gerrit.server.restapi.project.ProjectsCollection
import com.google.gerrit.sshd.{CommandMetaData, SshCommand}
import com.google.inject.Inject
import com.googlesource.gerrit.plugins.analytics.common.DateConversions._
import com.googlesource.gerrit.plugins.analytics.common._
import org.kohsuke.args4j.{Option => ArgOption}
@CommandMetaData(name = "contributors", description = "Extracts the list of contributors to a project")
class ContributorsCommand @Inject()(val executor: ContributorsService,
val projects: ProjectsCollection,
val gsonFmt: GsonFormatter)
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]]")
def setBeginDate(date: String) {
try {
beginDate = Some(date)
} catch {
case e: Exception => throw die(s"Invalid begin date ${e.getMessage}")
}
}
@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) {
try {
endDate = Some(date)
} catch {
case e: Exception => throw die(s"Invalid end date ${e.getMessage}")
}
}
@ArgOption(name = "--aggregate", aliases = Array("-g"),
usage = "Type of aggregation requested. ")
def setGranularity(value: String) {
try {
granularity = Some(AggregationStrategy.apply(value))
} catch {
case e: Exception => throw die(s"Invalid granularity ${e.getMessage}")
}
}
@ArgOption(name = "--extract-issues", aliases = Array("-i"),
usage = "Extract a list of issues and links using the Gerrit's commentLink configuration")
private var extractIssues: Boolean = false
override protected def run =
gsonFmt.format(executor.get(projectRes, beginDate, endDate,
granularity.getOrElse(AggregationStrategy.EMAIL), extractBranches, extractIssues), stdout)
}
class ContributorsResource @Inject()(val executor: ContributorsService,
val gson: GsonFormatter)
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]]")
def setBeginDate(date: String) {
try {
beginDate = Some(date)
} catch {
case e: Exception => throw new BadRequestException(s"Invalid begin date ${e.getMessage}")
}
}
@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) {
try {
endDate = Some(date)
} catch {
case e: Exception => throw new BadRequestException(s"Invalid end date ${e.getMessage}")
}
}
@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) {
try {
granularity = Some(AggregationStrategy.apply(value))
} catch {
case e: Exception => throw new BadRequestException(s"Invalid granularity ${e.getMessage}")
}
}
@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 = "--extract-issues", aliases = Array("-i"),
usage = "Extract a list of issues and links using the Gerrit's commentLink configuration")
private var extractIssues: Boolean = false
override def apply(projectRes: ProjectResource) =
Response.ok(
new GsonStreamedResult[UserActivitySummary](gson,
executor.get(projectRes, beginDate, endDate,
granularity.getOrElse(AggregationStrategy.EMAIL), extractBranches, extractIssues)))
}
class ContributorsService @Inject()(repoManager: GitRepositoryManager,
projectCache:ProjectCache,
histogram: UserActivityHistogram,
gsonFmt: GsonFormatter) {
import RichBoolean._
import scala.collection.JavaConverters._
def get(projectRes: ProjectResource, startDate: Option[Long], stopDate: Option[Long],
aggregationStrategy: AggregationStrategy, extractBranches: Boolean, extractIssues: Boolean)
: TraversableOnce[UserActivitySummary] = {
val nameKey = projectRes.getNameKey
val commentLinks: List[CommentLinkInfo] = extractIssues.option {
projectCache.get(nameKey).getCommentLinks.asScala
}.toList.flatten
ManagedResource.use(repoManager.openRepository(projectRes.getNameKey)) { repo =>
val stats = new Statistics(repo, commentLinks.asJava)
val branchesExtractor = extractBranches.option(new BranchesExtractor(repo))
histogram.get(repo, new AggregatedHistogramFilterByDates(startDate, stopDate, branchesExtractor, aggregationStrategy))
.par
.flatMap(aggregatedCommitActivity => UserActivitySummary.apply(stats)(aggregatedCommitActivity))
.toStream
}
}
}
case class CommitInfo(sha1: String, date: Long, merge: Boolean, files: java.util.Set[String])
case class IssueInfo(code: String, link: String)
case class UserActivitySummary(year: Integer,
month: Integer,
day: Integer,
hour: Integer,
name: String,
email: String,
numCommits: Integer,
numFiles: Integer,
numDistinctFiles: Integer,
addedLines: Integer,
deletedLines: Integer,
commits: Array[CommitInfo],
branches: Array[String],
issuesCodes: Array[String],
issuesLinks: Array[String],
lastCommitDate: Long,
isMerge: Boolean
)
object UserActivitySummary {
def apply(statisticsHandler: Statistics)(uca: AggregatedUserCommitActivity)
: Iterable[UserActivitySummary] = {
implicit def stringToIntOrNull(x: String): Integer = if (x.isEmpty) null else new Integer(x)
uca.key.split("/", AggregationStrategy.MAX_MAPPING_TOKENS) match {
case Array(email, year, month, day, hour, branch) =>
statisticsHandler.forCommits(uca.getIds: _*).map { stat =>
val maybeBranches =
Option(branch).filter(_.nonEmpty).map(b => Array(b)).getOrElse(Array.empty)
UserActivitySummary(
year, month, day, hour, uca.getName, email, stat.commits.size,
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
)
}
case _ => throw new Exception(s"invalid key format found ${uca.key}")
}
}
}