@@ -20,17 +20,31 @@ import java.util.{Date, Properties}
2020
2121import com .netflix .oss .tools .osstrackerscraper .OssLifecycle .OssLifecycle
2222import 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+
2526import scala .collection .JavaConversions ._
2627
2728case 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+ ) {}
2943case 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
0 commit comments