ghtorrent 0.8 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG +9 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +32 -20
- data/README.md +50 -34
- data/Rakefile +3 -3
- data/bin/ght-retrieve-dependents +6 -0
- data/bin/ght-retrieve-repos +6 -0
- data/lib/ghtorrent.rb +3 -0
- data/lib/ghtorrent/adapters/mongo_persister.rb +3 -3
- data/lib/ghtorrent/api_client.rb +6 -4
- data/lib/ghtorrent/command.rb +2 -28
- data/lib/ghtorrent/commands/ght_data_retrieval.rb +3 -2
- data/lib/ghtorrent/commands/ght_load.rb +7 -5
- data/lib/ghtorrent/commands/ght_retrieve_dependents.rb +84 -0
- data/lib/ghtorrent/commands/ght_retrieve_repo.rb +1 -70
- data/lib/ghtorrent/commands/ght_retrieve_repos.rb +206 -0
- data/lib/ghtorrent/commands/ght_retrieve_user.rb +9 -2
- data/lib/ghtorrent/ghtorrent.rb +103 -82
- data/lib/ghtorrent/logging.rb +2 -1
- data/lib/ghtorrent/migrations/015_fix_table_issue_labels.rb +17 -5
- data/lib/ghtorrent/migrations/016_add_actor_pull_request_history.rb +1 -1
- data/lib/ghtorrent/retriever.rb +8 -17
- data/lib/ghtorrent/settings.rb +9 -5
- data/lib/ghtorrent/transacted_ghtorrent.rb +91 -0
- data/lib/version.rb +1 -1
- data/spec/api_client_spec.rb +42 -0
- data/spec/spec_helper.rb +21 -0
- metadata +46 -52
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 964930d19ab5d35f6c70cafe72c8abf0e682ac87
|
4
|
+
data.tar.gz: 7bda9c3d934ab7c3d292587991dedc3f6e762cbc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 591e92ae026990ba6a2f0dddfc32b47df8983b829d1b2d675f6fe9fde977fc063cce322649e7c3b05398bc891568c6ab11117c253a4f19ff7edc6a9aa1045129
|
7
|
+
data.tar.gz: f4ba04030bdec8a390dc800e676fbb1dfd2c6f0efc26ce4696f8b2e14a0ed3c1a7975f98c1e670fbaa89c8e3b5e0c8e96d171e88cf9a70dea6b99a71a28d351b
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
= Version 0.8.1
|
2
|
+
* New tool to retrieve specific entities and their dependencies
|
3
|
+
* New tool to retrieve repositories en masse
|
4
|
+
* Support for resuming when exception occurs while processing items in loops
|
5
|
+
* Support for finer grained transactions when processing large entities
|
6
|
+
* Commit comments are now indexed per owner/repo (was just by comment id)
|
7
|
+
* Remove the unused daemon mode
|
8
|
+
* Various exception fixes and more detailed logging
|
9
|
+
|
1
10
|
= Version 0.8
|
2
11
|
* Retrieve and process issue labels
|
3
12
|
* Retrive and process actors for pull request events
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,34 +1,44 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
ghtorrent (0.
|
5
|
-
amqp (~> 1.
|
6
|
-
bson_ext (~> 1.
|
7
|
-
|
8
|
-
|
9
|
-
sequel (~> 3.47)
|
4
|
+
ghtorrent (0.8.1)
|
5
|
+
amqp (~> 1.1.0)
|
6
|
+
bson_ext (~> 1.9.0)
|
7
|
+
mongo (~> 1.9.0)
|
8
|
+
sequel (~> 4.5.0)
|
10
9
|
trollop (~> 2.0.0)
|
11
10
|
|
12
11
|
GEM
|
13
12
|
remote: https://rubygems.org/
|
14
13
|
specs:
|
15
|
-
|
16
|
-
|
14
|
+
addressable (2.3.5)
|
15
|
+
amq-protocol (1.9.0)
|
16
|
+
amqp (1.1.7)
|
17
|
+
amq-protocol (>= 1.9.0)
|
17
18
|
eventmachine
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
bson_ext (1.8.5)
|
25
|
-
bson (~> 1.8.5)
|
26
|
-
daemons (1.1.9)
|
19
|
+
bson (1.9.2)
|
20
|
+
bson_ext (1.9.2)
|
21
|
+
bson (~> 1.9.2)
|
22
|
+
crack (0.4.1)
|
23
|
+
safe_yaml (~> 0.9.0)
|
24
|
+
diff-lcs (1.2.5)
|
27
25
|
eventmachine (1.0.3)
|
28
|
-
mongo (1.
|
29
|
-
bson (~> 1.
|
30
|
-
|
26
|
+
mongo (1.9.2)
|
27
|
+
bson (~> 1.9.2)
|
28
|
+
rspec (2.14.1)
|
29
|
+
rspec-core (~> 2.14.0)
|
30
|
+
rspec-expectations (~> 2.14.0)
|
31
|
+
rspec-mocks (~> 2.14.0)
|
32
|
+
rspec-core (2.14.7)
|
33
|
+
rspec-expectations (2.14.4)
|
34
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
35
|
+
rspec-mocks (2.14.4)
|
36
|
+
safe_yaml (0.9.7)
|
37
|
+
sequel (4.5.0)
|
31
38
|
trollop (2.0)
|
39
|
+
webmock (1.16.0)
|
40
|
+
addressable (>= 2.2.7)
|
41
|
+
crack (>= 0.3.2)
|
32
42
|
|
33
43
|
PLATFORMS
|
34
44
|
ruby
|
@@ -36,3 +46,5 @@ PLATFORMS
|
|
36
46
|
DEPENDENCIES
|
37
47
|
ghtorrent!
|
38
48
|
jdbc-mysql
|
49
|
+
rspec (~> 2.14.0)
|
50
|
+
webmock (~> 1.16)
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
# ghtorrent: Mirror and process data from the Github API
|
2
2
|
|
3
3
|
A library and a collection of scripts used to retrieve data from the Github API
|
4
4
|
and extract metadata in an SQL database, in a modular and scalable manner. The
|
@@ -12,8 +12,10 @@ GHTorrent can be used for a variety of purposes, such as:
|
|
12
12
|
* Create a queriable metadata index for a specific repository
|
13
13
|
* Query the Github API using intelligent caching to avoid duplicate queries
|
14
14
|
|
15
|
-
|
16
|
-
|
15
|
+
|
16
|
+
## Components
|
17
|
+
|
18
|
+
GHTorrents components (which can be used individually) are:
|
17
19
|
|
18
20
|
* [APIClient](https://github.com/gousiosg/github-mirror/blob/master/lib/ghtorrent/api_client.rb): Knows how to query the Github API (both single entities and
|
19
21
|
pages) and respect the API request limit. Can be configured to override the
|
@@ -26,56 +28,65 @@ store must support arbitrary queries to the stored JSON objects.
|
|
26
28
|
* [GHTorrent](https://github.com/gousiosg/github-mirror/blob/master/lib/ghtorrent/ghtorrent.rb): Knows how to extract information from the data retrieved by
|
27
29
|
the retriever in order to update an SQL database (see [schema](http://ghtorrent.org/relational.html)) with metadata.
|
28
30
|
|
31
|
+
### Component Configuration
|
32
|
+
|
29
33
|
The Persister and GHTorrent components have configurable back ends:
|
30
34
|
|
31
|
-
* Persister
|
32
|
-
* GHTorrent
|
35
|
+
* **Persister:** Either uses MongoDB > 2.0 (`mongo` driver) or no persister (`noop` driver)
|
36
|
+
* **GHTorrent:** GHTorrent is tested mainly with MySQL, but can theoretically be
|
33
37
|
used with any SQL database compatible with [Sequel](http://sequel.rubyforge.org/rdoc/files/doc/opening_databases_rdoc.html). Your milaege may vary.
|
34
38
|
|
35
|
-
|
36
39
|
The distributed mirroring scripts also require RabbitMQ >= 2.8 or other
|
37
40
|
|
38
|
-
#### Installing
|
39
41
|
|
42
|
+
## Installation
|
43
|
+
|
44
|
+
|
45
|
+
### 1. Install GHTorrent
|
40
46
|
GHTorrent is written in Ruby (tested with 1.9). To install it as a Gem do:
|
41
47
|
|
42
48
|
<code>
|
43
49
|
sudo gem install ghtorrent
|
44
50
|
</code>
|
45
51
|
|
52
|
+
|
53
|
+
### 2. Install Your Preferred Database
|
54
|
+
|
46
55
|
Depending on which SQL database you want to use, install the appropriate
|
47
56
|
dependency gem.
|
48
57
|
|
49
58
|
<code>
|
50
|
-
sudo gem install mysql2 #or sqlite3-ruby
|
59
|
+
sudo gem install mysql2 # or <sqlite3-ruby|postgres>
|
51
60
|
</code>
|
52
61
|
|
53
|
-
#### Configuring
|
54
62
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
63
|
+
## Configuration
|
64
|
+
|
65
|
+
Copy [config.yaml.tmpl](https://github.com/gousiosg/github-mirror/blob/master/config.yaml.tmpl)
|
66
|
+
to a file in your home directory.
|
67
|
+
|
68
|
+
All provided scripts accept the `-c` option, which accepts the location of the configuration file as
|
59
69
|
a parameter.
|
60
70
|
|
61
71
|
You can find more information of how you can setup a mirroring cluster of machines
|
62
72
|
to retrieve data in parallel on the [Wiki](https://github.com/gousiosg/github-mirror/wiki/Setting-up-a-mirroring-cluster).
|
63
73
|
|
64
|
-
|
74
|
+
|
75
|
+
## Using GHTorrent
|
65
76
|
|
66
77
|
To mirror the event stream and capture all data:
|
67
78
|
|
68
79
|
* `ght-mirror-events.rb` periodically polls Github's event
|
69
80
|
queue (`https://api.github.com/events`), stores all new events in the
|
70
|
-
configured pestister and posts them to the `github` exchange in
|
81
|
+
configured pestister, and posts them to the `github` exchange in
|
71
82
|
RabbitMQ.
|
72
83
|
|
73
84
|
* `ght-data_retrieval.rb` creates queues that route posted events to processor
|
74
|
-
functions
|
75
|
-
linked contents, extract metadata
|
85
|
+
functions. The functions use the appropriate Github API call to retrieve the
|
86
|
+
linked contents, extract metadata (for database storage), and store the
|
76
87
|
retrieved data in the appropriate collection in the persister, to avoid
|
77
|
-
duplicate API
|
78
|
-
|
88
|
+
duplicate API calls.
|
89
|
+
Data in the SQL database contain pointers (the `ext_ref_id` field) to the
|
79
90
|
"raw" data in the persister.
|
80
91
|
|
81
92
|
To retrieve data for a repository or user:
|
@@ -89,27 +100,31 @@ To perform maintenance:
|
|
89
100
|
the `ght-data-retrieval` script to reprocess them
|
90
101
|
* `ght-get-more-commits` retrieves all commits for a specific repository
|
91
102
|
|
92
|
-
|
103
|
+
|
104
|
+
### Data Torrents
|
93
105
|
|
94
106
|
You can find torrents for retrieving data on the
|
95
|
-
[Available Torrents](https://ghtorrent.org/downloads.html) page.
|
107
|
+
[Available Torrents](https://ghtorrent.org/downloads.html) page.
|
96
108
|
|
97
|
-
|
109
|
+
There are two sets of data:
|
110
|
+
|
111
|
+
* **Raw events:** Github's [event stream](https://api.github.com/events). These
|
98
112
|
are the roots for mirroring operations. The `ght-data-retrieval` crawler starts
|
99
113
|
from an event and goes deep into the rabbit hole.
|
100
|
-
* SQL dumps+Linked data
|
114
|
+
* **SQL dumps + Linked data:** Data dumps from the SQL database and the corresponding
|
101
115
|
MongoDB entities.
|
102
116
|
|
103
|
-
#### Reporting bugs
|
104
117
|
|
105
|
-
|
106
|
-
|
107
|
-
|
118
|
+
## Bugs & Feature Requests
|
119
|
+
|
120
|
+
Please tell us about features you'd like or bugs you've discovered on our
|
121
|
+
[Issue Tracker](https://github.com/gousiosg/github-mirror/issues).
|
108
122
|
|
109
|
-
Patches, bug fixes etc are welcome. Please fork the repository and create
|
123
|
+
Patches, bug fixes, etc are welcome. Please fork the repository and create
|
110
124
|
a pull request when done fixing/implementing the new feature.
|
111
125
|
|
112
|
-
|
126
|
+
|
127
|
+
## Citing GHTorrent in your Research
|
113
128
|
|
114
129
|
If you find GHTorrent and the accompanying datasets useful in your research,
|
115
130
|
please consider citing the following paper:
|
@@ -118,16 +133,17 @@ please consider citing the following paper:
|
|
118
133
|
|
119
134
|
See also the following presentation:
|
120
135
|
|
121
|
-
<iframe src="http://www.slideshare.net/slideshow/embed_code/13184524?rel=0" width="342" height="291" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC;border-width:1px 1px 0;margin-bottom:5px" allowfullscreen/>
|
136
|
+
<iframe src="http://www.slideshare.net/slideshow/embed_code/13184524?rel=0" width="342" height="291" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC;border-width:1px 1px 0;margin-bottom:5px" allowfullscreen />
|
122
137
|
<div style="margin-bottom:5px"> <strong> <a href="http://www.slideshare.net/gousiosg/ghtorrent-githubs-data-from-a-firehose-13184524" title="GHTorrent: Github's Data from a Firehose" target="_blank">GHTorrent: Github's Data from a Firehose</a> </strong> </div>
|
123
138
|
|
124
|
-
#### Authors
|
125
139
|
|
126
|
-
|
140
|
+
## Authors
|
141
|
+
|
142
|
+
* [Georgios Gousios](http://istlab.dmst.aueb.gr/~george) <gousiosg@gmail.com>
|
143
|
+
* [Diomidis Spinellis](http://www.dmst.aueb.gr/dds) <dds@aueb.gr>
|
127
144
|
|
128
|
-
[Diomidis Spinellis](http://www.dmst.aueb.gr/dds) <dds@aueb.gr>
|
129
145
|
|
130
|
-
|
146
|
+
## License
|
131
147
|
|
132
148
|
[2-clause BSD](http://www.opensource.org/licenses/bsd-license.php)
|
133
149
|
|
data/Rakefile
CHANGED
@@ -2,11 +2,11 @@ require 'rake'
|
|
2
2
|
require 'rake/testtask'
|
3
3
|
require 'rake/rdoctask'
|
4
4
|
|
5
|
-
task :default => [:
|
5
|
+
task :default => [:spec, :rdoc]
|
6
6
|
|
7
7
|
desc "Run basic tests"
|
8
|
-
Rake::TestTask.new(:
|
9
|
-
t.pattern = '
|
8
|
+
Rake::TestTask.new(:spec) do |t|
|
9
|
+
t.pattern = 'spec/*_test.rb'
|
10
10
|
t.verbose = true
|
11
11
|
t.warning = true
|
12
12
|
end
|
data/lib/ghtorrent.rb
CHANGED
@@ -48,6 +48,7 @@ require 'ghtorrent/retriever'
|
|
48
48
|
|
49
49
|
# SQL database fillup methods
|
50
50
|
require 'ghtorrent/ghtorrent'
|
51
|
+
require 'ghtorrent/transacted_ghtorrent'
|
51
52
|
|
52
53
|
# Commands
|
53
54
|
require 'ghtorrent/commands/ght_data_retrieval'
|
@@ -57,5 +58,7 @@ require 'ghtorrent/commands/ght_rm_dupl'
|
|
57
58
|
require 'ghtorrent/commands/ght_load'
|
58
59
|
require 'ghtorrent/commands/ght_retrieve_repo'
|
59
60
|
require 'ghtorrent/commands/ght_retrieve_user'
|
61
|
+
require 'ghtorrent/commands/ght_retrieve_dependents'
|
62
|
+
require 'ghtorrent/commands/ght_retrieve_repos'
|
60
63
|
|
61
64
|
# vim: set sta sts=2 shiftwidth=2 sw=2 et ai :
|
@@ -24,7 +24,7 @@ module GHTorrent
|
|
24
24
|
:events => %w(id),
|
25
25
|
:users => %w(login),
|
26
26
|
:commits => %w(sha),
|
27
|
-
:commit_comments => %w(
|
27
|
+
:commit_comments => %w(commit_id id),
|
28
28
|
:repos => %w(name owner.login),
|
29
29
|
:repo_labels => %w(repo owner),
|
30
30
|
:repo_collaborators => %w(repo owner login),
|
@@ -184,9 +184,9 @@ module GHTorrent
|
|
184
184
|
idx_fields = v.reduce({}){|acc, x| acc.merge({x => 1})}
|
185
185
|
if exists.nil?
|
186
186
|
col.create_index(idx_fields, :background => true)
|
187
|
-
STDERR.puts "Creating index on #{
|
187
|
+
STDERR.puts "Creating index on #{col}(#{v})"
|
188
188
|
else
|
189
|
-
STDERR.puts "Index on #{
|
189
|
+
STDERR.puts "Index on #{col}(#{v}) exists"
|
190
190
|
end
|
191
191
|
|
192
192
|
end
|
data/lib/ghtorrent/api_client.rb
CHANGED
@@ -169,7 +169,7 @@ module GHTorrent
|
|
169
169
|
end
|
170
170
|
|
171
171
|
total = Time.now.to_ms - start_time.to_ms
|
172
|
-
debug "APIClient: Request: #{url} #{if from_cache then "from cache," else "(#{@remaining} remaining)," end} Total: #{total} ms"
|
172
|
+
debug "APIClient[#{@attach_ip}]: Request: #{url} #{if from_cache then "from cache," else "(#{@remaining} remaining)," end} Total: #{total} ms"
|
173
173
|
|
174
174
|
contents
|
175
175
|
rescue OpenURI::HTTPError => e
|
@@ -190,9 +190,9 @@ module GHTorrent
|
|
190
190
|
end
|
191
191
|
ensure
|
192
192
|
if not from_cache and config(:respect_api_ratelimit) and @remaining < 10
|
193
|
-
|
194
|
-
debug "APIClient: Request limit reached, sleeping for #{
|
195
|
-
sleep(
|
193
|
+
to_sleep = @reset - Time.now.to_i + 2
|
194
|
+
debug "APIClient: Request limit reached, sleeping for #{to_sleep} secs"
|
195
|
+
sleep(to_sleep)
|
196
196
|
end
|
197
197
|
end
|
198
198
|
end
|
@@ -202,6 +202,8 @@ module GHTorrent
|
|
202
202
|
@username ||= config(:github_username)
|
203
203
|
@passwd ||= config(:github_passwd)
|
204
204
|
@user_agent ||= config(:user_agent)
|
205
|
+
@remaining ||= 10
|
206
|
+
@reset ||= Time.now.to_i + 3600
|
205
207
|
|
206
208
|
open_func ||= if @username.nil?
|
207
209
|
lambda {|url| open(url, 'User-Agent' => @user_agent)}
|
data/lib/ghtorrent/command.rb
CHANGED
@@ -54,29 +54,6 @@ module GHTorrent
|
|
54
54
|
command.options[:password])
|
55
55
|
end
|
56
56
|
|
57
|
-
if command.options[:daemon]
|
58
|
-
if Process.uid == 0
|
59
|
-
# Daemonize as a proper system daemon
|
60
|
-
Daemons.daemonize(:app_name => File.basename($0),
|
61
|
-
:dir_mode => :system,
|
62
|
-
:log_dir => "/var/log",
|
63
|
-
:backtrace => true,
|
64
|
-
:log_output => true)
|
65
|
-
STDERR.puts "Became a daemon"
|
66
|
-
# Change effective user id for the process
|
67
|
-
unless command.options[:user].nil?
|
68
|
-
Process.euid = Etc.getpwnam(command.options[:user]).uid
|
69
|
-
end
|
70
|
-
else
|
71
|
-
# Daemonize, but output in current directory
|
72
|
-
Daemons.daemonize(:app_name => File.basename($0),
|
73
|
-
:dir_mode => :normal,
|
74
|
-
:dir => Dir.getwd,
|
75
|
-
:backtrace => true,
|
76
|
-
:log_output => true)
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
57
|
begin
|
81
58
|
command.go
|
82
59
|
rescue => e
|
@@ -107,10 +84,7 @@ Standard options:
|
|
107
84
|
opt :verbose, 'verbose mode', :short => 'v'
|
108
85
|
opt :addr, 'ip address to use for performing requests', :short => 'a',
|
109
86
|
:type => String
|
110
|
-
opt :
|
111
|
-
opt :user, 'run as the specified user (only when started as root)',
|
112
|
-
:short => 'u', :type => String
|
113
|
-
opt :username, 'Username at Github', :type => String
|
87
|
+
opt :username, 'Username at Github', :short => 's', :type => String
|
114
88
|
opt :password, 'Password at Github', :type => String
|
115
89
|
end
|
116
90
|
end
|
@@ -163,7 +137,7 @@ Standard options:
|
|
163
137
|
|
164
138
|
def override_config(config_file, setting, new_value)
|
165
139
|
puts "Overriding configuration #{setting}=#{config(setting)} with cmd line #{new_value}"
|
166
|
-
merge_config_values({setting => new_value})
|
140
|
+
merge_config_values(config_file, {setting => new_value})
|
167
141
|
end
|
168
142
|
|
169
143
|
private
|
@@ -58,11 +58,12 @@ class GHTDataRetrieval < GHTorrent::Command
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def CommitCommentEvent(data)
|
61
|
-
user = data['
|
61
|
+
user = data['repo']['name'].split(/\//)[0]
|
62
62
|
repo = data['repo']['name'].split(/\//)[1]
|
63
63
|
id = data['payload']['comment']['id']
|
64
|
+
sha = data['payload']['comment']['commit_id']
|
64
65
|
|
65
|
-
ghtorrent.get_commit_comment(user, repo, id)
|
66
|
+
ghtorrent.get_commit_comment(user, repo, sha, id)
|
66
67
|
end
|
67
68
|
|
68
69
|
def PullRequestEvent(data)
|
@@ -60,7 +60,7 @@ Loads object ids from a collection to a queue for further processing.
|
|
60
60
|
|
61
61
|
def go
|
62
62
|
# Num events read
|
63
|
-
|
63
|
+
total_read = 0
|
64
64
|
|
65
65
|
puts "Loading items after #{Time.at(options[:earliest])}" if options[:verbose]
|
66
66
|
puts "Loading items before #{Time.at(options[:latest])}" if options[:verbose]
|
@@ -107,9 +107,10 @@ Loads object ids from a collection to a queue for further processing.
|
|
107
107
|
|
108
108
|
# Read next options[:batch] items and queue them
|
109
109
|
read_and_publish = Proc.new {
|
110
|
-
|
110
|
+
num_read = 0
|
111
111
|
persister.get_underlying_connection[:events].find(what.merge(from),
|
112
|
-
:
|
112
|
+
:snapshot => true,
|
113
|
+
:skip => total_read,
|
113
114
|
:limit => options[:batch]).each do |e|
|
114
115
|
unq = read_value(e, 'type')
|
115
116
|
if unq.class != String or unq.nil? then
|
@@ -120,10 +121,11 @@ Loads object ids from a collection to a queue for further processing.
|
|
120
121
|
:routing_key => "evt.#{e['type']}"
|
121
122
|
|
122
123
|
num_read += 1
|
123
|
-
|
124
|
+
total_read += 1
|
125
|
+
puts "Publish id = #{e['id']} (#{num_read} read, #{total_read} total)" if options.verbose
|
124
126
|
end
|
125
127
|
|
126
|
-
if
|
128
|
+
if total_read >= options[:number] or num_read == 0
|
127
129
|
puts 'Finished reading, exiting'
|
128
130
|
show_stopper.call
|
129
131
|
else
|