gh-issues-stats 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Github
4
+ class Issues
5
+ ##
6
+ # Mixin for calculating issues statistics
7
+ module Statistics
8
+ private
9
+
10
+ ##
11
+ # Collects statistics about issues
12
+ #
13
+ # @param object [Hash] Hash containing :created, :closed, and :finished issue arrays
14
+ #
15
+ # @return [Hash] Hash containing calculated statistics
16
+ def stats_combined(object)
17
+ {
18
+ ratio: {
19
+ all: stats_closed_created_ratio(object[:closed], object[:created]),
20
+ finished: stats_closed_created_ratio(object[:finished], object[:created])
21
+ },
22
+ close_time: {
23
+ all: {
24
+ average: stats_average_closing_time(object[:closed]),
25
+ median: stats_median_closing_time(object[:closed])
26
+ },
27
+ finished: {
28
+ average: stats_average_closing_time(object[:finished]),
29
+ median: stats_median_closing_time(object[:finished])
30
+ }
31
+ }
32
+ }
33
+ end
34
+
35
+ ##
36
+ # Calculates the ratio of closed issues to created issues
37
+ #
38
+ # @param closed [Array<Hashie::Mash>] Array of closed issues
39
+ # @param created [Array<Hashie::Mash>] Array of created issues
40
+ #
41
+ # @return [Float] Ratio of closed to created issues
42
+ def stats_closed_created_ratio(closed, created)
43
+ return 0 if created.nil? || created.empty? || closed.nil? || closed.empty?
44
+
45
+ closed.size.to_f / created.size
46
+ end
47
+
48
+ ##
49
+ # Calculates the average closing time for a set of issues
50
+ #
51
+ # @param closed [Array<Hashie::Mash>] Array of closed issues
52
+ #
53
+ # @return [Float] Average closing time in seconds
54
+ def stats_average_closing_time(closed)
55
+ return 0 if closed.nil? || closed.empty?
56
+
57
+ total = closed.sum { |issue| issue.closed_at - issue.created_at }
58
+ total.to_f / closed.size
59
+ end
60
+
61
+ ##
62
+ # Calculates the median closing time for a set of issues
63
+ #
64
+ # @param closed [Array<Hashie::Mash>] Array of closed issues
65
+ #
66
+ # @return [Float] Median closing time in seconds
67
+ def stats_median_closing_time(closed)
68
+ return 0 if closed.nil? || closed.empty?
69
+
70
+ times = closed.map { |issue| issue.closed_at - issue.created_at }
71
+ times.sort!
72
+ size = times.size
73
+ (times[(size - 1) / 2] + times[size / 2]) / 2.0
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Github
4
+ class Issues
5
+ VERSION = '0.4.0'
6
+ end
7
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'hashie'
5
+
6
+ module Github
7
+ ##
8
+ # Main class for analyzing GitHub issues
9
+ class Issues
10
+ include Statistics
11
+ include Fetch
12
+ include Group
13
+
14
+ CACHE_DIR = File.join(Dir.home, '.cache', 'gh-issues-stats').freeze
15
+ DATABASE_NAME = 'issues.db'
16
+
17
+ ##
18
+ # Repository to analyze (e.g., 'owner/repo')
19
+ attr_reader :repository
20
+
21
+ ## Cache path for storing database file
22
+ attr_reader :cache
23
+
24
+ ##
25
+ # Credentials for GitHub API access
26
+ attr_reader :credentials
27
+
28
+ ##
29
+ # Database instance for storing and retrieving issues
30
+ attr_reader :database
31
+
32
+ ##
33
+ # Refresh interval in seconds for updating the stored issues
34
+ attr_reader :refresh
35
+
36
+ ##
37
+ # Initialize the Issues analyzer
38
+ #
39
+ # @param repository [String] GitHub repository (e.g., 'owner/repo')
40
+ # @param cache [String] Path to cache directory
41
+ # @param credentials [Hash] Optional GitHub API credentials
42
+ # @param refresh [Integer] Refresh interval in seconds (default: 86400)
43
+ #
44
+ # @return [Github::Issues] The initialized issues analyzer
45
+ def initialize(repository, cache: CACHE_DIR, credentials: {}, refresh: 86_400)
46
+ @repository = repository
47
+ @credentials = credentials
48
+ @refresh = refresh
49
+
50
+ dir = File.join(cache, repository)
51
+ FileUtils.mkdir_p(dir)
52
+ @cache = dir
53
+
54
+ database_path = File.join(dir, DATABASE_NAME)
55
+ github_repository_exist!(credentials, repository) unless File.exist?(database_path)
56
+ @database = Database.new(database_path)
57
+
58
+ at_exit do
59
+ database.close
60
+ end
61
+ end
62
+
63
+ ##
64
+ # Get all unique labels from the issues
65
+ #
66
+ # @return [Array<String>] List of unique labels
67
+ def labels
68
+ ensure_issues_loaded(database, refresh, credentials, repository)
69
+ database.all_labels.sort.reverse
70
+ end
71
+
72
+ ##
73
+ # List all issues
74
+ #
75
+ # @return [Array<Hashie::Mash>] List of issues
76
+ def all
77
+ ensure_issues_loaded(database, refresh, credentials, repository)
78
+ fetch_issues_from_database(database)
79
+ end
80
+
81
+ ##
82
+ # List issues filtered by labels
83
+ #
84
+ # @param labels [Array<String>] List of labels to filter issues
85
+ #
86
+ # @return [Array<Hashie::Mash>] List of filtered issues
87
+ def filtered_by_labels(labels)
88
+ ensure_issues_loaded(database, refresh, credentials, repository)
89
+ fetch_issues_from_database(database, labels:)
90
+ end
91
+
92
+ ##
93
+ # Total average closing time of all issues
94
+ #
95
+ # @return [Float] Average closing time in seconds
96
+ def all_average_closing_time
97
+ average_closing_time_filtered_by_labels([])
98
+ end
99
+
100
+ ##
101
+ # Total average closing time of all issues filtered by labels
102
+ #
103
+ # @param labels [Array<String>] List of labels to filter issues
104
+ #
105
+ # @return [Float] average closing time in seconds
106
+ def average_closing_time_filtered_by_labels(labels)
107
+ ensure_issues_loaded(database, refresh, credentials, repository)
108
+ list = fetch_issues_from_database(database, labels:)
109
+ return 0 if list.empty?
110
+
111
+ closed = list.select { |issue| issue.state == 'closed' }
112
+ stats_average_closing_time(closed)
113
+ end
114
+
115
+ ##
116
+ # Total median closing time of all issues
117
+ #
118
+ # @return [Float] Median closing time in seconds
119
+ def all_median_closing_time
120
+ median_closing_time_filtered_by_labels([])
121
+ end
122
+
123
+ ##
124
+ # Total median closing time of all issues filtered by labels
125
+ #
126
+ # @param labels [Array<String>] List of labels to filter issues
127
+ #
128
+ # @return [Float] Median closing time in seconds
129
+ def median_closing_time_filtered_by_labels(labels)
130
+ ensure_issues_loaded(database, refresh, credentials, repository)
131
+ list = fetch_issues_from_database(database, labels:)
132
+ return 0 if list.empty?
133
+
134
+ closed = list.select { |issue| issue.state == 'closed' }
135
+ stats_median_closing_time(closed)
136
+ end
137
+
138
+ ##
139
+ # Issues grouped per year with optional statistics
140
+ #
141
+ # @param stats [Boolean] Whether to include statistics for each year
142
+ #
143
+ # @return [Hashie::Mash] Issues grouped by year with optional statistics
144
+ def per_year(stats: true)
145
+ per_year_filtered_by_labels([], stats:)
146
+ end
147
+
148
+ ##
149
+ # Issues grouped per year filtered by labels with optional statistics
150
+ #
151
+ # @param labels [Array<String>] List of labels to filter issues
152
+ # @param stats [Boolean] Whether to include statistics for each year
153
+ #
154
+ # @return [Hashie::Mash] Issues grouped by year with optional statistics
155
+ def per_year_filtered_by_labels(labels, stats: true)
156
+ ensure_issues_loaded(database, refresh, credentials, repository)
157
+ list = fetch_issues_from_database(database, labels:)
158
+ return if list.empty?
159
+
160
+ hash = group_by_period(list)
161
+ hash.each_key { |year| hash[year][:stats] = stats_combined(hash[year]) } if stats
162
+
163
+ Hashie::Mash.new(hash)
164
+ end
165
+
166
+ ##
167
+ # Issues grouped per month for a specific year with optional statistics
168
+ #
169
+ # @param year [Integer] Year to filter issues
170
+ # @param stats [Boolean] Whether to include statistics for each month
171
+ #
172
+ # @return [Hashie::Mash] Issues grouped by month with optional statistics
173
+ def per_month(year, stats: true)
174
+ per_month_filtered_by_labels(year, [], stats:)
175
+ end
176
+
177
+ ##
178
+ # Issues grouped per month for a specific year filtered by labels with
179
+ # optional statistics
180
+ #
181
+ # @param year [Integer] Year to filter issues
182
+ # @param labels [Array<String>] List of labels to filter issues
183
+ # @param stats [Boolean] Whether to include statistics for each month
184
+ #
185
+ # @return [Hashie::Mash] Issues grouped by month with optional statistics
186
+ def per_month_filtered_by_labels(year, labels, stats: true)
187
+ ensure_issues_loaded(database, refresh, credentials, repository)
188
+ list = fetch_issues_from_database(database, labels:)
189
+ return if list.empty?
190
+
191
+ list = list.select { |issue| issue.created_at.year == year }
192
+ return if list.empty?
193
+
194
+ hash = group_by_period(list, period: :month)
195
+ hash.each_key { |month| hash[month][:stats] = stats_combined(hash[month]) } if stats
196
+
197
+ Hashie::Mash.new(hash)
198
+ end
199
+ end
200
+ end
metadata ADDED
@@ -0,0 +1,222 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gh-issues-stats
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Tobias Schäfer
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: clamp
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 1.3.2
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 1.3.2
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday-retry
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 2.4.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 2.4.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: hashie
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 5.1.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 5.1.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: octokit
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 10.0.0
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: 10.0.0
68
+ - !ruby/object:Gem::Dependency
69
+ name: pastel
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 0.8.0
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 0.8.0
82
+ - !ruby/object:Gem::Dependency
83
+ name: sqlite3
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 2.9.0
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 2.9.0
96
+ - !ruby/object:Gem::Dependency
97
+ name: tty-pager
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: 0.14.0
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 0.14.0
110
+ - !ruby/object:Gem::Dependency
111
+ name: tty-spinner
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 0.9.3
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: 0.9.3
124
+ - !ruby/object:Gem::Dependency
125
+ name: tty-table
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: 0.12.0
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: 0.12.0
138
+ - !ruby/object:Gem::Dependency
139
+ name: unicode_plot
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: 0.0.5
145
+ type: :runtime
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: 0.0.5
152
+ - !ruby/object:Gem::Dependency
153
+ name: zeitwerk
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: 2.7.1
159
+ type: :runtime
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: 2.7.1
166
+ description: 'Analyse Github repository issues lifecycle.
167
+
168
+ '
169
+ email:
170
+ - github@blackox.org
171
+ executables:
172
+ - gh-issues-stats
173
+ extensions: []
174
+ extra_rdoc_files: []
175
+ files:
176
+ - bin/gh-issues-stats
177
+ - lib/gh-issues-stats.rb
178
+ - lib/github/issues.rb
179
+ - lib/github/issues/app.rb
180
+ - lib/github/issues/app/base.rb
181
+ - lib/github/issues/app/labels.rb
182
+ - lib/github/issues/app/mixins/chart.rb
183
+ - lib/github/issues/app/mixins/exec.rb
184
+ - lib/github/issues/app/mixins/legend.rb
185
+ - lib/github/issues/app/mixins/options.rb
186
+ - lib/github/issues/app/mixins/statistics.rb
187
+ - lib/github/issues/app/mixins/table.rb
188
+ - lib/github/issues/app/month.rb
189
+ - lib/github/issues/app/year.rb
190
+ - lib/github/issues/database.rb
191
+ - lib/github/issues/mixins/database/marshal.rb
192
+ - lib/github/issues/mixins/database/schema.rb
193
+ - lib/github/issues/mixins/database/store.rb
194
+ - lib/github/issues/mixins/fetch.rb
195
+ - lib/github/issues/mixins/group.rb
196
+ - lib/github/issues/mixins/statistics.rb
197
+ - lib/github/issues/version.rb
198
+ homepage: https://github.com/tschaefer/gh-issues-stats
199
+ licenses:
200
+ - MIT
201
+ metadata:
202
+ rubygems_mfa_required: 'true'
203
+ source_code_uri: https://github.com/tschaefer/gh-issues-stats
204
+ bug_tracker_uri: https://github.com/tschaefer/gh-issues-stats/issues
205
+ rdoc_options: []
206
+ require_paths:
207
+ - lib
208
+ required_ruby_version: !ruby/object:Gem::Requirement
209
+ requirements:
210
+ - - ">="
211
+ - !ruby/object:Gem::Version
212
+ version: 3.3.0
213
+ required_rubygems_version: !ruby/object:Gem::Requirement
214
+ requirements:
215
+ - - ">="
216
+ - !ruby/object:Gem::Version
217
+ version: '0'
218
+ requirements: []
219
+ rubygems_version: 4.0.4
220
+ specification_version: 4
221
+ summary: Analyse Github repository issues lifecycle.
222
+ test_files: []