github-stats 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7b06fb068ddd3c1e7e0e13ac784674cb3b74c33e
4
- data.tar.gz: 825ae518a430007fbdbd65b492556db6c365d917
3
+ metadata.gz: ab422a50b60641cae6051a695a86876b5e5ad707
4
+ data.tar.gz: 43dfaa3d02fe256f39641d7f132afd9879b724c8
5
5
  SHA512:
6
- metadata.gz: 8d5a88b2a8408bd4d259e0ea1eb5f9f423e1131540c165c1e3f1cefe4b4181e74915065b6179ba4a2888c6fc7d41522f78e1f32950d5479e01467c70a0bbfc31
7
- data.tar.gz: 0127f29ca42621cce9d1779331c0da9013b5ce6a05a42e31b83bf271d25df2e22f7f1d0d0ef607afe662f7a1e5199ea17d5b0f33d3d5c3308718faf6951ddb15
6
+ metadata.gz: c43a0a82b19ca8e1db9301965e89c0577da86e1253314da443873e0ab95fc689cb289833ba1cb99ad1052ac532b2bce397fbb04f908f8c051c7c1a2fe1dd54a9
7
+ data.tar.gz: 83a13c2de52cfa8b2a56af2f0db89057a39047d25ef2d1ee9a7ebe6f034403acac89bb158df4c3c261d958c222815cce760185571f56b48f35c4f7121b7a3749
data/README.md CHANGED
@@ -1,22 +1,23 @@
1
- # Github Issue Stats
1
+ # Github Issue Stats [![Gem Version](https://badge.fury.io/rb/github-stats.svg)](https://badge.fury.io/rb/github-stats)
2
+
2
3
 
3
4
  Github issues are a decent way to track work on a small project; and with the rise of tools such as Waffle.io and ZenHub, it appears that they will slowly but surely become better and better for managing long term projects.
4
5
 
5
- However statistical analysis of github issues is still very much lacking. `github-issue-stats` is a command line tool / gem that takes github searches and converts them into useful project reports.
6
+ However statistical analysis of github issues is still very much lacking. `github-stats` is a command line tool / gem that takes github searches and converts them into useful project reports.
6
7
 
7
8
  ## Installation & Usage
8
- Assuming you are using Ruby 1.9.3 or above: `gem install github-issue-stats` will install the tool. Running `github-issue-stats "whatever github search string you want"` will run the search, outputting a closed by week report.
9
+ Assuming you are using Ruby 1.9.3 or above: `gem install github-stats` will install the tool. Running `github-stats "whatever github search string you want"` will run the search, outputting a closed by week report.
9
10
 
10
- For a detailed list of options and command line flags, please refer to `github-issue-stats --help`.
11
+ For a detailed list of options and command line flags, please refer to `github-stats --help`.
11
12
 
12
13
  **Please Note!** Github's Search API restricts searches to [the first 1,000 results](https://developer.github.com/v3/search/#about-the-search-api) and limits [unauthenticated requests to 10 per minute](https://developer.github.com/v3/search/#rate-limit). This means you can run 1 report per-minute that would return a full 1,000 issues.
13
14
 
14
- I recommend using filters such as [`state:closed`](https://help.github.com/articles/searching-issues/#search-based-on-whether-an-issue-or-pull-request-is-open) and/or [`updated:>=2016-01-01`](https://help.github.com/articles/searching-issues/#search-based-on-when-an-issue-or-pull-request-was-created-or-last-updated) to scope your requests down, based upon the report type.
15
+ I recommend using filters such as [`state:closed`](https://help.github.com/articles/searching-issues/#search-based-on-whether-anor-pull-request-is-open) and/or [`updated:>=2016-01-01`](https://help.github.com/articles/searching-issues/#search-based-on-when-anor-pull-request-was-created-or-last-updated) to scope your requests down, based upon the report type.
15
16
 
16
17
  ### Example
17
18
  The following example shows how to get a closed by week report for Rails for the first 4 weeks of May 2016
18
19
  ```
19
- $ github-issue-stats "repo:rails/rails type:issue is:closed closed:2016-05-01..2016-05-28"
20
+ $ github-stats "repo:rails/rails type:issue is:closed closed:2016-05-01..2016-05-28"
20
21
  2016-17 3 1
21
22
  2016-18 27 10
22
23
  2016-19 30 20
@@ -28,7 +28,7 @@ OptionParser.new do |opts|
28
28
  options[:ingest] = ingest
29
29
  end
30
30
 
31
- opts.on('-r REPORT_TYPE', '--report REPORT_TYPE', 'Defaults to closed_by_week') do |report_type|
31
+ opts.on('-r REPORT_TYPE', '--report REPORT_TYPE', 'Default is closed_by_week; may be cycle_time, closed_by_week, or created_by_week') do |report_type|
32
32
  options[:report_type] = report_type
33
33
  end
34
34
  end.parse!
@@ -3,6 +3,8 @@ require 'sequel'
3
3
 
4
4
  require_relative 'github_stats/cli'
5
5
  require_relative 'github_stats/database'
6
+ require_relative 'github_stats/reports'
7
+ require_relative 'github_stats/created_by_week_report'
6
8
  require_relative 'github_stats/closed_by_week_report'
7
9
  require_relative 'github_stats/issue_ingester'
8
10
 
@@ -19,7 +19,7 @@ module GithubStats
19
19
  def run
20
20
  setup_db
21
21
  ingest
22
- report
22
+ results
23
23
  end
24
24
 
25
25
  private def setup_db
@@ -38,13 +38,16 @@ module GithubStats
38
38
  end
39
39
 
40
40
  private def report
41
- results = ClosedByWeekReport.new(search_string, options).results
42
- SpaceSeperatedLinePerResultResultsView.new(results)
41
+ Reports.for(options[:report_type]).new(search_string, options)
42
+ end
43
+
44
+ private def results
45
+ CommaSeperatedLinePerResultResultsView.new(report.results)
43
46
  end
44
47
 
45
48
  # Transforms a result set into a space-seperated table the results hash
46
49
  # keys becoming the table headers and line breaks between rows.
47
- class SpaceSeperatedLinePerResultResultsView
50
+ class CommaSeperatedLinePerResultResultsView
48
51
  attr_accessor :results
49
52
  def initialize(results)
50
53
  self.results = results
@@ -55,8 +58,8 @@ module GithubStats
55
58
  end
56
59
 
57
60
  def to_s
58
- fields.join(' ') + "\n" + results.map do |result|
59
- fields.map(&result.method(:fetch)).join(' ')
61
+ fields.join(',') + "\n" + results.map do |result|
62
+ fields.map(&result.method(:fetch)).join(',')
60
63
  end.join("\n")
61
64
  end
62
65
  end
@@ -1,65 +1,26 @@
1
1
  require_relative 'database'
2
+ require_relative 'report'
2
3
  module GithubStats
3
- # Provides week-by-week breakdown of issues closed, grouped by week
4
- # with a 3 week moving average.
5
- class ClosedByWeekReport
6
- attr_accessor :search_string, :options
7
-
8
- def initialize(search_string, options)
9
- self.search_string = search_string
10
- self.options = options
11
- end
12
-
13
- def results
14
- results = with_velocity(with_week_closed(with_qty(issues)))
15
- Results.new(results)
16
- end
17
-
18
- private def with_velocity(issues)
19
- closed_two_weeks_ago = 0
20
- closed_last_week = 0
21
- issues.map do |issue|
22
- issue[:velocity] = average(closed_two_weeks_ago, closed_last_week,
23
- issue[:qty])
24
- closed_two_weeks_ago = closed_last_week
25
- closed_last_week = issue[:qty]
26
- issue
4
+ module Reports
5
+ # Provides week-by-week breakdown of issues closed, grouped by week
6
+ # with a 3 week moving average.
7
+ class ClosedByWeekReport
8
+ attr_accessor :search_string, :options
9
+ include Report
10
+
11
+ def results
12
+ results = with_moving_average(:velocity,
13
+ by_week_closed(with_qty(issues)))
14
+ Reports::Results.new(results, keys: [:week_closed, :qty, :velocity])
27
15
  end
28
- end
29
-
30
- private def average(*numbers)
31
- numbers.reduce(:+) / numbers.length
32
- end
33
-
34
- private def with_qty(dataset)
35
- dataset.select { count(:id).as qty }
36
- end
37
-
38
- private def with_week_closed(dataset)
39
- dataset.select_append { strftime('%Y-%W', closed_at).as(week_closed) }
40
- .group_by(:week_closed)
41
- end
42
-
43
- private def issues
44
- db.issues.where(search_string: search_string).where { closed_at !~ nil }
45
- end
46
-
47
- private def db
48
- @db ||= Database.new(options)
49
- end
50
-
51
- # Provides enumerable access to the results
52
- class Results
53
- attr_accessor :data
54
- extend Forwardable
55
- def_delegators :data, :each, :map
56
16
 
57
- def initialize(data)
58
- self.data = data
17
+ private def by_week_closed(dataset)
18
+ dataset.select_append { strftime('%Y-%W', closed_at).as(week_closed) }
19
+ .group_by(:week_closed)
59
20
  end
60
21
 
61
- def keys
62
- [:week_closed, :qty, :velocity]
22
+ private def issues
23
+ db.issues.where(search_string: search_string).where { closed_at !~ nil }
63
24
  end
64
25
  end
65
26
  end
@@ -0,0 +1,29 @@
1
+ require_relative 'database'
2
+ require_relative 'report'
3
+ module GithubStats
4
+ module Reports
5
+ # Provides week-by-week breakdown of issues created, grouped by week
6
+ # with a 3 week moving average.
7
+ class CreatedByWeekReport
8
+ attr_accessor :search_string, :options
9
+
10
+ include Report
11
+
12
+ def results
13
+ results = with_moving_average(:add_rate,
14
+ by_week_created(with_qty(issues)))
15
+ Reports::Results.new(results, keys: [:week_created, :qty, :add_rate])
16
+ end
17
+
18
+ private def by_week_created(dataset)
19
+ dataset.select_append { strftime('%Y-%W', created_at).as(week_created) }
20
+ .group_by(:week_created)
21
+ end
22
+
23
+ private def issues
24
+ db.issues.where(search_string: search_string)
25
+ .where { created_at !~ nil }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ require_relative 'database'
2
+ require_relative 'report'
3
+ module GithubStats
4
+ module Reports
5
+ # Provides issue cycle time from started at to done
6
+ class CycleTimeReport
7
+ attr_accessor :search_string, :options
8
+ include Report
9
+
10
+ def results
11
+ Results.new(with_cycle_time(issues), keys: [:week_closed, :created_at, :started_at, :closed_at, :cycle_time, :url])
12
+ end
13
+
14
+ private def with_cycle_time(dataset)
15
+ dataset.all.map do |item|
16
+ item[:week_closed] = (item[:closed_at].strftime("%Y-%W"))
17
+ item[:cycle_time] = work_days_between(item[:closed_at], item[:started_at])
18
+ item
19
+ end
20
+ end
21
+
22
+ private def work_days_between(end_time, start_time)
23
+ hours_between = (end_time - start_time) / 60 / 60
24
+ return hours_between.to_f / 24.to_f if hours_between < 12
25
+ days_between = (end_time.strftime('%j').to_i - start_time.strftime('%j').to_i)
26
+ weeks_between = (end_time.strftime('%W').to_i - start_time.strftime('%W').to_i)
27
+ return (hours_between - (days_between * 10 + weeks_between * 28)).to_f / 24.to_f
28
+ end
29
+
30
+ private def issues
31
+ db.issues.where(search_string: search_string).where { closed_at !~ nil }
32
+ .where { started_at !~ nil }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -6,33 +6,12 @@ module GithubStats
6
6
 
7
7
  def initialize(options)
8
8
  self.octokit = Octokit::Client.new(access_token:
9
- options[:github_access_token])
9
+ options[:github_access_token],
10
+ auto_paginate: true)
10
11
  end
11
12
 
12
13
  def search_issues(query, options = { per_page: 100 })
13
14
  octokit.search_issues(query, options)
14
- Response.new(self)
15
- end
16
-
17
- def last_response
18
- octokit.last_response
19
- end
20
-
21
- # Auto-paginate!
22
- class Response
23
- attr_reader :client
24
- def initialize(client)
25
- @client = client
26
- end
27
-
28
- def each(&block)
29
- last_response = client.last_response
30
- loop do
31
- last_response.data.items.each(&block)
32
- last_response = last_response.rels[:next].get
33
- break if last_response.rels[:next].nil?
34
- end
35
- end
36
15
  end
37
16
  end
38
17
  end
@@ -11,25 +11,39 @@ module GithubStats
11
11
 
12
12
  def ingest
13
13
  return unless options[:ingest]
14
- github.search_issues(search_string).each(&method(:insert_or_update))
14
+ github.search_issues(search_string).items.each(&method(:insert_or_update))
15
15
  end
16
16
 
17
17
  private def insert_or_update(result)
18
18
  issue = issues.first(github_id: result[:id])
19
- return insert(result) unless issue
20
- return update(issue, result) if issue[:closed_at] != result[:closed_at] ||
21
- issue[:created_at] != result[:created_at]
19
+ started_at = started_at(result)
20
+ return insert(result, started_at: started_at) unless issue
21
+ return update(issue, result, started_at: started_at) if issue[:closed_at] != result[:closed_at] ||
22
+ issue[:created_at] != result[:created_at] ||
23
+ issue[:started_at] != started_at
22
24
  end
23
25
 
24
- private def update(issue, result)
26
+ private def started_at(result)
27
+ starting_events = result.rels[:events].get.data.select do |event|
28
+ event[:event] == 'labeled' && (event[:label] || {})[:name] == 'in-progress'
29
+ end.sort_by do |event|
30
+ event[:created_at]
31
+ end
32
+ starting_event = starting_events.last
33
+ starting_event.nil? ? nil : starting_event[:created_at]
34
+ end
35
+
36
+ private def update(issue, result, started_at:)
25
37
  issue.update(closed_at: result[:closed_at],
26
- created_at: result[:created_at])
38
+ created_at: result[:created_at],
39
+ started_at: started_at)
27
40
  end
28
41
 
29
- private def insert(result)
42
+ private def insert(result, started_at:)
30
43
  issues.insert(search_string: search_string, github_id: result[:id],
31
44
  closed_at: result[:closed_at],
32
45
  created_at: result[:created_at],
46
+ started_at: started_at,
33
47
  url: result[:url])
34
48
  end
35
49
 
@@ -0,0 +1,34 @@
1
+ module GithubStats
2
+ module Reports
3
+ module Report
4
+ def initialize(search_string, options)
5
+ self.search_string = search_string
6
+ self.options = options
7
+ end
8
+
9
+ private def with_moving_average(label, weeks)
10
+ closed_two_weeks_ago = 0
11
+ closed_last_week = 0
12
+ weeks.map do |week|
13
+ week[label] = average(closed_two_weeks_ago, closed_last_week,
14
+ week[:qty])
15
+ closed_two_weeks_ago = closed_last_week
16
+ closed_last_week = week[:qty]
17
+ week
18
+ end
19
+ end
20
+
21
+ private def average(*numbers)
22
+ numbers.reduce(:+) / numbers.length
23
+ end
24
+
25
+ private def with_qty(dataset)
26
+ dataset.select { count(:id).as qty }
27
+ end
28
+
29
+ private def db
30
+ @db ||= Database.new(options)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'closed_by_week_report'
2
+ require_relative 'cycle_time_report'
3
+ require_relative 'created_by_week_report'
4
+
5
+ module GithubStats
6
+ module Reports
7
+ def self.for(report_type)
8
+ { 'closed_by_week' => ClosedByWeekReport,
9
+ 'created_by_week' => CreatedByWeekReport,
10
+ 'cycle_time' => CycleTimeReport }.fetch(report_type)
11
+ end
12
+
13
+ # Provides enumerable access to the results
14
+ class Results
15
+ attr_accessor :data, :keys
16
+ extend Forwardable
17
+ def_delegators :data, :each, :map
18
+
19
+ def initialize(data, keys: [])
20
+ self.data = data
21
+ self.keys = keys
22
+ end
23
+ end
24
+ end
25
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: github-stats
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zee@Zinc
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-29 00:00:00.000000000 Z
11
+ date: 2016-11-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: octokit
@@ -69,9 +69,13 @@ files:
69
69
  - lib/github_stats.rb
70
70
  - lib/github_stats/cli.rb
71
71
  - lib/github_stats/closed_by_week_report.rb
72
+ - lib/github_stats/created_by_week_report.rb
73
+ - lib/github_stats/cycle_time_report.rb
72
74
  - lib/github_stats/database.rb
73
75
  - lib/github_stats/github_client.rb
74
76
  - lib/github_stats/issue_ingester.rb
77
+ - lib/github_stats/report.rb
78
+ - lib/github_stats/reports.rb
75
79
  homepage: https://github.com/zincmade/github-stats
76
80
  licenses:
77
81
  - MIT