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 +7 -0
- data/LICENSE.md +22 -0
- data/README.md +314 -0
- data/exe/nya-pr +14 -0
- data/lib/nya_pr/cli/command.rb +110 -0
- data/lib/nya_pr/client.rb +12 -0
- data/lib/nya_pr/connection.rb +34 -0
- data/lib/nya_pr/error.rb +13 -0
- data/lib/nya_pr/paths.rb +60 -0
- data/lib/nya_pr/progress.rb +110 -0
- data/lib/nya_pr/pull_request/csv_store.rb +32 -0
- data/lib/nya_pr/pull_request/finder.rb +136 -0
- data/lib/nya_pr/pull_request/printer.rb +42 -0
- data/lib/nya_pr/repository/collector.rb +85 -0
- data/lib/nya_pr/repository/csv_store.rb +49 -0
- data/lib/nya_pr/repository/filter.rb +117 -0
- data/lib/nya_pr/repository/finder.rb +89 -0
- data/lib/nya_pr/request.rb +97 -0
- data/lib/nya_pr/runner.rb +101 -0
- data/lib/nya_pr/version.rb +5 -0
- data/lib/nya_pr.rb +24 -0
- metadata +151 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NyaPr
|
|
4
|
+
module PullRequest
|
|
5
|
+
class CsvStore
|
|
6
|
+
HEADERS = %w[
|
|
7
|
+
repository
|
|
8
|
+
number
|
|
9
|
+
title
|
|
10
|
+
author
|
|
11
|
+
draft
|
|
12
|
+
created_at
|
|
13
|
+
updated_at
|
|
14
|
+
html_url
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :path
|
|
18
|
+
|
|
19
|
+
def initialize(path)
|
|
20
|
+
@path = path
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def write(pull_requests)
|
|
24
|
+
CSV.open(path, 'w', write_headers: true, headers: HEADERS) do |csv|
|
|
25
|
+
pull_requests.each do |pull_request|
|
|
26
|
+
csv << HEADERS.map { |header| pull_request[header] }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NyaPr
|
|
4
|
+
module PullRequest
|
|
5
|
+
class Finder
|
|
6
|
+
include NyaPr::Request
|
|
7
|
+
|
|
8
|
+
MAX_PULL_REQUESTS = 100
|
|
9
|
+
MAX_PER_PAGE = 100
|
|
10
|
+
|
|
11
|
+
attr_reader :client,
|
|
12
|
+
:repositories,
|
|
13
|
+
:limit,
|
|
14
|
+
:progress_enabled
|
|
15
|
+
|
|
16
|
+
def initialize(
|
|
17
|
+
client,
|
|
18
|
+
repositories,
|
|
19
|
+
limit: MAX_PULL_REQUESTS,
|
|
20
|
+
progress_enabled: true
|
|
21
|
+
)
|
|
22
|
+
@client = client
|
|
23
|
+
@repositories = repositories
|
|
24
|
+
@limit = limit
|
|
25
|
+
@progress_enabled = progress_enabled
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def find
|
|
29
|
+
NyaPr.logger.info(
|
|
30
|
+
"Searching for up to #{limit} open pull requests " \
|
|
31
|
+
"in #{repositories.size} repositories"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
progress_bar = build_progress
|
|
35
|
+
pull_requests = collect_pull_requests(progress_bar)
|
|
36
|
+
|
|
37
|
+
progress_bar.finish
|
|
38
|
+
|
|
39
|
+
NyaPr.logger.info(
|
|
40
|
+
"Finished searching for pull requests: found #{pull_requests.size}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
pull_requests
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def collect_pull_requests(progress_bar)
|
|
49
|
+
pull_requests = []
|
|
50
|
+
|
|
51
|
+
repositories.each do |repository|
|
|
52
|
+
break if pull_requests.size >= limit
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
next unless repository.fetch('has_pull_requests', true)
|
|
56
|
+
|
|
57
|
+
remaining = limit - pull_requests.size
|
|
58
|
+
|
|
59
|
+
pull_requests.concat(
|
|
60
|
+
pull_requests_for(repository, remaining)
|
|
61
|
+
)
|
|
62
|
+
ensure
|
|
63
|
+
progress_bar.advance
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
pull_requests
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def pull_requests_for(repository, remaining)
|
|
71
|
+
NyaPr.logger.debug(
|
|
72
|
+
"Checking pull requests in #{repository.fetch('full_name')}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
pull_requests = []
|
|
76
|
+
page = 1
|
|
77
|
+
|
|
78
|
+
loop do
|
|
79
|
+
per_page = page_size(remaining, pull_requests.size)
|
|
80
|
+
batch = get_batch(repository, per_page, page)
|
|
81
|
+
|
|
82
|
+
break if batch.empty?
|
|
83
|
+
|
|
84
|
+
pull_requests.concat(
|
|
85
|
+
batch.map do |pull_request|
|
|
86
|
+
normalize(repository, pull_request)
|
|
87
|
+
end
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
break if batch.size < per_page
|
|
91
|
+
break if pull_requests.size >= remaining
|
|
92
|
+
|
|
93
|
+
page += 1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
pull_requests
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def page_size(remaining, collected)
|
|
100
|
+
[remaining - collected, MAX_PER_PAGE].min
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def get_batch(repository, per_page, page)
|
|
104
|
+
get(
|
|
105
|
+
"/repos/#{repository.fetch('full_name')}/pulls",
|
|
106
|
+
state: 'open',
|
|
107
|
+
sort: 'updated',
|
|
108
|
+
direction: 'desc',
|
|
109
|
+
per_page: per_page,
|
|
110
|
+
page: page
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_progress
|
|
115
|
+
Progress.new(
|
|
116
|
+
'🐾 Checking pull requests',
|
|
117
|
+
total: repositories.size,
|
|
118
|
+
enabled: progress_enabled && $stderr.tty?
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def normalize(repository, pull_request)
|
|
123
|
+
{
|
|
124
|
+
'repository' => repository.fetch('full_name'),
|
|
125
|
+
'number' => pull_request.fetch('number'),
|
|
126
|
+
'title' => pull_request.fetch('title'),
|
|
127
|
+
'author' => pull_request.dig('user', 'login'),
|
|
128
|
+
'draft' => pull_request.fetch('draft', false),
|
|
129
|
+
'created_at' => pull_request['created_at'],
|
|
130
|
+
'updated_at' => pull_request['updated_at'],
|
|
131
|
+
'html_url' => pull_request.fetch('html_url')
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NyaPr
|
|
4
|
+
module PullRequest
|
|
5
|
+
class Printer
|
|
6
|
+
def print(pull_requests)
|
|
7
|
+
grouped_pull_requests(pull_requests).each do |repository, repository_pull_requests|
|
|
8
|
+
print_repository(repository, repository_pull_requests)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def grouped_pull_requests(pull_requests)
|
|
15
|
+
pull_requests.group_by do |pull_request|
|
|
16
|
+
pull_request['repository']
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def print_repository(repository, pull_requests)
|
|
21
|
+
puts
|
|
22
|
+
puts repository
|
|
23
|
+
puts '-' * repository.length
|
|
24
|
+
|
|
25
|
+
pull_requests.each do |pull_request|
|
|
26
|
+
puts format_pull_request(pull_request)
|
|
27
|
+
puts
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def format_pull_request(pull_request)
|
|
32
|
+
draft = pull_request['draft'] ? ' [DRAFT]' : ''
|
|
33
|
+
|
|
34
|
+
<<~OUTPUT.chomp
|
|
35
|
+
##{pull_request['number']}#{draft} #{pull_request['title']}
|
|
36
|
+
Author: @#{pull_request['author']}
|
|
37
|
+
#{pull_request['html_url']}
|
|
38
|
+
OUTPUT
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NyaPr
|
|
4
|
+
module Repository
|
|
5
|
+
class Collector
|
|
6
|
+
attr_reader :client, :username, :owners, :store, :options
|
|
7
|
+
|
|
8
|
+
def initialize(client, username, owners, store, **options)
|
|
9
|
+
@client = client
|
|
10
|
+
@username = username
|
|
11
|
+
@owners = owners
|
|
12
|
+
@store = store
|
|
13
|
+
@options = options
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def collect
|
|
17
|
+
return load_cached if use_cache?
|
|
18
|
+
|
|
19
|
+
discover
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def load_cached
|
|
25
|
+
NyaPr.logger.info("Loading repositories from #{store.path}")
|
|
26
|
+
|
|
27
|
+
reject_archived(store.read).tap do |repositories|
|
|
28
|
+
NyaPr.logger.info(
|
|
29
|
+
"Loaded #{repositories.size} repositories from cache"
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def discover
|
|
35
|
+
NyaPr.logger.info(
|
|
36
|
+
'Repository cache unavailable or refresh requested'
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
repositories = Finder.
|
|
40
|
+
new(client, username).
|
|
41
|
+
repositories_for(owners)
|
|
42
|
+
|
|
43
|
+
repositories = filter_contributed(reject_archived(repositories))
|
|
44
|
+
|
|
45
|
+
store.write(repositories)
|
|
46
|
+
|
|
47
|
+
NyaPr.logger.info(
|
|
48
|
+
"Saved #{repositories.size} repositories to #{store.path}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
repositories
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def filter_contributed(repositories)
|
|
55
|
+
own, external = repositories.partition do |repository|
|
|
56
|
+
repository_owner(repository).casecmp?(username)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
own + Filter.new(client, username, progress_enabled: options[:progress]).contributed(external)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def reject_archived(repositories)
|
|
63
|
+
return repositories unless options[:skip_archived]
|
|
64
|
+
|
|
65
|
+
filtered = repositories.reject do |repository|
|
|
66
|
+
repository['archived']
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
NyaPr.logger.info(
|
|
70
|
+
"Skipped #{repositories.size - filtered.size} archived repositories"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
filtered
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def repository_owner(repository)
|
|
77
|
+
repository.dig('owner', 'login').to_s
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def use_cache?
|
|
81
|
+
store.available? && !options[:refresh]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NyaPr
|
|
4
|
+
module Repository
|
|
5
|
+
class CsvStore
|
|
6
|
+
HEADERS = %w[
|
|
7
|
+
full_name
|
|
8
|
+
html_url
|
|
9
|
+
visibility
|
|
10
|
+
private
|
|
11
|
+
archived
|
|
12
|
+
has_pull_requests
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :path
|
|
16
|
+
|
|
17
|
+
def initialize(path)
|
|
18
|
+
@path = path
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def available?
|
|
22
|
+
return false unless File.file?(path)
|
|
23
|
+
|
|
24
|
+
CSV.open(path, headers: true) { |csv| !csv.first.nil? }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def read
|
|
28
|
+
CSV.read(path, headers: true).map do |row|
|
|
29
|
+
{
|
|
30
|
+
'full_name' => row['full_name'],
|
|
31
|
+
'html_url' => row['html_url'],
|
|
32
|
+
'visibility' => row['visibility'],
|
|
33
|
+
'private' => row['private'] == 'true',
|
|
34
|
+
'archived' => row['archived'] == 'true',
|
|
35
|
+
'has_pull_requests' => row['has_pull_requests'] != 'false'
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def write(repositories)
|
|
41
|
+
CSV.open(path, 'w', write_headers: true, headers: HEADERS) do |csv|
|
|
42
|
+
repositories.each do |repository|
|
|
43
|
+
csv << HEADERS.map { |header| repository[header] }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NyaPr
|
|
4
|
+
module Repository
|
|
5
|
+
class Filter
|
|
6
|
+
include NyaPr::Request
|
|
7
|
+
|
|
8
|
+
DEFAULT_WORKERS = 10
|
|
9
|
+
|
|
10
|
+
attr_reader :client, :username, :workers, :progress_enabled
|
|
11
|
+
|
|
12
|
+
def initialize(client, username, workers: DEFAULT_WORKERS, progress_enabled: true)
|
|
13
|
+
@client = client
|
|
14
|
+
@username = username
|
|
15
|
+
@workers = workers
|
|
16
|
+
@progress_enabled = progress_enabled
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def contributed(repositories)
|
|
20
|
+
log_start(repositories)
|
|
21
|
+
|
|
22
|
+
progress_bar = build_progress(repositories)
|
|
23
|
+
result = process_repositories(repositories, progress_bar)
|
|
24
|
+
|
|
25
|
+
progress_bar.finish
|
|
26
|
+
|
|
27
|
+
log_result(result)
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def log_start(repositories)
|
|
34
|
+
NyaPr.logger.info(
|
|
35
|
+
"Checking contributions in #{repositories.size} repositories " \
|
|
36
|
+
"using #{workers} workers"
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_progress(repositories)
|
|
41
|
+
Progress.new(
|
|
42
|
+
'🐈 Checking repositories',
|
|
43
|
+
total: repositories.size,
|
|
44
|
+
enabled: progress_enabled && $stderr.tty?
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def process_repositories(repositories, progress_bar)
|
|
49
|
+
queue = build_queue(repositories)
|
|
50
|
+
result = Array.new(repositories.size)
|
|
51
|
+
|
|
52
|
+
build_workers(queue, result, progress_bar).
|
|
53
|
+
each(&:value)
|
|
54
|
+
|
|
55
|
+
result.compact
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_workers(queue, result, progress_bar)
|
|
59
|
+
Array.new(workers) do
|
|
60
|
+
Thread.new do
|
|
61
|
+
process_queue(queue, result, progress_bar)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def log_result(result)
|
|
67
|
+
NyaPr.logger.info(
|
|
68
|
+
"Finished contribution check: #{result.size} repositories matched"
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def build_queue(repositories)
|
|
73
|
+
Queue.new.tap do |queue|
|
|
74
|
+
repositories.each_with_index do |repository, index|
|
|
75
|
+
queue << [index, repository]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def process_queue(queue, result, progress_bar)
|
|
81
|
+
loop do
|
|
82
|
+
index, repository = queue.pop(true)
|
|
83
|
+
|
|
84
|
+
if contributed_to?(repository)
|
|
85
|
+
result[index] = repository
|
|
86
|
+
|
|
87
|
+
NyaPr.logger.debug(
|
|
88
|
+
"Found contributions in #{repository.fetch('full_name')}"
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
progress_bar.advance
|
|
93
|
+
rescue ThreadError
|
|
94
|
+
break
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def contributed_to?(repository)
|
|
99
|
+
commits = get(
|
|
100
|
+
"/repos/#{repository.fetch('full_name')}/commits",
|
|
101
|
+
author: username,
|
|
102
|
+
per_page: 1
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
commits.any?
|
|
106
|
+
rescue NyaPr::Error => e
|
|
107
|
+
raise unless e.status == 409
|
|
108
|
+
|
|
109
|
+
NyaPr.logger.debug(
|
|
110
|
+
"Skipping empty repository #{repository.fetch('full_name')}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
false
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NyaPr
|
|
4
|
+
module Repository
|
|
5
|
+
class Finder
|
|
6
|
+
include NyaPr::Request
|
|
7
|
+
|
|
8
|
+
attr_reader :client, :username
|
|
9
|
+
|
|
10
|
+
def initialize(client, username)
|
|
11
|
+
@client = client
|
|
12
|
+
@username = username
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def repositories_for(owners)
|
|
16
|
+
owners = Array(owners)
|
|
17
|
+
|
|
18
|
+
NyaPr.logger.info(
|
|
19
|
+
"Starting repository collection for #{owners.size} owners"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
repositories = owners.flat_map do |owner|
|
|
23
|
+
collect_repositories_for(owner)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
NyaPr.logger.info(
|
|
27
|
+
"Finished repository collection: #{repositories.size} repositories found"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
repositories
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def collect_repositories_for(owner)
|
|
36
|
+
NyaPr.logger.info("Collecting repositories for #{owner}")
|
|
37
|
+
|
|
38
|
+
repositories_for_owner(owner).tap do |repositories|
|
|
39
|
+
NyaPr.logger.info(
|
|
40
|
+
"Collected #{repositories.size} repositories for #{owner}"
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def repositories_for_owner(owner)
|
|
46
|
+
return own_repositories if owner.casecmp?(username)
|
|
47
|
+
|
|
48
|
+
type = owner_type(owner)
|
|
49
|
+
|
|
50
|
+
NyaPr.logger.debug("#{owner} is a GitHub #{type}")
|
|
51
|
+
|
|
52
|
+
case type
|
|
53
|
+
when 'Organization'
|
|
54
|
+
organization_repositories(owner)
|
|
55
|
+
when 'User'
|
|
56
|
+
user_repositories(owner)
|
|
57
|
+
else
|
|
58
|
+
raise NyaPr::Error, "Unsupported GitHub owner type: #{owner}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def owner_type(owner)
|
|
63
|
+
get("/users/#{owner}").fetch('type')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def own_repositories
|
|
67
|
+
paginate(
|
|
68
|
+
'/user/repos',
|
|
69
|
+
affiliation: 'owner',
|
|
70
|
+
visibility: 'all'
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def organization_repositories(owner)
|
|
75
|
+
paginate(
|
|
76
|
+
"/orgs/#{owner}/repos",
|
|
77
|
+
type: 'all'
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def user_repositories(owner)
|
|
82
|
+
paginate(
|
|
83
|
+
"/users/#{owner}/repos",
|
|
84
|
+
type: 'owner'
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'oj'
|
|
4
|
+
|
|
5
|
+
module NyaPr
|
|
6
|
+
module Request
|
|
7
|
+
include NyaPr::Connection
|
|
8
|
+
|
|
9
|
+
MAX_PAGES = 1_000
|
|
10
|
+
PER_PAGE = 100
|
|
11
|
+
|
|
12
|
+
def get(path, params = {})
|
|
13
|
+
_response, body = do_get(path, params)
|
|
14
|
+
|
|
15
|
+
body
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def paginate(path, params = {})
|
|
19
|
+
params = params.merge(per_page: params.fetch(:per_page, PER_PAGE))
|
|
20
|
+
|
|
21
|
+
page = 1
|
|
22
|
+
result = []
|
|
23
|
+
|
|
24
|
+
loop do
|
|
25
|
+
raise NyaPr::Error, 'Pagination limit exceeded' if page > MAX_PAGES
|
|
26
|
+
|
|
27
|
+
NyaPr.logger.debug("Fetching page #{page} for #{path}")
|
|
28
|
+
response, body = do_get(path, params.merge(page: page))
|
|
29
|
+
|
|
30
|
+
raise NyaPr::Error, 'Expected paginated response to be an array' unless body.is_a?(Array)
|
|
31
|
+
|
|
32
|
+
result.concat(body)
|
|
33
|
+
|
|
34
|
+
break if body.empty?
|
|
35
|
+
break unless next_page?(response)
|
|
36
|
+
|
|
37
|
+
page += 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def do_get(path, params = {})
|
|
46
|
+
prepared_path = prepare(path)
|
|
47
|
+
|
|
48
|
+
NyaPr.logger.debug("GET #{prepared_path} #{params.inspect}")
|
|
49
|
+
|
|
50
|
+
response = connection(client).get(prepared_path, params)
|
|
51
|
+
body = parse_body(response)
|
|
52
|
+
|
|
53
|
+
NyaPr.logger.debug(
|
|
54
|
+
"GET #{prepared_path} -> HTTP #{response.status}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
raise_on_error!(response, body, prepared_path)
|
|
58
|
+
|
|
59
|
+
[response, body]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def parse_body(response)
|
|
63
|
+
return nil if response.body.nil? || response.body.empty?
|
|
64
|
+
|
|
65
|
+
Oj.load(response.body)
|
|
66
|
+
rescue Oj::ParseError
|
|
67
|
+
raise NyaPr::Error, "Failed to parse response: #{response.body}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def raise_on_error!(response, body, path)
|
|
71
|
+
return if response.success?
|
|
72
|
+
|
|
73
|
+
message =
|
|
74
|
+
if body.is_a?(Hash)
|
|
75
|
+
body['message'] || body
|
|
76
|
+
else
|
|
77
|
+
body
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
raise NyaPr::Error.new(
|
|
81
|
+
"GET #{path}: GitHub API error #{response.status}: #{message}",
|
|
82
|
+
status: response.status
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def next_page?(response)
|
|
87
|
+
response.headers['link']&.include?('rel="next"')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def prepare(path)
|
|
91
|
+
path.
|
|
92
|
+
delete_prefix('/').
|
|
93
|
+
gsub(%r{//}, '/').
|
|
94
|
+
gsub(%r{/+\z}, '')
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|