fbe 0.38.2 → 0.40.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84ae98aaec66eaacc0865c0be05fc5490305a5e723c402c66eabc65d9d5490f2
4
- data.tar.gz: 4d3922a6dbfd4b04d8ac2eda2a155fda33ddc3e2c71886c0b99d61558830ff26
3
+ metadata.gz: d19c0535556f2bc17f04be6cd25be80552d3016e22d1abc52ad867425e29cd31
4
+ data.tar.gz: 9a9dc13820984a088462dae32ce4b1c3adeb236290770a56529ee9aa1308405e
5
5
  SHA512:
6
- metadata.gz: 15d6d7d1b03ebe56d06fef55c9981ccd6a97c5c072bbf54a81e87c9841323659af82124b6e968e2a4d9d1495672830249c8b66a7273f8411b56a7d6b85f9b0a5
7
- data.tar.gz: 79b5bd15396e2bed6317b1f202cd07f302c8a0db674165fafa1796da2119c0e22e2d218211106f4d6438b1aaa7012f1f102f80f59250908481876a00ebc6b956
6
+ metadata.gz: d81b8b123cd43763746e53d02f62b9f2430fb9ab0a21e70d4f6250ca499ac5a3fc59775ddfbd37138fadcc8c7d51f0cc8a724e4923c3c9837deecd022d3c789b
7
+ data.tar.gz: 63e1fd591916726362b7fdc68c2153f57f3e2a22c5db01b4d280262d6dd3fbbf86b573bc612c2e4c3fb51b8e0211f7c89e910f0b85bdb59bf1edfa4913f3e7ab
@@ -16,6 +16,6 @@ jobs:
16
16
  runs-on: ubuntu-24.04
17
17
  steps:
18
18
  - uses: actions/checkout@v5
19
- - uses: crate-ci/typos@v1.36.2
19
+ - uses: crate-ci/typos@v1.37.2
20
20
  with:
21
21
  config: .github/.typos.toml
data/Gemfile.lock CHANGED
@@ -87,7 +87,7 @@ GEM
87
87
  others (~> 0.1)
88
88
  tago (~> 0.1)
89
89
  yaml (~> 0.3)
90
- faraday (2.13.4)
90
+ faraday (2.14.0)
91
91
  faraday-net_http (>= 2.0, < 3.5)
92
92
  json
93
93
  logger
@@ -189,7 +189,7 @@ GEM
189
189
  regexp_parser (2.11.3)
190
190
  retries (0.0.5)
191
191
  rexml (3.4.4)
192
- rubocop (1.80.2)
192
+ rubocop (1.81.1)
193
193
  json (~> 2.3)
194
194
  language_server-protocol (~> 3.17.0.2)
195
195
  lint_roller (~> 1.1.0)
@@ -197,7 +197,7 @@ GEM
197
197
  parser (>= 3.3.0.2)
198
198
  rainbow (>= 2.2.2, < 4.0)
199
199
  regexp_parser (>= 2.9.3, < 3.0)
200
- rubocop-ast (>= 1.46.0, < 2.0)
200
+ rubocop-ast (>= 1.47.1, < 2.0)
201
201
  ruby-progressbar (~> 1.7)
202
202
  unicode-display_width (>= 2.4.0, < 4.0)
203
203
  rubocop-ast (1.47.1)
data/lib/fbe/conclude.rb CHANGED
@@ -206,7 +206,7 @@ class Fbe::Conclude
206
206
  passed = 0
207
207
  oct = Fbe.octo(loog: @loog, options: @options, global: @global)
208
208
  @fb.query(@query).each do |a|
209
- if @quota_aware && oct.off_quota?
209
+ if @quota_aware && oct.off_quota?(threshold: 100)
210
210
  @loog.info('We ran out of GitHub quota, must stop here')
211
211
  break
212
212
  end
@@ -232,6 +232,132 @@ class Fbe::Graph
232
232
  }
233
233
  end
234
234
 
235
+ # Get pulls id and number with review from since
236
+ #
237
+ # @param [String] owner The repository owner (username or organization)
238
+ # @param [String] name The repository name
239
+ # @param [Time] since The datetime from
240
+ # @param [String, nil] cursor Github cursor for next page
241
+ # @return [Hash] A hash with pulls
242
+ # @example
243
+ # graph = Fbe::Graph.new(token: 'github_token')
244
+ # cursor = nil
245
+ # pulls = []
246
+ # loop do
247
+ # json = graph.pull_requests_with_reviews(
248
+ # 'zerocracy', 'judges-action', Time.parse('2025-08-01T18:00:00Z'), cursor:
249
+ # )
250
+ # json['pulls_with_reviews'].each do |p|
251
+ # pulls.push(p['number'])
252
+ # end
253
+ # break unless json['has_next_page']
254
+ # cursor = json['next_cursor']
255
+ # end
256
+ def pull_requests_with_reviews(owner, name, since, cursor: nil)
257
+ result = query(
258
+ <<~GRAPHQL
259
+ {
260
+ repository(owner: "#{owner}", name: "#{name}") {
261
+ pullRequests(first: 100, after: "#{cursor}") {
262
+ nodes {
263
+ id
264
+ number
265
+ timelineItems(first: 1, itemTypes: [PULL_REQUEST_REVIEW], since: "#{since.utc.iso8601}") {
266
+ nodes {
267
+ ... on PullRequestReview { id }
268
+ }
269
+ }
270
+ }
271
+ pageInfo {
272
+ hasNextPage
273
+ endCursor
274
+ }
275
+ }
276
+ }
277
+ }
278
+ GRAPHQL
279
+ ).to_h
280
+ {
281
+ 'pulls_with_reviews' => result
282
+ .dig('repository', 'pullRequests', 'nodes')
283
+ .reject { _1.dig('timelineItems', 'nodes').empty? }
284
+ .map do |pull|
285
+ {
286
+ 'id' => pull['id'],
287
+ 'number' => pull['number']
288
+ }
289
+ end,
290
+ 'has_next_page' => result.dig('repository', 'pullRequests', 'pageInfo', 'hasNextPage'),
291
+ 'next_cursor' => result.dig('repository', 'pullRequests', 'pageInfo', 'endCursor')
292
+ }
293
+ end
294
+
295
+ # Get reviews by pull numbers
296
+ #
297
+ # @param [String] owner The repository owner (username or organization)
298
+ # @param [String] name The repository name
299
+ # @param [Array<Array<Integer, (String, nil)>>] pulls Array of pull number and Github cursor
300
+ # @return [Hash] A hash with reviews
301
+ # @example
302
+ # graph = Fbe::Graph.new(token: 'github_token')
303
+ # queue = [[1108, nil], [1105, nil]]
304
+ # until queue.empty?
305
+ # pulls = graph.pull_request_reviews('zerocracy', 'judges-action', pulls: queue.shift(10))
306
+ # pulls.each do |pull|
307
+ # puts pull['id'], pull['number']
308
+ # pull['reviews'].each do |r|
309
+ # puts r['id'], r['submitted_at']
310
+ # end
311
+ # end
312
+ # pulls.select { _1['reviews_has_next_page'] }.each do |p|
313
+ # queue.push([p['number'], p['reviews_next_cursor']])
314
+ # end
315
+ # end
316
+ def pull_request_reviews(owner, name, pulls: [])
317
+ requests =
318
+ pulls.map do |number, cursor|
319
+ <<~GRAPHQL
320
+ pr_#{number}: pullRequest(number: #{number}) {
321
+ id
322
+ number
323
+ reviews(first: 100, after: "#{cursor}") {
324
+ nodes {
325
+ id
326
+ submittedAt
327
+ }
328
+ pageInfo {
329
+ hasNextPage
330
+ endCursor
331
+ }
332
+ }
333
+ }
334
+ GRAPHQL
335
+ end
336
+ result = query(
337
+ <<~GRAPHQL
338
+ {
339
+ repository(owner: "#{owner}", name: "#{name}") {
340
+ #{requests.join("\n")}
341
+ }
342
+ }
343
+ GRAPHQL
344
+ ).to_h
345
+ result['repository'].map do |_k, v|
346
+ {
347
+ 'id' => v['id'],
348
+ 'number' => v['number'],
349
+ 'reviews' => v.dig('reviews', 'nodes').map do |r|
350
+ {
351
+ 'id' => r['id'],
352
+ 'submitted_at' => Time.parse(r['submittedAt'])
353
+ }
354
+ end,
355
+ 'reviews_has_next_page' => v.dig('reviews', 'pageInfo', 'hasNextPage'),
356
+ 'reviews_next_cursor' => v.dig('reviews', 'pageInfo', 'endCursor')
357
+ }
358
+ end
359
+ end
360
+
235
361
  private
236
362
 
237
363
  # Creates or returns a cached GraphQL client instance.
@@ -403,6 +529,47 @@ class Fbe::Graph
403
529
  end
404
530
  end
405
531
 
532
+ def pull_requests_with_reviews(_owner, _name, _since, **)
533
+ {
534
+ 'pulls_with_reviews' => [
535
+ { 'id' => 'PR_kwDOL6J6Ss6iprCx', 'number' => 2 },
536
+ { 'id' => 'PR_kwDOL6J6Ss6rhJ7T', 'number' => 5 },
537
+ { 'id' => 'PR_kwDOL6J6Ss6r13fG', 'number' => 21 }
538
+ ],
539
+ 'has_next_page' => false,
540
+ 'next_cursor' => 'Y3Vyc29yOnYyOpHOdh_xUw=='
541
+ }
542
+ end
543
+
544
+ def pull_request_reviews(_owner, _name, **)
545
+ [
546
+ {
547
+ 'id' => 'PR_kwDOL6J6Ss6iprCx',
548
+ 'number' => 2,
549
+ 'reviews' => [
550
+ { 'id' => 'PRR_kwDOL6J6Ss647NCl', 'submitted_at' => Time.parse('2025-10-02 12:58:42 UTC') },
551
+ { 'id' => 'PRR_kwDOL6J6Ss647NC8', 'submitted_at' => Time.parse('2025-10-02 15:58:42 UTC') }
552
+ ],
553
+ 'reviews_has_next_page' => false,
554
+ 'reviews_next_cursor' => 'yc29yOnYyO1'
555
+ },
556
+ {
557
+ 'id' => 'PR_kwDOL6J6Ss6rhJ7T',
558
+ 'number' => 5,
559
+ 'reviews' => [{ 'id' => 'PRR_kwDOL6J6Ss64_mnn', 'submitted_at' => Time.parse('2025-10-03 15:58:42 UTC') }],
560
+ 'reviews_has_next_page' => false,
561
+ 'reviews_next_cursor' => 'yc29yOnYyO2'
562
+ },
563
+ {
564
+ 'id' => 'PR_kwDOL6J6Ss6r13fG',
565
+ 'number' => 21,
566
+ 'reviews' => [{ 'id' => 'PRR_kwDOL6J6Ss65AbIA', 'submitted_at' => Time.parse('2025-10-04 15:58:42 UTC') }],
567
+ 'reviews_has_next_page' => false,
568
+ 'reviews_next_cursor' => 'yc29yOnYyO3'
569
+ }
570
+ ]
571
+ end
572
+
406
573
  private
407
574
 
408
575
  # Generates mock conversation thread data.
data/lib/fbe/iterate.rb CHANGED
@@ -97,6 +97,7 @@ class Fbe::Iterate
97
97
  @label = nil
98
98
  @since = 0
99
99
  @query = nil
100
+ @sort_by = nil
100
101
  @repeats = 1
101
102
  @quota_aware = true
102
103
  @lifetime_aware = true
@@ -164,6 +165,24 @@ class Fbe::Iterate
164
165
  @query = query
165
166
  end
166
167
 
168
+ # Sets the field to sort results by in ascending order.
169
+ #
170
+ # When set, all matching results will be fetched, sorted by the specified
171
+ # field, and iterated in order. This executes the query once per repository
172
+ # instead of calling one() repeatedly.
173
+ #
174
+ # @param [String] prop The fact attribute to sort by
175
+ # @return [nil] Nothing is returned
176
+ # @raise [RuntimeError] If prop is nil, already set, or not a valid field name
177
+ # @example Sort issues by number
178
+ # iterator.sort_by('issue')
179
+ def sort_by(prop)
180
+ raise 'Sort field is already set' unless @sort_by.nil?
181
+ raise 'Cannot set sort field to nil' if prop.nil?
182
+ raise 'Sort field must be a String' unless prop.is_a?(String)
183
+ @sort_by = prop
184
+ end
185
+
167
186
  # Sets the label for tracking iteration state.
168
187
  #
169
188
  # The label is used to create marker facts in the factbase that track
@@ -230,6 +249,7 @@ class Fbe::Iterate
230
249
  repos = Fbe.unmask_repos(
231
250
  loog: @loog, options: @options, global: @global, quota_aware: @quota_aware
232
251
  ).map { |n| oct.repo_id_by_name(n) }
252
+ started = Time.now
233
253
  restarted = []
234
254
  before =
235
255
  repos.to_h do |repo|
@@ -245,8 +265,9 @@ class Fbe::Iterate
245
265
  ]
246
266
  end
247
267
  starts = before.dup
268
+ values = {}
248
269
  loop do
249
- if @quota_aware && oct.off_quota?
270
+ if @quota_aware && oct.off_quota?(threshold: 100)
250
271
  @loog.info("We are off GitHub quota, time to stop after #{started.ago}")
251
272
  break
252
273
  end
@@ -259,7 +280,7 @@ class Fbe::Iterate
259
280
  break
260
281
  end
261
282
  repos.each do |repo|
262
- if @quota_aware && oct.off_quota?
283
+ if @quota_aware && oct.off_quota?(threshold: 100)
263
284
  @loog.info("We are off GitHub quota, we must skip #{repo}")
264
285
  break
265
286
  end
@@ -277,11 +298,24 @@ class Fbe::Iterate
277
298
  @loog.debug("We've seen too many (#{seen[repo]}) in #{repo}, let's see next one")
278
299
  next
279
300
  end
280
- nxt = @fb.query(@query).one(@fb, before: before[repo], repository: repo)
301
+ nxt =
302
+ if @sort_by
303
+ values[repo] ||= @fb.query(@query).each(
304
+ @fb, before: before[repo], repository: repo
305
+ ).map { _1[@sort_by]&.first }.compact.sort.each
306
+ begin
307
+ values[repo].next
308
+ rescue StopIteration
309
+ nil
310
+ end
311
+ else
312
+ @fb.query(@query).one(@fb, before: before[repo], repository: repo)
313
+ end
281
314
  before[repo] =
282
315
  if nxt.nil?
283
316
  @loog.debug("Next element after ##{before[repo]} not suggested, re-starting from ##{@since}: #{@query}")
284
317
  restarted << repo
318
+ values.delete(repo) if @sort_by
285
319
  @since
286
320
  else
287
321
  @loog.debug("Next is ##{nxt}, starting from it")
data/lib/fbe/octo.rb CHANGED
@@ -24,6 +24,9 @@ require_relative 'middleware/rate_limit'
24
24
  require_relative 'middleware/sqlite_store'
25
25
  require_relative 'middleware/trace'
26
26
 
27
+ # When we are off quota.
28
+ class Fbe::OffQuota < StandardError; end
29
+
27
30
  # Makes a call to the GitHub API.
28
31
  #
29
32
  # It is supposed to be used instead of +Octokit::Client+, because it
@@ -221,7 +224,7 @@ def Fbe.octo(options: $options, global: $global, loog: $loog)
221
224
  o =
222
225
  intercepted(o) do |e, m, _args, _r|
223
226
  if e == :before && m != :off_quota? && m != :print_trace! && m != :rate_limit && o.off_quota?
224
- raise "We are off-quota (remaining: #{o.rate_limit.remaining}), can't do #{m}()"
227
+ raise Fbe::OffQuota, "We are off-quota (remaining: #{o.rate_limit.remaining}), can't do #{m}()"
225
228
  end
226
229
  end
227
230
  o
data/lib/fbe.rb CHANGED
@@ -10,5 +10,5 @@
10
10
  # License:: MIT
11
11
  module Fbe
12
12
  # Current version of the gem (changed by +.rultor.yml+ on every release)
13
- VERSION = '0.38.2' unless const_defined?(:VERSION)
13
+ VERSION = '0.40.0' unless const_defined?(:VERSION)
14
14
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fbe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.38.2
4
+ version: 0.40.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko