Skip to content

Commit ccbe617

Browse files
authored
Merge pull request #45 from aspyker/issues_labels
Issues labels - PR for issue #43 #43
2 parents 372e032 + dc674c7 commit ccbe617

File tree

11 files changed

+2998
-23
lines changed

11 files changed

+2998
-23
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ build/
55
*.iws
66
*.ipr
77
.idea/
8+
*/out
89
osstracker-scrapernetflixapp
10+
cassandra_data
11+
es_data

osstracker-scraper/build.gradle

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ sourceCompatibility = 1.8
44
targetCompatibility = 1.8
55

66
dependencies {
7-
compile 'org.scala-lang:scala-library:2.11.7'
7+
compile 'org.scala-lang:scala-library:2.12.4'
88
compile 'org.slf4j:slf4j-api:1.7.13'
99
compile 'org.slf4j:slf4j-log4j12:1.7.13'
1010
compile 'org.kohsuke:github-api:1.92'
11-
compile 'com.typesafe.play:play-json_2.11:2.4.5'
11+
compile 'com.typesafe.play:play-json_2.12:2.6.8'
1212
compile 'joda-time:joda-time:2.9.1'
13-
compile 'org.rogach:scallop_2.11:0.9.5'
13+
compile 'org.rogach:scallop_2.12:3.1.1'
1414
compile 'com.datastax.cassandra:cassandra-driver-core:3.0.0'
1515
compile 'org.apache.httpcomponents:httpclient:4.5.1'
16+
testCompile 'org.scalatest:scalatest_2.12:3.0.4'
1617
}

osstracker-scraper/src/main/scala/com/netflix/oss/tools/osstrackerscraper/Conf.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import org.rogach.scallop.ScallopConf
1919

2020
class Conf(args: Seq[String]) extends ScallopConf(args) {
2121
val action = opt[String](required = true)
22+
verify()
2223
}
2324

2425
object Conf {

osstracker-scraper/src/main/scala/com/netflix/oss/tools/osstrackerscraper/GithubAccess.scala

Lines changed: 158 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,31 @@ import java.util.{Date, Properties}
2020

2121
import com.netflix.oss.tools.osstrackerscraper.OssLifecycle.OssLifecycle
2222
import org.kohsuke.github._
23-
import org.slf4j.LoggerFactory
24-
import play.api.libs.json.{Json, JsObject}
23+
import org.slf4j.{Logger, LoggerFactory}
24+
import play.api.libs.json.{JsObject, Json}
25+
2526
import scala.collection.JavaConversions._
2627

2728
case class CommitInfo(numCommits: Int, daysSinceLastCommit: Int, contributorLogins: List[String]) {}
28-
case class IssuesInfo(val closedIssuesSize: Int, val openIssuesSize: Int, val avgIssues: Int) {}
29+
case class IssuesInfo(
30+
val closedCount: Int,
31+
val openCount: Int,
32+
val avgDayToClose: Int,
33+
val openCountWithNoLabels: Int,
34+
val openCountWithLabelBug: Int,
35+
val openCountWithLabelDuplicate: Int,
36+
val openCountWithLabelEnhancement: Int,
37+
val openCountWithLabelHelpWanted: Int,
38+
val openCountWithLabelInvalid: Int,
39+
val openCountWithLabelQuestion: Int,
40+
val openCountWithLabelWontfix: Int,
41+
val openCountTrulyOpen: Int
42+
) {}
2943
case class PRsInfo(val closedPRsSize: Int, val avgPRs: Int) {}
3044

31-
class GithubAccess(val asOfYYYYMMDD: String, val asOfISO: String) {
45+
class GithubAccess(val asOfYYYYMMDD: String, val asOfISO: String, val connectToGithub: Boolean) {
3246
val logger = LoggerFactory.getLogger(getClass)
33-
val github: GitHub = GitHub.connect()
47+
val github: Option[GitHub] = if (connectToGithub) Some(GitHub.connect()) else None
3448

3549
def getOSSMetaDataOSSLifecycle(repo: GHRepository): OssLifecycle = {
3650
try {
@@ -61,7 +75,7 @@ class GithubAccess(val asOfYYYYMMDD: String, val asOfISO: String) {
6175

6276
val (commitInfo: CommitInfo, issuesInfo: IssuesInfo, prsInfo: PRsInfo) = if (neverPushed) {
6377
logger.warn("repo has never been pushed, so providing fake zero counts for issues and pull requests")
64-
(CommitInfo(0, 0, List[String]()), IssuesInfo(0, 0, 0), PRsInfo(0, 0))
78+
(CommitInfo(0, 0, List[String]()), IssuesInfo(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), PRsInfo(0, 0))
6579
} else {
6680
val commitInfo = getCommitInfo(repo)
6781
val issuesInfo = getIssuesStats(repo)
@@ -79,9 +93,19 @@ class GithubAccess(val asOfYYYYMMDD: String, val asOfISO: String) {
7993
"stars" -> repo.getWatchers(),
8094
"numContributors" -> commitInfo.contributorLogins.size,
8195
"issues" -> Json.obj(
82-
"openCount" -> issuesInfo.openIssuesSize,
83-
"closedCount" -> issuesInfo.closedIssuesSize,
84-
"avgTimeToCloseInDays" -> issuesInfo.avgIssues
96+
"openCount" -> issuesInfo.openCount,
97+
"openCountOnlyIssues" -> issuesInfo.openCountTrulyOpen,
98+
"closedCount" -> issuesInfo.closedCount,
99+
"avgTimeToCloseInDays" -> issuesInfo.avgDayToClose,
100+
"openCountByStandardTags" -> Json.obj(
101+
"bug" -> issuesInfo.openCountWithLabelBug,
102+
"helpWanted" -> issuesInfo.openCountWithLabelHelpWanted,
103+
"question" -> issuesInfo.openCountWithLabelQuestion,
104+
"duplicate" -> issuesInfo.openCountWithLabelDuplicate,
105+
"enhancement" -> issuesInfo.openCountWithLabelEnhancement,
106+
"invalid" -> issuesInfo.openCountWithLabelInvalid,
107+
"wontfix" -> issuesInfo.openCountWithLabelWontfix
108+
),
85109
),
86110
"pullRequests" -> Json.obj(
87111
"openCount" -> openPullRequests.size(),
@@ -129,22 +153,139 @@ class GithubAccess(val asOfYYYYMMDD: String, val asOfISO: String) {
129153
PRsInfo(closedPRs.size, avgPRs)
130154
}
131155

132-
def getIssuesStats(repo: GHRepository) : IssuesInfo = {
133-
val closedIssues = repo.getIssues(GHIssueState.CLOSED).filter(_.getPullRequest == null)
134-
val openIssues = repo.getIssues(GHIssueState.OPEN).filter(_.getPullRequest == null)
156+
def getIssuesStats(repo: GHRepository): IssuesInfo = {
157+
val closedIssues = repo.getIssues(GHIssueState.CLOSED).filter(_.getPullRequest == null).toArray
158+
val openIssues = repo.getIssues(GHIssueState.OPEN).filter(_.getPullRequest == null).toArray
159+
getIssuesStats(closedIssues, openIssues)
160+
}
161+
162+
def getIssuesStats(closedIssues: Array[GHIssue], openIssues: Array[GHIssue]): IssuesInfo = {
163+
val (openCountNoLabels, openCountWithLabelBug, openCountWithLabelDuplicate,
164+
openCountWithLabelEnhancement, openCountWithLabelHelpWanted,
165+
openCountWithLabelInvalid, openCountWithLabelQuestion, openCountWithLabelWontfix,
166+
openCountTrulyOpen) = getIssuesLabelStats(openIssues)
167+
168+
135169
val timeToCloseIssue = closedIssues.map(issue => {
136170
val opened = issue.getCreatedAt()
137171
val closed = issue.getClosedAt()
138172
val difference = daysBetween(opened, closed)
139173
difference
140174
})
141175
val sumIssues = timeToCloseIssue.sum
142-
val avgIssues = timeToCloseIssue.size match {
176+
val avgDaysToCloseIssues = timeToCloseIssue.size match {
143177
case 0 => 0
144178
case _ => sumIssues / timeToCloseIssue.size
145179
}
146-
logger.debug(s"avg days to close ${closedIssues.size()} issues = ${avgIssues} days")
147-
IssuesInfo(closedIssues.size, openIssues.size, avgIssues)
180+
logger.debug(s"avg days to close ${closedIssues.length} issues = ${avgDaysToCloseIssues} days")
181+
182+
IssuesInfo(closedIssues.size, openIssues.size, avgDaysToCloseIssues, openCountNoLabels, openCountWithLabelBug,
183+
openCountWithLabelDuplicate, openCountWithLabelEnhancement,
184+
openCountWithLabelHelpWanted, openCountWithLabelInvalid, openCountWithLabelQuestion, openCountWithLabelWontfix,
185+
openCountTrulyOpen)
186+
}
187+
188+
def getIssuesLabelStats(openIssues: Array[GHIssue]): (Int, Int, Int, Int, Int, Int, Int, Int, Int) = {
189+
val openCountNoLabels = openIssues.count(issue => issue.getLabels.size() == 0)
190+
// standard labels that count
191+
val openCountWithLabelBug = countLabelForIssues(openIssues, "bug")
192+
val openCountWithLabelHelpWanted = countLabelForIssues(openIssues, "help wanted")
193+
val openCountWithLabelQuestion = countLabelForIssues(openIssues, "question")
194+
// standard labels that dont' count
195+
val openCountWithLabelDuplicate = countLabelForIssues(openIssues, "duplicate")
196+
val openCountWithLabelEnhancement = countLabelForIssues(openIssues, "enhancement")
197+
val openCountWithLabelInvalid = countLabelForIssues(openIssues, "invalid")
198+
val openCountWithLabelWontfix = countLabelForIssues(openIssues, "wontfix")
199+
val openCountTrulyOpen = countLabelsForTrueIssues(openIssues)
200+
(
201+
openCountNoLabels, openCountWithLabelBug, openCountWithLabelDuplicate,
202+
openCountWithLabelEnhancement, openCountWithLabelHelpWanted,
203+
openCountWithLabelInvalid, openCountWithLabelQuestion, openCountWithLabelWontfix,
204+
openCountTrulyOpen)
205+
}
206+
207+
def countLabelsForTrueIssues(issues: Array[GHIssue]): Int = {
208+
// note that some issues will have bug and enhancement, we need to honor the worst case label (bug)
209+
// note that some issues will have bug and invalid, we don't want to double count
210+
// so, if no label, count it
211+
// for single labels
212+
// if (bug || help wanted || question) count it
213+
// if (duplicate || enhancement || invalid || wont fix) don't count it
214+
// for multiple labels
215+
// if (bug || help wanted || question) count it
216+
// if no standard github labels count it
217+
val count: Int = issues.count(issue => {
218+
val labels = issue.getLabels.toList
219+
220+
val shouldCount = if (labels.size == 0) {
221+
true // no labels so counts
222+
} else {
223+
if (hasBugOrQuestionLabel(labels)) {
224+
true // has bug or question, so counts
225+
}
226+
else if (hasInvalidOrWontFix(labels)) {
227+
false // has invalid or wontfix, so doesn't count
228+
}
229+
else {
230+
val duplicate = hasLabelOfName(labels, "duplicate")
231+
val enhancement = hasLabelOfName(labels, "enhancement")
232+
val helpwanted = hasLabelOfName(labels, "helpwanted")
233+
// by this point bug and question and invalid and wontfix = false
234+
val computed = (duplicate, enhancement, helpwanted) match {
235+
case (false, false, false) => true // no labels except custom labels
236+
case (false, false, true) => true // help wanted and [custom labels]
237+
case (false, true, false) => false // enhancement and [custom labels]
238+
case (false, true, true) => false // enhancement and helpwanted and [custom labels]
239+
case (true, false, false) => true // duplicate and [custom labels]
240+
case (true, false, true) => true // duplicate and helpwanted and [custom labels]
241+
case (true, true, false) => false // duplicate and enhancement and [custom labels]
242+
case (true, true, true) => false // duplicate, enhancement, help wanted and [custom labels]
243+
}
244+
computed
245+
}
246+
}
247+
248+
249+
// val shouldCount = if (labels.size == 0) true else {
250+
// // TODO: this doesn't work for enhancement&&help wanted (counts it, but shouldn't)
251+
// val standardCounts = hasLabelOfName(labels, "bug") || hasLabelOfName(labels, "help wanted") || hasLabelOfName(labels, "question")
252+
// val helpWantedAndEnhancement = hasLabelOfName(labels, "help wanted") && hasLabelOfName(labels, "enhancement")
253+
// val doesNotHaveSomeStandardLabels = !hasSomeStandardGithubLabels(labels)
254+
// standardCounts || doesNotHaveSomeStandardLabels
255+
// }
256+
logger.debug(s"issue ${issue.getNumber} counts = ${shouldCount}, labels = ${labels.map{_.getName}}")
257+
shouldCount
258+
})
259+
count
260+
}
261+
262+
// Issues with these labels ALWAYS count
263+
def hasBugOrQuestionLabel(labels: List[GHLabel]): Boolean = {
264+
// Future: Eventually we can let custom labels be configured per scraper or per project (OSSMETADATA)
265+
hasLabelOfName(labels, "bug") || hasLabelOfName(labels, "question")
266+
}
267+
268+
// Issues with these labels will never count as long as not Bug or Question
269+
def hasInvalidOrWontFix(labels: List[GHLabel]): Boolean = {
270+
// Future: Eventually we can let custom labels be configured per scraper or per project (OSSMETADATA)
271+
hasLabelOfName(labels, "invalid") || hasLabelOfName(labels, "wontfix")
272+
}
273+
274+
// def hasSomeStandardGithubLabels(labels: List[GHLabel]): Boolean = {
275+
// hasLabelOfName(labels, "bug") || hasLabelOfName(labels, "help wanted") || hasLabelOfName(labels, "question") ||
276+
// hasLabelOfName(labels, "duplicate") || hasLabelOfName(labels, "enhancement") || hasLabelOfName(labels, "invalid") || hasLabelOfName(labels, "wontfix")
277+
// }
278+
279+
def hasLabelOfName(labels: List[GHLabel], name: String): Boolean = {
280+
!labels.find(_.getName == name).isEmpty
281+
}
282+
283+
def countLabelForIssues(issues: Array[GHIssue], label: String): Int = {
284+
val openCountWithLabelBug: Int = issues.count(issue =>
285+
issue.getLabels.size() != 0 &&
286+
!issue.getLabels.find(_.getName == label).isEmpty
287+
)
288+
openCountWithLabelBug
148289
}
149290

150291
def daysBetween(smaller: Date, bigger: Date): Int = {
@@ -153,11 +294,11 @@ class GithubAccess(val asOfYYYYMMDD: String, val asOfISO: String) {
153294
}
154295

155296
def getRemainingHourlyRate(): Int = {
156-
github.getRateLimit.remaining
297+
github.get.getRateLimit.remaining
157298
}
158299

159300
def getAllRepositoriesForOrg(githubOrg: String): List[GHRepository] = {
160-
val org = github.getOrganization(githubOrg)
301+
val org = github.get.getOrganization(githubOrg)
161302
val githubRepos = org.listRepositories(100).asList().toList
162303
logger.info(s"Found ${githubRepos.size} total repos for ${githubOrg}")
163304
githubRepos

osstracker-scraper/src/main/scala/com/netflix/oss/tools/osstrackerscraper/GithubScraper.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class GithubScraper(githubOrg: String, cassHost: String, cassPort: Int, esHost:
3636
def updateElasticSearch(): Boolean = {
3737
val es = new ElasticSearchAccess(esHost, esPort)
3838
val cass = new CassandraAccesss(cassHost, cassPort)
39-
val github = new GithubAccess(asOfYYYYMMDD, asOfISO)
39+
val github = new GithubAccess(asOfYYYYMMDD, asOfISO, true)
4040

4141
try {
4242
println(Console.RED + s"remaining calls ${github.getRemainingHourlyRate()}" + Console.RESET)
@@ -154,7 +154,7 @@ class GithubScraper(githubOrg: String, cassHost: String, cassPort: Int, esHost:
154154

155155
def updateCassandra(): Boolean = {
156156
val cass = new CassandraAccesss(cassHost, cassPort)
157-
val github = new GithubAccess(asOfYYYYMMDD, asOfISO)
157+
val github = new GithubAccess(asOfYYYYMMDD, asOfISO, true)
158158
val report = StringBuilder.newBuilder
159159

160160
report.append(s"OSSTracker Report for ${asOfYYYYMMDD}\n\n")

0 commit comments

Comments
 (0)