nya_pr 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 74fef078ebf20c6a46a8596b5001a66b744436a33ee407d14168699e577fa7e1
4
+ data.tar.gz: 3e686721cb075eead9c7cd7070cf49ee6d0b9eca7ce879b8555380c6d1a52626
5
+ SHA512:
6
+ metadata.gz: 40aa1079ac60ac7d0de2d79135eeb75f7b1854e4299277a94ec424268d0fb59719be1f8832546fb6fd28abeac63786b4043a2128ea60749daa183839850658f3
7
+ data.tar.gz: 0377b72b765df89b9da1ff33d92224431a1ebe16c10c70e457f4ebdb93381479d9c1a6e0122366075363f1d550ca1491647d9f0cd9b44347d9cca6304249806e
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Elijah S. Krukowski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,314 @@
1
+ # NyaPr ฅ^•ﻌ•^ฅ
2
+
3
+ **NyaPr** is a tiny Ruby tool for people who have too many GitHub repositories, too many organizations, too many pull requests, and absolutely no desire to click through all that manually.
4
+
5
+ It collects repositories you own or contribute to, finds their open pull requests, and puts everything into one place.
6
+
7
+ For maximum kawaii.
8
+
9
+ ## Why NyaPr?
10
+
11
+ GitHub is perfectly happy to show you:
12
+
13
+ * all repositories in a giant organization;
14
+ * pull requests you authored;
15
+ * pull requests where somebody mentioned you;
16
+ * approximately seventeen billion other things.
17
+
18
+ What it does not make particularly convenient is:
19
+
20
+ > Show me open pull requests from my own repositories **plus** repositories in these organizations where I actually contribute.
21
+
22
+ NyaPr does exactly that.
23
+
24
+ The basic flow is:
25
+
26
+ ```text
27
+ GitHub users / organizations
28
+
29
+ collect repositories
30
+
31
+ keep repositories you actually contribute to
32
+
33
+ optionally skip archived repositories
34
+
35
+ cache the repository list
36
+
37
+ find open pull requests
38
+
39
+ terminal + timestamped CSV
40
+
41
+ nya ฅ^•ﻌ•^ฅ
42
+ ```
43
+
44
+ ## Requirements
45
+
46
+ * Ruby 4+
47
+ * A GitHub personal access token
48
+ * A reasonable appreciation of cats
49
+
50
+ ## Setup
51
+
52
+ Install the dependencies:
53
+
54
+ ```bash
55
+ bundle install
56
+ ```
57
+
58
+ Create a `.env` file:
59
+
60
+ ```text
61
+ GITHUB_TOKEN=your_github_token
62
+ ```
63
+
64
+ For private repositories, make sure the token has access to them. If an organization uses SSO, the token may also need to be authorized for that organization.
65
+
66
+ ## Usage
67
+
68
+ Run NyaPr with your GitHub username:
69
+
70
+ ```bash
71
+ ruby run.rb --user bodrovis
72
+ ```
73
+
74
+ You can also provide additional users or organizations whose repositories should be inspected:
75
+
76
+ ```bash
77
+ ruby run.rb \
78
+ --user bodrovis \
79
+ --owners org1,org2
80
+ ```
81
+
82
+ Your own repositories are always included automatically.
83
+
84
+ For other users and organizations, NyaPr checks which repositories contain commits authored by the specified GitHub user.
85
+
86
+ ## Data directory
87
+
88
+ By default, NyaPr stores generated data inside the `data` directory.
89
+
90
+ The repository cache is stored as:
91
+
92
+ ```text
93
+ data/repositories.csv
94
+ ```
95
+
96
+ Each pull request scan gets its own timestamped directory:
97
+
98
+ ```text
99
+ data/
100
+ ├── repositories.csv
101
+ ├── pull_requests_20260912_201845/
102
+ │ └── pull_requests.csv
103
+ └── pull_requests_20260912_204112/
104
+ └── pull_requests.csv
105
+ ```
106
+
107
+ This keeps repository discovery cached while preserving the results of individual pull request scans.
108
+
109
+ Missing directories are created automatically.
110
+
111
+ ## Repository cache
112
+
113
+ Repository discovery can involve a lot of GitHub API requests, especially for large organizations.
114
+
115
+ NyaPr therefore stores discovered repositories in:
116
+
117
+ ```text
118
+ data/repositories.csv
119
+ ```
120
+
121
+ On subsequent runs, this file is used as a cache instead of scanning GitHub again.
122
+
123
+ To force a fresh repository scan:
124
+
125
+ ```bash
126
+ ruby run.rb \
127
+ --user bodrovis \
128
+ --owners org1,org2 \
129
+ --refresh
130
+ ```
131
+
132
+ You can use another repository cache file:
133
+
134
+ ```bash
135
+ ruby run.rb \
136
+ --user bodrovis \
137
+ --repositories-csv "C:\Users\me\repositories.csv"
138
+ ```
139
+
140
+ Both relative and absolute paths are supported.
141
+
142
+ Repository contribution checks are performed concurrently using a small thread pool, because waiting for hundreds of HTTP requests one by one is not particularly kawaii.
143
+
144
+ ## Skipping archived repositories
145
+
146
+ Archived repositories are usually ancient tombs where pull requests go to sleep forever.
147
+
148
+ You can ignore them completely:
149
+
150
+ ```bash
151
+ ruby run.rb \
152
+ --user bodrovis \
153
+ --owners org1,org2 \
154
+ --skip-archived
155
+ ```
156
+
157
+ When this option is enabled, archived repositories are removed before expensive contribution checks are performed.
158
+
159
+ Maximum efficiency. Minimum archaeology.
160
+
161
+ ## Pull requests
162
+
163
+ After preparing the repository list, NyaPr searches each repository for open pull requests.
164
+
165
+ Results are grouped by repository and printed to the terminal:
166
+
167
+ ```text
168
+ bodrovis/example
169
+ ----------------
170
+
171
+ #42 Fix pagination
172
+ Author: @someone
173
+ https://github.com/bodrovis/example/pull/42
174
+
175
+ #43 [DRAFT] Add nya support
176
+ Author: @someone_else
177
+ https://github.com/bodrovis/example/pull/43
178
+ ```
179
+
180
+ They are also written to a timestamped CSV file:
181
+
182
+ ```text
183
+ data/pull_requests_20260912_201845/pull_requests.csv
184
+ ```
185
+
186
+ The CSV contains:
187
+
188
+ ```text
189
+ repository
190
+ number
191
+ title
192
+ author
193
+ draft
194
+ created_at
195
+ updated_at
196
+ html_url
197
+ ```
198
+
199
+ By default, NyaPr stops after collecting **100 open pull requests**.
200
+
201
+ You can change the limit with `--limit`:
202
+
203
+ ```bash
204
+ ruby run.rb \
205
+ --user bodrovis \
206
+ --limit 250
207
+ ```
208
+
209
+ You can also change the base directory where pull request runs are stored:
210
+
211
+ ```bash
212
+ ruby run.rb \
213
+ --user bodrovis \
214
+ --pull-requests-dir "C:\Users\me\nya-results"
215
+ ```
216
+
217
+ This produces something like:
218
+
219
+ ```text
220
+ C:\Users\me\nya-results\pull_requests_20260912_201845\pull_requests.csv
221
+ ```
222
+
223
+ Each run still gets its own timestamped directory.
224
+
225
+ ## Logging
226
+
227
+ NyaPr uses normal log levels instead of spraying random `puts` statements everywhere like an animal.
228
+
229
+ The default level is `info`.
230
+
231
+ Example:
232
+
233
+ ```bash
234
+ ruby run.rb \
235
+ --user bodrovis \
236
+ --owners org1,org2 \
237
+ --log-level info
238
+ ```
239
+
240
+ At the `info` level, NyaPr reports the major stages of the run: loading or discovering repositories, filtering contributions, searching for pull requests, and saving results.
241
+
242
+ For HTTP-level details:
243
+
244
+ ```bash
245
+ ruby run.rb ... --log-level debug
246
+ ```
247
+
248
+ This also shows requests, responses, skipped empty repositories, detected contributions, and other useful diagnostic information.
249
+
250
+ Available levels:
251
+
252
+ ```text
253
+ debug
254
+ info
255
+ warn
256
+ error
257
+ fatal
258
+ ```
259
+
260
+ Logs are written to `stderr`, while actual pull request output goes to `stdout`.
261
+
262
+ ## CLI options
263
+
264
+ ```text
265
+ -u, --user USERNAME GitHub username
266
+ -o, --owners LIST Additional users or organizations, comma-separated
267
+ --token TOKEN GitHub token (GITHUB_TOKEN env variable is recommended)
268
+ --repositories-csv PATH Repository cache CSV path
269
+ --pull-requests-dir PATH Directory for pull request runs
270
+ -l, --limit N Maximum number of pull requests to collect
271
+ -r, --refresh Refresh repositories from GitHub
272
+ --skip-archived Ignore archived repositories
273
+ --[no-]progress Show or hide progress bars
274
+ --log-level LEVEL Logging level
275
+ -v, --version Show NyaPr version
276
+ ```
277
+
278
+ Example with most options enabled:
279
+
280
+ ```bash
281
+ ruby run.rb \
282
+ --user bodrovis \
283
+ --owners org1,org2 \
284
+ --repositories-csv "data/repositories.csv" \
285
+ --pull-requests-dir "data" \
286
+ --limit 250 \
287
+ --skip-archived \
288
+ --refresh \
289
+ --log-level debug
290
+ ```
291
+
292
+ ## How repository discovery works
293
+
294
+ For your own GitHub username, NyaPr collects repositories owned by your authenticated account.
295
+
296
+ For additional owners:
297
+
298
+ 1. NyaPr determines whether the owner is a GitHub user or organization.
299
+ 2. It collects their repositories.
300
+ 3. Archived repositories can optionally be removed.
301
+ 4. NyaPr checks whether your GitHub user has commits in each repository.
302
+ 5. Only repositories where you contributed are retained.
303
+
304
+ Contribution checks run concurrently using a limited number of worker threads.
305
+
306
+ Empty Git repositories are simply skipped.
307
+
308
+ Repositories with pull requests disabled are also skipped when searching for PRs.
309
+
310
+ The prepared repository list is cached, but pull requests are fetched fresh on every run.
311
+
312
+ ## License
313
+
314
+ (c) [Elijah S. Krukowski](https://bodrovis.tech), MIT license
data/exe/nya-pr ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'nya_pr'
5
+
6
+ begin
7
+ NyaPr.run
8
+ rescue OptionParser::ParseError, NyaPr::Error => e
9
+ warn "nya-pr: #{e.message}"
10
+ exit 1
11
+ rescue Interrupt
12
+ warn "\nnya-pr: interrupted"
13
+ exit 130
14
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NyaPr
4
+ module Cli
5
+ class Command < Dry::CLI::Command
6
+ LOG_LEVELS = {
7
+ 'debug' => Logger::DEBUG,
8
+ 'info' => Logger::INFO,
9
+ 'warn' => Logger::WARN,
10
+ 'error' => Logger::ERROR,
11
+ 'fatal' => Logger::FATAL
12
+ }.freeze
13
+
14
+ desc 'Collect open GitHub pull requests'
15
+
16
+ option :username,
17
+ aliases: ['-u', '--user'],
18
+ desc: 'GitHub username'
19
+
20
+ option :owners,
21
+ aliases: ['-o'],
22
+ type: :array,
23
+ default: [],
24
+ desc: 'Repository owners'
25
+
26
+ option :token,
27
+ desc: 'GitHub token'
28
+
29
+ option :repositories_csv,
30
+ desc: 'Repository cache CSV path'
31
+
32
+ option :pull_requests_dir,
33
+ desc: 'Directory for pull request runs'
34
+
35
+ option :limit,
36
+ aliases: ['-l'],
37
+ default: PullRequest::Finder::MAX_PULL_REQUESTS,
38
+ desc: 'Maximum number of pull requests'
39
+
40
+ option :refresh,
41
+ aliases: ['-r'],
42
+ type: :boolean,
43
+ default: false,
44
+ desc: 'Refresh repositories from GitHub'
45
+
46
+ option :skip_archived,
47
+ type: :boolean,
48
+ default: false,
49
+ desc: 'Skip archived repositories'
50
+
51
+ option :progress,
52
+ type: :boolean,
53
+ default: true,
54
+ desc: 'Show progress bars'
55
+
56
+ option :log_level,
57
+ values: LOG_LEVELS.keys,
58
+ default: 'info',
59
+ desc: 'Log level'
60
+
61
+ option :version,
62
+ aliases: ['-v'],
63
+ type: :flag,
64
+ desc: 'Show version'
65
+
66
+ def call(version: false, **options)
67
+ if version
68
+ puts NyaPr::VERSION
69
+ return
70
+ end
71
+
72
+ normalize_options!(options)
73
+ validate_username!(options)
74
+
75
+ Runner.new(options).run
76
+ end
77
+
78
+ private
79
+
80
+ def normalize_options!(options)
81
+ options[:owners] = normalize_owners(options[:owners])
82
+ options[:limit] = normalize_limit(options[:limit])
83
+ options[:log_level] = LOG_LEVELS.fetch(options[:log_level])
84
+ end
85
+
86
+ def normalize_owners(owners)
87
+ Array(owners).
88
+ flat_map { |owner| owner.split(',') }.
89
+ map(&:strip).
90
+ reject(&:empty?)
91
+ end
92
+
93
+ def normalize_limit(value)
94
+ limit = Integer(value)
95
+
96
+ return limit if limit.positive?
97
+
98
+ raise NyaPr::Error, 'limit must be greater than 0'
99
+ rescue ArgumentError, TypeError
100
+ raise NyaPr::Error, 'limit must be a positive integer'
101
+ end
102
+
103
+ def validate_username!(options)
104
+ return unless options[:username].to_s.empty?
105
+
106
+ raise NyaPr::Error, 'GitHub username is required'
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NyaPr
4
+ class Client
5
+ attr_reader :token, :options
6
+
7
+ def initialize(token, **options)
8
+ @token = token
9
+ @options = options
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ module NyaPr
6
+ module Connection
7
+ API_URL = 'https://api.github.com'
8
+ API_VERSION = '2026-03-10'
9
+
10
+ def connection(client)
11
+ Faraday.new(connection_options(client)) do |faraday|
12
+ faraday.adapter Faraday.default_adapter
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def connection_options(client)
19
+ {
20
+ url: API_URL,
21
+ headers: {
22
+ accept: 'application/vnd.github+json',
23
+ user_agent: "nya_pr gem/#{NyaPr::VERSION}",
24
+ authorization: "Bearer #{client.token}",
25
+ 'X-GitHub-Api-Version': client.options.fetch(:api_version, API_VERSION)
26
+ },
27
+ request: {
28
+ timeout: client.options.fetch(:timeout, 60),
29
+ open_timeout: client.options.fetch(:open_timeout, 10)
30
+ }
31
+ }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NyaPr
4
+ class Error < StandardError
5
+ attr_reader :status
6
+
7
+ def initialize(message, status: nil)
8
+ @status = status
9
+
10
+ super(message)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module NyaPr
6
+ class Paths
7
+ DEFAULT_DATA_DIR = 'data'
8
+ REPOSITORIES_FILENAME = 'repositories.csv'
9
+ PULL_REQUESTS_FILENAME = 'pull_requests.csv'
10
+
11
+ attr_reader :repositories_path, :pull_requests_dir
12
+
13
+ def initialize(
14
+ repositories_path: nil,
15
+ pull_requests_dir: nil,
16
+ timestamp: Time.now
17
+ )
18
+ @repositories_path = repositories_path
19
+ @pull_requests_dir = pull_requests_dir
20
+ @timestamp = timestamp
21
+ end
22
+
23
+ def repositories_csv
24
+ path = Pathname.new(
25
+ repositories_path || File.join(DEFAULT_DATA_DIR, REPOSITORIES_FILENAME)
26
+ )
27
+
28
+ prepare_parent(path)
29
+ path.to_s
30
+ end
31
+
32
+ def pull_requests_csv
33
+ path = pull_requests_run_dir.join(PULL_REQUESTS_FILENAME)
34
+
35
+ FileUtils.mkdir_p(path.dirname)
36
+
37
+ path.to_s
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :timestamp
43
+
44
+ def pull_requests_run_dir
45
+ @pull_requests_run_dir ||= begin
46
+ base = Pathname.new(pull_requests_dir || DEFAULT_DATA_DIR)
47
+
48
+ base.join("pull_requests_#{formatted_timestamp}")
49
+ end
50
+ end
51
+
52
+ def formatted_timestamp
53
+ timestamp.strftime('%Y%m%d_%H%M%S')
54
+ end
55
+
56
+ def prepare_parent(path)
57
+ FileUtils.mkdir_p(path.dirname)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NyaPr
4
+ class Progress
5
+ DEFAULT_WIDTH = 30
6
+
7
+ attr_reader :title, :total, :width, :output
8
+
9
+ def initialize(
10
+ title,
11
+ total:,
12
+ enabled: true,
13
+ width: DEFAULT_WIDTH,
14
+ output: $stderr
15
+ )
16
+ @title = title
17
+ @total = total
18
+ @width = width
19
+ @output = output
20
+ @enabled = enabled
21
+ @current = 0
22
+ @started_at = monotonic_time
23
+ @mutex = Mutex.new
24
+
25
+ render if enabled?
26
+ end
27
+
28
+ def advance(step = 1)
29
+ return unless enabled?
30
+
31
+ mutex.synchronize do
32
+ @current = [@current + step, total].min
33
+ render
34
+ end
35
+ end
36
+
37
+ def finish
38
+ return unless enabled?
39
+
40
+ mutex.synchronize do
41
+ render
42
+ output.puts
43
+ output.flush
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ attr_reader :current, :started_at, :mutex
50
+
51
+ def enabled?
52
+ @enabled && total.positive?
53
+ end
54
+
55
+ def render
56
+ output.print(
57
+ "\r\e[2K",
58
+ title,
59
+ ' [',
60
+ progress_bar,
61
+ '] ',
62
+ "#{current}/#{total} ",
63
+ "#{percentage}% ",
64
+ "ETA #{eta}"
65
+ )
66
+
67
+ output.flush
68
+ end
69
+
70
+ def progress_bar
71
+ completed = (width * progress).round
72
+ remaining = width - completed
73
+
74
+ ('█' * completed) + ('░' * remaining)
75
+ end
76
+
77
+ def progress
78
+ current.to_f / total
79
+ end
80
+
81
+ def percentage
82
+ (progress * 100).round
83
+ end
84
+
85
+ def eta
86
+ return '--' if current.zero?
87
+
88
+ elapsed = monotonic_time - started_at
89
+ rate = current / elapsed
90
+
91
+ return '--' unless rate.positive?
92
+
93
+ seconds = ((total - current) / rate).round
94
+
95
+ format_duration(seconds)
96
+ end
97
+
98
+ def format_duration(seconds)
99
+ return "#{seconds}s" if seconds < 60
100
+
101
+ minutes, seconds = seconds.divmod(60)
102
+
103
+ "#{minutes}m #{seconds.to_s.rjust(2, '0')}s"
104
+ end
105
+
106
+ def monotonic_time
107
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
108
+ end
109
+ end
110
+ end