hexabat 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +20 -0
- data/.rvmrc +1 -0
- data/.travis.yml +12 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +119 -0
- data/Rakefile +32 -0
- data/features/callbacks.feature +21 -0
- data/features/fixtures/100_open_issues.rb +3500 -0
- data/features/fixtures/300_closed_issues.rb +9515 -0
- data/features/fixtures/single_open_issue.rb +60 -0
- data/features/step_definitions/callbacks.rb +74 -0
- data/features/support/env.rb +37 -0
- data/hexabat.gemspec +26 -0
- data/lib/hexabat/client.rb +68 -0
- data/lib/hexabat/importer.rb +45 -0
- data/lib/hexabat/issue_count.rb +49 -0
- data/lib/hexabat/page_range.rb +126 -0
- data/lib/hexabat/page_request_builder.rb +76 -0
- data/lib/hexabat/page_response_processor.rb +35 -0
- data/lib/hexabat/version.rb +3 -0
- data/lib/hexabat.rb +26 -0
- data/spec/hexabat/client_spec.rb +45 -0
- data/spec/hexabat/importer_spec.rb +36 -0
- data/spec/hexabat/issue_count_spec.rb +42 -0
- data/spec/hexabat/page_range_spec.rb +121 -0
- data/spec/hexabat/page_request_buider_spec.rb +47 -0
- data/spec/hexabat/page_response_processor_spec.rb +42 -0
- data/spec/hexabat_spec.rb +23 -0
- metadata +211 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
SINGLE_OPEN_ISSUE = %q{
|
2
|
+
[
|
3
|
+
{
|
4
|
+
"url": "https://api.github.com/repos/path11/hexabat/issues/1",
|
5
|
+
"html_url": "https://github.com/path11/hexabat/issues/1",
|
6
|
+
"number": 1347,
|
7
|
+
"state": "open",
|
8
|
+
"title": "Found a bug",
|
9
|
+
"body": "I'm having a problem with this.",
|
10
|
+
"user": {
|
11
|
+
"login": "path11",
|
12
|
+
"id": 1,
|
13
|
+
"avatar_url": "https://github.com/images/error/path11_happy.gif",
|
14
|
+
"gravatar_id": "somehexcode",
|
15
|
+
"url": "https://api.github.com/users/path11"
|
16
|
+
},
|
17
|
+
"labels": [
|
18
|
+
{
|
19
|
+
"url": "https://api.github.com/repos/path11/hexabat/labels/bug",
|
20
|
+
"name": "bug",
|
21
|
+
"color": "f29513"
|
22
|
+
}
|
23
|
+
],
|
24
|
+
"assignee": {
|
25
|
+
"login": "path11",
|
26
|
+
"id": 1,
|
27
|
+
"avatar_url": "https://github.com/images/error/path11_happy.gif",
|
28
|
+
"gravatar_id": "somehexcode",
|
29
|
+
"url": "https://api.github.com/users/path11"
|
30
|
+
},
|
31
|
+
"milestone": {
|
32
|
+
"url": "https://api.github.com/repos/path11/hexabat/milestones/1",
|
33
|
+
"number": 1,
|
34
|
+
"state": "open",
|
35
|
+
"title": "v1.0",
|
36
|
+
"description": "",
|
37
|
+
"creator": {
|
38
|
+
"login": "path11",
|
39
|
+
"id": 1,
|
40
|
+
"avatar_url": "https://github.com/images/error/path11_happy.gif",
|
41
|
+
"gravatar_id": "somehexcode",
|
42
|
+
"url": "https://api.github.com/users/path11"
|
43
|
+
},
|
44
|
+
"open_issues": 4,
|
45
|
+
"closed_issues": 8,
|
46
|
+
"created_at": "2011-04-10T20:09:31Z",
|
47
|
+
"due_on": null
|
48
|
+
},
|
49
|
+
"comments": 0,
|
50
|
+
"pull_request": {
|
51
|
+
"html_url": "https://github.com/path11/hexabat/issues/1",
|
52
|
+
"diff_url": "https://github.com/path11/hexabat/issues/1.diff",
|
53
|
+
"patch_url": "https://github.com/path11/hexabat/issues/1.patch"
|
54
|
+
},
|
55
|
+
"closed_at": null,
|
56
|
+
"created_at": "2011-04-22T13:33:48Z",
|
57
|
+
"updated_at": "2011-04-22T13:33:48Z"
|
58
|
+
}
|
59
|
+
]
|
60
|
+
}
|
@@ -0,0 +1,74 @@
|
|
1
|
+
Given /^there is a single open issue on "(.*?)"$/ do |repository|
|
2
|
+
stub_request(:get, "https://api.github.com/repos/#{repository}/issues?page=1&per_page=100&state=open").
|
3
|
+
to_return(:status => 200, :body => SINGLE_OPEN_ISSUE, :headers => {})
|
4
|
+
stub_request(:get, "https://api.github.com/repos/#{repository}/issues?page=1&per_page=100&state=closed").
|
5
|
+
to_return(:status => 200, :body => '[]', :headers => {})
|
6
|
+
end
|
7
|
+
|
8
|
+
Given /^there are 101 open issues and 300 closed issues on "(.*?)"$/ do |repository|
|
9
|
+
#open issues page 1
|
10
|
+
link = '<https://api.github.com/repos/path11/hexabat/issues?page=2&per_page=100>; rel="next", <https://api.github.com/repos/path11/hexabat/issues?page=2&per_page=100>; rel="last"'
|
11
|
+
stub_request(:get, "https://api.github.com/repos/#{repository}/issues?page=1&per_page=100&state=open").
|
12
|
+
to_return(:status => 200, :body => A_100_OPEN_ISSUES, :headers => {'Link' => link})
|
13
|
+
|
14
|
+
#open issues page 2
|
15
|
+
link = '<https://api.github.com/repos/path11/hexabat/issues?page=1&per_page=100>; rel="last", <https://api.github.com/repos/path11/hexabat/issues?page=1&per_page=100>; rel="first", <https://api.github.com/repos/path11/hexabat/issues?page=1&per_page=100>; rel="prev"'
|
16
|
+
stub_request(:get, "https://api.github.com/repos/#{repository}/issues?page=2&per_page=100&state=open").
|
17
|
+
to_return(:status => 200, :body => SINGLE_OPEN_ISSUE, :headers => {'Link' => link})
|
18
|
+
|
19
|
+
#closed issues page 1
|
20
|
+
link = '<https://api.github.com/repos/path11/hexabat/issues?page=2&per_page=100&state=closed>; rel="next", <https://api.github.com/repos/path11/hexabat/issues?page=3&per_page=100&state=closed>; rel="last"'
|
21
|
+
stub_request(:get, "https://api.github.com/repos/#{repository}/issues?page=1&per_page=100&state=closed").
|
22
|
+
to_return(:status => 200, :body => CLOSED_ISSUES_PAGE_1, :headers => {'Link' => link})
|
23
|
+
|
24
|
+
#closed issues page 2
|
25
|
+
link = '<https://api.github.com/repos/path11/hexabat/issues?page=3&per_page=100&state=closed>; rel="next", <https://api.github.com/repos/path11/hexabat/issues?page=3&per_page=100&state=closed>; rel="last", <https://api.github.com/repos/path11/hexabat/issues?page=1&per_page=100&state=closed>; rel="first", <https://api.github.com/repos/path11/hexabat/issues?page=1&per_page=100&state=closed>; rel="prev"'
|
26
|
+
stub_request(:get, "https://api.github.com/repos/#{repository}/issues?page=2&per_page=100&state=closed").
|
27
|
+
to_return(:status => 200, :body => CLOSED_ISSUES_PAGE_2, :headers => {'Link' => link})
|
28
|
+
|
29
|
+
#closed issues page 3
|
30
|
+
link = '<https://api.github.com/repos/path11/hexabat/issues?page=1&per_page=100&state=closed>; rel="last", <https://api.github.com/repos/path11/hexabat/issues?page=1&per_page=100&state=closed>; rel="first", <https://api.github.com/repos/path11/hexabat/issues?page=2&per_page=100&state=closed>; rel="prev"'
|
31
|
+
stub_request(:get, "https://api.github.com/repos/#{repository}/issues?page=3&per_page=100&state=closed").
|
32
|
+
to_return(:status => 200, :body => CLOSED_ISSUES_PAGE_3, :headers => {'Link' => link})
|
33
|
+
end
|
34
|
+
|
35
|
+
Given /^the repository "(.*?)" doesn't exist$/ do |repository|
|
36
|
+
stub_request(:get, "https://api.github.com/repos/#{repository}/issues?page=1&per_page=100&state=closed").
|
37
|
+
to_return(:status => 404, :body => '{"message": "Not found"}', :headers => {})
|
38
|
+
stub_request(:get, "https://api.github.com/repos/#{repository}/issues?page=1&per_page=100&state=open").
|
39
|
+
to_return(:status => 404, :body => '{"message": "Not found"}', :headers => {})
|
40
|
+
end
|
41
|
+
|
42
|
+
When /^I set up an issue retrieved callback$/ do
|
43
|
+
setup_callbacks
|
44
|
+
end
|
45
|
+
|
46
|
+
When /^I setup the issue count known callback$/ do
|
47
|
+
setup_callbacks
|
48
|
+
end
|
49
|
+
|
50
|
+
When /^I import the "(.*?)" repository$/ do |repository|
|
51
|
+
EM.run do
|
52
|
+
EM.add_timer(TIMEOUT){ EM.stop }
|
53
|
+
Hexabat.import repository
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
When /^I setup the errback$/ do
|
58
|
+
setup_errback
|
59
|
+
end
|
60
|
+
|
61
|
+
Then /^the callback is called with the issue in that repository$/ do
|
62
|
+
@retrieved_issues.count.should be 1
|
63
|
+
end
|
64
|
+
|
65
|
+
Then /^the callback is called with the number of issues of the repository$/ do
|
66
|
+
@issue_count.should eq 401
|
67
|
+
@retrieved_issues.count.should eq @issue_count
|
68
|
+
end
|
69
|
+
|
70
|
+
Then /^the errback is called with the error message$/ do
|
71
|
+
@error_repository = 'path11/rails'
|
72
|
+
@error_status = '404'
|
73
|
+
@error_message = 'Not found'
|
74
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', '..', 'lib')
|
2
|
+
|
3
|
+
require 'hexabat'
|
4
|
+
require 'webmock/cucumber'
|
5
|
+
|
6
|
+
TIMEOUT = 2
|
7
|
+
|
8
|
+
module CallbackSetup
|
9
|
+
def setup_callbacks
|
10
|
+
setup_issue_count_known
|
11
|
+
setup_issue_retrieved
|
12
|
+
end
|
13
|
+
|
14
|
+
def setup_errback
|
15
|
+
Hexabat.on_error do |repository, status, message|
|
16
|
+
@error_repository = repository
|
17
|
+
@error_status = status
|
18
|
+
@error_message = message
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def setup_issue_retrieved
|
23
|
+
@retrieved_issues = []
|
24
|
+
Hexabat.on_issue_retrieved do |issue|
|
25
|
+
@retrieved_issues << issue
|
26
|
+
EM.stop if @retrieved_issues.count == @issue_count_known
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def setup_issue_count_known
|
31
|
+
@issue_count = 0
|
32
|
+
Hexabat.on_issue_count_known { |count| @issue_count = count }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
World(CallbackSetup)
|
37
|
+
World(WebMock::API)
|
data/hexabat.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/hexabat/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = %w{path11}
|
6
|
+
gem.email = %w{alberto@path11.com ecomba@path11.com javier@path11.com sebastian@path11.com}
|
7
|
+
gem.description = %q{A Github issues importing tool}
|
8
|
+
gem.summary = %q{Importing all the issues of a Github repository is a complex task: the Github API does not provide an easy way of doing it. Hexabat will help you with that. It will allow you to find out the total number of issues (counting both open and closed ones) and to perform an action with each one of them.}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "hexabat"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Hexabat::VERSION
|
17
|
+
|
18
|
+
gem.add_runtime_dependency 'eventmachine', '~> 1.0.0'
|
19
|
+
gem.add_runtime_dependency 'em-http-request'
|
20
|
+
gem.add_runtime_dependency 'yajl-ruby'
|
21
|
+
|
22
|
+
gem.add_development_dependency 'cucumber'
|
23
|
+
gem.add_development_dependency 'rake'
|
24
|
+
gem.add_development_dependency 'rspec'
|
25
|
+
gem.add_development_dependency 'webmock'
|
26
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
require_relative 'importer'
|
4
|
+
require_relative 'issue_count'
|
5
|
+
require_relative 'page_request_builder'
|
6
|
+
require_relative 'page_response_processor'
|
7
|
+
|
8
|
+
module Hexabat
|
9
|
+
class Client
|
10
|
+
attr_reader :repository, :params, :callbacks
|
11
|
+
|
12
|
+
def initialize(repository, params = {})
|
13
|
+
@repository = repository
|
14
|
+
@params = params
|
15
|
+
@callbacks = {
|
16
|
+
issue_retrieved: ->(issue){},
|
17
|
+
issue_count_known: ->(issue_count){},
|
18
|
+
error: ->(repository, status, message) { STDERR.puts "#{repository} #{status} #{message}" }
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def import
|
23
|
+
if EM.reactor_running?
|
24
|
+
start_importing
|
25
|
+
else
|
26
|
+
EM.run &method(:start_importing)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def on(event_callback)
|
31
|
+
event = event_callback.keys.first
|
32
|
+
raise_unknown_event_error event unless known? event
|
33
|
+
callbacks.merge! event_callback
|
34
|
+
end
|
35
|
+
|
36
|
+
def known_events
|
37
|
+
callbacks.keys
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def start_importing
|
43
|
+
Importer.new(issue_count, request_creator).import
|
44
|
+
end
|
45
|
+
|
46
|
+
def request_creator
|
47
|
+
PageRequestBuilder.for repository, response_processor, params
|
48
|
+
end
|
49
|
+
|
50
|
+
def issue_count
|
51
|
+
IssueCount.new(&callbacks[:issue_count_known])
|
52
|
+
end
|
53
|
+
|
54
|
+
def response_processor
|
55
|
+
PageResponseProcessor.new repository, callbacks[:issue_retrieved], callbacks[:error]
|
56
|
+
end
|
57
|
+
|
58
|
+
def known? event
|
59
|
+
known_events.include? event
|
60
|
+
end
|
61
|
+
|
62
|
+
def raise_unknown_event_error event
|
63
|
+
known = known_events.join(', ')
|
64
|
+
raise ArgumentError.new "Unknown #{event} event. Events: #{known}"
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Hexabat
|
2
|
+
class Importer
|
3
|
+
def initialize(issue_counter, request_creator)
|
4
|
+
@issue_counter = issue_counter
|
5
|
+
@request_creator = request_creator
|
6
|
+
end
|
7
|
+
|
8
|
+
def import
|
9
|
+
retrieve_first_page_of_issues :open
|
10
|
+
retrieve_first_page_of_issues :closed
|
11
|
+
end
|
12
|
+
|
13
|
+
def first_page_retrieved(state)
|
14
|
+
->(page_range, issue_count) do
|
15
|
+
notify_counted_page :first, state, page_range, issue_count
|
16
|
+
retrieve_last_page_of_issues state, page_range.last if page_range.multiple_pages?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def last_page_retrieved(state)
|
21
|
+
->(page_range, issue_count) do
|
22
|
+
notify_counted_page :last, state, page_range, issue_count
|
23
|
+
retrieve_pages_of_issues state, page_range.middle
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def retrieve_first_page_of_issues(state)
|
30
|
+
@request_creator.for page: 1, state: state.to_s, &first_page_retrieved(state)
|
31
|
+
end
|
32
|
+
|
33
|
+
def retrieve_last_page_of_issues(state, page)
|
34
|
+
@request_creator.for page: page, state: state.to_s, &last_page_retrieved(state)
|
35
|
+
end
|
36
|
+
|
37
|
+
def retrieve_pages_of_issues(state, pages)
|
38
|
+
pages.each { |page| @request_creator.for page: page, state: state.to_s }
|
39
|
+
end
|
40
|
+
|
41
|
+
def notify_counted_page(first_or_last, state, page_range, issue_count)
|
42
|
+
@issue_counter.counted first_or_last, state, page_range, issue_count
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Hexabat
|
2
|
+
class IssueCount
|
3
|
+
ISSUES_PER_PAGE = 100
|
4
|
+
|
5
|
+
attr_reader :page_ranges, :issue_counts
|
6
|
+
|
7
|
+
def initialize(&issue_count_known)
|
8
|
+
@count_known = issue_count_known
|
9
|
+
@page_ranges = { open: nil, closed: nil }
|
10
|
+
@issue_counts = { open: {first: nil, last: nil}, closed: {first: nil, last: nil}}
|
11
|
+
end
|
12
|
+
|
13
|
+
def counted(page, state, page_range, issue_count)
|
14
|
+
check_done_counting do
|
15
|
+
@page_ranges[state] = page_range
|
16
|
+
@issue_counts[state][page] = issue_count
|
17
|
+
@issue_counts[state][:last] = 0 unless page_range.multiple_pages?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def check_done_counting
|
24
|
+
yield
|
25
|
+
@count_known.call(count) if all_done?
|
26
|
+
end
|
27
|
+
|
28
|
+
def all_done?
|
29
|
+
done_counting_issues? :open and done_counting_issues? :closed
|
30
|
+
end
|
31
|
+
|
32
|
+
def count
|
33
|
+
issue_count(:open) + issue_count(:closed)
|
34
|
+
end
|
35
|
+
|
36
|
+
def done_counting_issues?(state)
|
37
|
+
not @page_ranges[state].nil? and
|
38
|
+
not @issue_counts[state][:first].nil? and
|
39
|
+
not @issue_counts[state][:last].nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
def issue_count(state)
|
43
|
+
@issue_counts[state][:first] +
|
44
|
+
@issue_counts[state][:last] +
|
45
|
+
@page_ranges[state].middle_page_count * ISSUES_PER_PAGE
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Hexabat
|
4
|
+
module PageRange
|
5
|
+
def self.from(headers)
|
6
|
+
return SinglePageRange.new(1) unless headers.has_key? 'LINK'
|
7
|
+
return MultiplePageRange.new(1, LinkHeader.new(headers['LINK']).last)
|
8
|
+
end
|
9
|
+
|
10
|
+
class LinkHeader
|
11
|
+
PREV = /<https:\/\/\S+\?page=(?'prev_page'\d+)[\S]+>; rel="prev"/
|
12
|
+
LAST = /<https:\/\/\S+\?page=(?'last_page'\d+)[\S]+>; rel="last"/
|
13
|
+
|
14
|
+
def initialize(header)
|
15
|
+
@header = header
|
16
|
+
end
|
17
|
+
|
18
|
+
def last
|
19
|
+
return prev.succ if last_missing? or last_incorrect?
|
20
|
+
return header_last
|
21
|
+
end
|
22
|
+
|
23
|
+
def prev
|
24
|
+
@prev ||= extract(PREV)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def header_last
|
30
|
+
@last ||= extract(LAST)
|
31
|
+
end
|
32
|
+
|
33
|
+
def extract(page_regexp)
|
34
|
+
@header.scan(page_regexp).flatten.map(&:to_i).first
|
35
|
+
end
|
36
|
+
|
37
|
+
def last_missing?
|
38
|
+
header_last.nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
def last_incorrect?
|
42
|
+
# This check of the last page is needed because some times Github
|
43
|
+
# returns a LINK header with a wrong last page.
|
44
|
+
#
|
45
|
+
# This happens when you request the last page of a repository. When you
|
46
|
+
# do that the LINK header returns a link to the first page as the last
|
47
|
+
# page.
|
48
|
+
#
|
49
|
+
# See: https://gist.github.com/3754973
|
50
|
+
not prev.nil? and header_last < prev
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class MultiplePageRange < DelegateClass(Range)
|
56
|
+
def initialize(first, last)
|
57
|
+
super(first..last)
|
58
|
+
end
|
59
|
+
|
60
|
+
def page_count
|
61
|
+
last - first + 1
|
62
|
+
end
|
63
|
+
|
64
|
+
def middle
|
65
|
+
if page_count > 3
|
66
|
+
MultiplePageRange.new(first.succ, last.pred)
|
67
|
+
elsif page_count == 3
|
68
|
+
SinglePageRange.new(first.succ)
|
69
|
+
else
|
70
|
+
EmptyPageRange.new
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def middle_page_count
|
75
|
+
middle.page_count
|
76
|
+
end
|
77
|
+
|
78
|
+
def multiple_pages?
|
79
|
+
true
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class SinglePageRange < DelegateClass(Range)
|
84
|
+
def initialize(page)
|
85
|
+
super page..page
|
86
|
+
end
|
87
|
+
|
88
|
+
def page_count
|
89
|
+
1
|
90
|
+
end
|
91
|
+
|
92
|
+
def middle
|
93
|
+
EmptyPageRange.new
|
94
|
+
end
|
95
|
+
|
96
|
+
def middle_page_count
|
97
|
+
middle.page_count
|
98
|
+
end
|
99
|
+
|
100
|
+
def multiple_pages?
|
101
|
+
false
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class EmptyPageRange < DelegateClass(Range)
|
106
|
+
def initialize
|
107
|
+
super 0...0
|
108
|
+
end
|
109
|
+
|
110
|
+
def page_count
|
111
|
+
0
|
112
|
+
end
|
113
|
+
|
114
|
+
def middle
|
115
|
+
EmptyPageRange.new
|
116
|
+
end
|
117
|
+
|
118
|
+
def middle_page_count
|
119
|
+
middle.page_count
|
120
|
+
end
|
121
|
+
|
122
|
+
def multiple_pages?
|
123
|
+
false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'em-http-request'
|
2
|
+
require 'em-http/middleware/json_response'
|
3
|
+
require 'hexabat/page_range'
|
4
|
+
|
5
|
+
module Hexabat
|
6
|
+
module PageRequestBuilder
|
7
|
+
|
8
|
+
def self.for(repository, response_processor, params = {})
|
9
|
+
if params.has_key? :token
|
10
|
+
TokenAuthorized.new(repository, params[:token], response_processor)
|
11
|
+
elsif params.has_key? :oauth
|
12
|
+
raise 'OAuth not supported yet'
|
13
|
+
else
|
14
|
+
Unauthorized.new(repository, response_processor)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Base
|
19
|
+
MAX_PAGE_SIZE = 100
|
20
|
+
|
21
|
+
def initialize(repository, response_processor)
|
22
|
+
@repository = repository
|
23
|
+
@response_processor = response_processor
|
24
|
+
EM::HttpRequest.use(EM::Middleware::JSONResponse)
|
25
|
+
end
|
26
|
+
|
27
|
+
def for(params, &page_callback)
|
28
|
+
build_request(query_from params).callback &page_retrieved(page_callback)
|
29
|
+
end
|
30
|
+
|
31
|
+
def page_retrieved(page_callback = nil)
|
32
|
+
->(http) { @response_processor.process(http, &page_callback) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def endpoint
|
36
|
+
"https://api.github.com/repos/#{@repository}/issues"
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def build_request(query)
|
42
|
+
raise "PageRequestBuilder::Base does not know how to build requests. Use one if it's subclasses"
|
43
|
+
end
|
44
|
+
|
45
|
+
def query_from(params)
|
46
|
+
params.merge per_page: MAX_PAGE_SIZE
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Unauthorized < Base
|
51
|
+
def build_request(query)
|
52
|
+
EM::HttpRequest.new(endpoint).get query: query
|
53
|
+
end
|
54
|
+
|
55
|
+
private :build_request
|
56
|
+
end
|
57
|
+
|
58
|
+
class TokenAuthorized < Base
|
59
|
+
def initialize(repository, token, response_processor)
|
60
|
+
@token = token
|
61
|
+
super(repository, response_processor)
|
62
|
+
end
|
63
|
+
|
64
|
+
def build_request(query)
|
65
|
+
EM::HttpRequest.new(endpoint).get query: query, head: headers
|
66
|
+
end
|
67
|
+
|
68
|
+
def headers
|
69
|
+
{ 'Authorization' => "token #{@token}" }
|
70
|
+
end
|
71
|
+
|
72
|
+
private :build_request, :headers
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'hexabat/page_range'
|
3
|
+
|
4
|
+
module Hexabat
|
5
|
+
class PageResponseProcessor
|
6
|
+
def initialize(repository, issue_callback, errback)
|
7
|
+
@repository = repository
|
8
|
+
@issue_callback = issue_callback
|
9
|
+
@errback = errback
|
10
|
+
end
|
11
|
+
|
12
|
+
def process(http, &page_callback)
|
13
|
+
check_for_errors(http)
|
14
|
+
process_response(http.response_header, http.response, page_callback)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def check_for_errors(http)
|
20
|
+
status = http.response_header.status
|
21
|
+
@errback.call @repository, status, http.response['message'] if status > 200
|
22
|
+
end
|
23
|
+
|
24
|
+
def process_response(headers, issues, page_callback)
|
25
|
+
page_callback.call PageRange.from(headers), issues.count unless page_callback.nil?
|
26
|
+
notify_issue_retrieved issues
|
27
|
+
end
|
28
|
+
|
29
|
+
def notify_issue_retrieved(issues)
|
30
|
+
issues.each do |issue|
|
31
|
+
EM.next_tick { @issue_callback.call issue }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/hexabat.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'hexabat/client'
|
2
|
+
require 'hexabat/importer'
|
3
|
+
require 'hexabat/version'
|
4
|
+
|
5
|
+
module Hexabat
|
6
|
+
def self.import(repository, params = {})
|
7
|
+
Client.new(repository, params).tap do |hexabat|
|
8
|
+
hexabat.on issue_retrieved: @issue_retrieved
|
9
|
+
hexabat.on issue_count_known: @issue_count_known
|
10
|
+
hexabat.on error: @errback
|
11
|
+
hexabat.import
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.on_issue_retrieved(&callback)
|
16
|
+
@issue_retrieved = callback
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.on_issue_count_known(&callback)
|
20
|
+
@issue_count_known = callback
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.on_error(&callback)
|
24
|
+
@errback = callback
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'hexabat/client'
|
2
|
+
|
3
|
+
describe Hexabat::Client do
|
4
|
+
subject { described_class.new(repository) }
|
5
|
+
let(:repository) { 'path11/hexabat' }
|
6
|
+
let(:a_callback) { lambda {} }
|
7
|
+
|
8
|
+
it 'can be set with the issue retrieved callback' do
|
9
|
+
subject.on issue_retrieved: a_callback
|
10
|
+
subject.callbacks[:issue_retrieved].should be a_callback
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'can be set with the issue count retrieved callback' do
|
14
|
+
subject.on issue_count_known: a_callback
|
15
|
+
subject.callbacks[:issue_count_known].should be a_callback
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'raises an error if a callback for an unknown event is set' do
|
19
|
+
expect { subject.on unknown_event: a_callback}.to raise_error ArgumentError
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'starts Event Machine if it is not running' do
|
23
|
+
EM.stub(:reactor_running?).and_return(false)
|
24
|
+
EM.should_receive(:run)
|
25
|
+
subject.stub(:start_importing)
|
26
|
+
subject.import
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'does not start Event Machine if it is already running' do
|
30
|
+
EM.stub(:reactor_running?).and_return(true)
|
31
|
+
EM.should_not_receive(:run)
|
32
|
+
subject.stub(:start_importing)
|
33
|
+
subject.import
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'imports all the issues from the repository' do
|
37
|
+
importer = mock(:importer)
|
38
|
+
EM.stub(:run).and_yield
|
39
|
+
Hexabat::Importer.stub(:new).and_return(importer)
|
40
|
+
importer.should_receive(:import)
|
41
|
+
subject.on issue_retrieved: :callback1
|
42
|
+
subject.on issue_count_known: :callback2
|
43
|
+
subject.import
|
44
|
+
end
|
45
|
+
end
|