sup 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sup might be problematic. Click here for more details.
- data/History.txt +4 -0
- data/Manifest.txt +1 -0
- data/Rakefile +1 -1
- data/bin/sup +3 -4
- data/bin/sup-import +2 -3
- data/doc/UserGuide.txt +54 -0
- data/lib/sup.rb +1 -1
- data/lib/sup/buffer.rb +26 -12
- data/lib/sup/imap.rb +12 -2
- data/lib/sup/mbox/loader.rb +1 -0
- data/lib/sup/mbox/ssh-file.rb +33 -31
- data/lib/sup/modes/contact-list-mode.rb +1 -1
- data/lib/sup/modes/inbox-mode.rb +6 -4
- data/lib/sup/modes/label-list-mode.rb +1 -1
- data/lib/sup/modes/label-search-results-mode.rb +3 -1
- data/lib/sup/modes/person-search-results-mode.rb +3 -1
- data/lib/sup/modes/search-results-mode.rb +3 -1
- data/lib/sup/modes/thread-index-mode.rb +1 -0
- data/lib/sup/source.rb +1 -1
- metadata +12 -2
data/History.txt
CHANGED
data/Manifest.txt
CHANGED
data/Rakefile
CHANGED
@@ -12,7 +12,7 @@ Hoe.new('sup', Redwood::VERSION) do |p|
|
|
12
12
|
p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[2].gsub(/^\s+/, "")
|
13
13
|
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
14
14
|
p.email = "wmorgan-sup@masanjin.net"
|
15
|
-
p.extra_deps = [['ferret', '>= 0.10.13'], ['ncurses', '>= 0.9.1'], ['rmail', '>= 0.17'], 'highline']
|
15
|
+
p.extra_deps = [['ferret', '>= 0.10.13'], ['ncurses', '>= 0.9.1'], ['rmail', '>= 0.17'], 'highline', 'net-ssh']
|
16
16
|
end
|
17
17
|
|
18
18
|
rule 'ss?.png' => 'ss?-small.png' do |t|
|
data/bin/sup
CHANGED
@@ -100,9 +100,8 @@ begin
|
|
100
100
|
Logger.make_buf
|
101
101
|
|
102
102
|
bm.draw_screen
|
103
|
-
imode.load_more_threads ibuf.content_height
|
103
|
+
imode.load_more_threads :num => ibuf.content_height, :when_done => lambda { reporting_thread { sleep 1; PollManager.poll } }
|
104
104
|
|
105
|
-
reporting_thread { sleep 5; PollManager.poll }
|
106
105
|
PollManager.start_thread
|
107
106
|
|
108
107
|
until $exception
|
@@ -141,7 +140,7 @@ begin
|
|
141
140
|
log "built query from #{text.inspect}: #{qobj}"
|
142
141
|
mode = SearchResultsMode.new qobj
|
143
142
|
bm.spawn "search: \"#{short_text}\"", mode
|
144
|
-
mode.load_more_threads mode.buffer.content_height
|
143
|
+
mode.load_more_threads :num => mode.buffer.content_height
|
145
144
|
rescue Ferret::QueryParser::QueryParseException => e
|
146
145
|
bm.flash "Couldn't parse query."
|
147
146
|
end
|
@@ -168,7 +167,7 @@ begin
|
|
168
167
|
b = BufferManager.spawn_unless_exists(:draft) do
|
169
168
|
mode = LabelSearchResultsMode.new [:draft]
|
170
169
|
end
|
171
|
-
b.mode.load_more_threads b.content_height
|
170
|
+
b.mode.load_more_threads :num => b.content_height
|
172
171
|
end
|
173
172
|
when :nothing
|
174
173
|
when :redraw
|
data/bin/sup-import
CHANGED
@@ -76,7 +76,7 @@ def get_login_info uri, sources
|
|
76
76
|
|
77
77
|
username, password = nil, nil
|
78
78
|
unless accounts.empty?
|
79
|
-
say "Would you like to use the same account as for a previous source?"
|
79
|
+
say "Would you like to use the same account as for a previous source for #{uri}?"
|
80
80
|
choose do |menu|
|
81
81
|
accounts.each do |host, olduser, oldpw|
|
82
82
|
menu.choice("Use the account info for #{olduser}@#{host}") { username, password = olduser, oldpw }
|
@@ -95,7 +95,6 @@ def get_login_info uri, sources
|
|
95
95
|
[username, password]
|
96
96
|
end
|
97
97
|
|
98
|
-
|
99
98
|
educate_user if ARGV.member? '--help'
|
100
99
|
|
101
100
|
archive = ARGV.delete "--archive"
|
@@ -180,7 +179,7 @@ begin
|
|
180
179
|
end
|
181
180
|
|
182
181
|
m.remove_label :unread if m.status == "RO" unless force_read
|
183
|
-
puts "# message at #{offset}, labels: #{labels * ', '}"
|
182
|
+
puts "# message at #{offset}, labels: #{labels * ', '}" unless rebuild
|
184
183
|
if (rebuild || force_rebuild) &&
|
185
184
|
(docid, entry = index.load_entry_for_id(m.id)) && entry
|
186
185
|
if force_rebuild || entry[:source_info].to_i != offset
|
data/doc/UserGuide.txt
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
Welcome to Sup! Here's how to actually use it.
|
2
|
+
|
3
|
+
To get started, just run 'sup'. Assuming this is your first time,
|
4
|
+
you'll be confronted with a mostly blank screen, and a little message
|
5
|
+
at the bottom telling you that you have no messages. That's because
|
6
|
+
Sup doesn't have any messages in its index. In order to add messages,
|
7
|
+
Sup needs to have some "sources" from which to load messages.
|
8
|
+
|
9
|
+
If you want to play around a little at this point, you can press 'b'
|
10
|
+
to cycle between buffers and 'x' to kill a buffer. There's probably
|
11
|
+
not too much interesting there, but there's a log buffer with some
|
12
|
+
cryptic messages. You can also press '?' at any point to get a list of
|
13
|
+
keyboard commands, but in the absense of any email, these will be
|
14
|
+
pretty useless. When you're done, press 'q' to quit.
|
15
|
+
|
16
|
+
Now let's add a source to Sup. Run sup-import with a URI pointing to
|
17
|
+
an email source. The URI should be of the form:
|
18
|
+
- mbox://path/to/a/filename, for an mbox file on disk. (You can also
|
19
|
+
just provide the filename).
|
20
|
+
- imap://imap.server/folder or imaps://secure.imap.server/folder for
|
21
|
+
an IMAP server.
|
22
|
+
- mbox+ssh://remote.machine/path/to/a/filename for a remote mbox file.
|
23
|
+
|
24
|
+
Note: sup-import tries to be smart about setting the labels on
|
25
|
+
messages that it adds (including the special labels "unread" and
|
26
|
+
"inbox"). There are options that control this behavior, and it's
|
27
|
+
worth taking a look at the output of sup-import --help to make
|
28
|
+
sure that you don't end up with 1000 new messages that you've
|
29
|
+
actually already read.
|
30
|
+
|
31
|
+
If sup-import requires a username and password for the source, it will
|
32
|
+
prompt you for one. Either way, it will start loading messages from
|
33
|
+
that source into the index. Depending on the size of the source, this
|
34
|
+
may take some time. Don't worry! This is a one-time step, and all the
|
35
|
+
computation done now makes operating on the index faster.
|
36
|
+
|
37
|
+
Now, before we run 'sup' again, take a moment to edit your
|
38
|
+
~/.sup/config.yaml file. Replace "Your Name Here" with your name,
|
39
|
+
"your.email.here@domain.tld" with your email address, and fill in your
|
40
|
+
.signature file if you choose. You can also set the default editor.
|
41
|
+
|
42
|
+
Now run 'sup'. You should messages being loaded into your "inbox!"
|
43
|
+
There are two things that are worth understanding at this point.
|
44
|
+
First, Sup does not actually use folders. Instead, messages can have
|
45
|
+
any number of labels applied to them. Rather than viewing a folder,
|
46
|
+
you view the results of a search. So your inbox is simply the set of
|
47
|
+
messages that have the 'inbox' label applied to them.
|
48
|
+
|
49
|
+
Second, Sup groups together messages into threads. You rarely operate
|
50
|
+
on an individual message in Sup.
|
51
|
+
|
52
|
+
Press enter to view a thread.
|
53
|
+
|
54
|
+
To be continued...
|
data/lib/sup.rb
CHANGED
data/lib/sup/buffer.rb
CHANGED
@@ -136,6 +136,7 @@ class BufferManager
|
|
136
136
|
@focus_buf = nil
|
137
137
|
@dirty = true
|
138
138
|
@minibuf_stack = []
|
139
|
+
@minibuf_mutex = Mutex.new
|
139
140
|
@textfields = {}
|
140
141
|
@flash = nil
|
141
142
|
@shelled = false
|
@@ -366,12 +367,19 @@ class BufferManager
|
|
366
367
|
end
|
367
368
|
end
|
368
369
|
|
369
|
-
def minibuf_lines
|
370
|
+
def minibuf_lines
|
371
|
+
@minibuf_mutex.synchronize do
|
372
|
+
[(@flash ? 1 : 0) + @minibuf_stack.compact.size, 1].max
|
373
|
+
end
|
374
|
+
end
|
370
375
|
|
371
376
|
def draw_minibuf opts={}
|
372
|
-
m =
|
373
|
-
|
374
|
-
|
377
|
+
m = nil
|
378
|
+
@minibuf_mutex.synchronize do
|
379
|
+
m = @minibuf_stack.compact
|
380
|
+
m << @flash if @flash
|
381
|
+
m << "" if m.empty?
|
382
|
+
end
|
375
383
|
|
376
384
|
Ncurses.mutex.lock unless opts[:sync] == false
|
377
385
|
Ncurses.attrset Colormap.color_for(:none)
|
@@ -383,9 +391,13 @@ class BufferManager
|
|
383
391
|
end
|
384
392
|
|
385
393
|
def say s, id=nil
|
386
|
-
new_id =
|
387
|
-
|
388
|
-
|
394
|
+
new_id = nil
|
395
|
+
@minibuf_mutex.synchronize do
|
396
|
+
new_id = id.nil?
|
397
|
+
id ||= @minibuf_stack.length
|
398
|
+
@minibuf_stack[id] = s
|
399
|
+
end
|
400
|
+
|
389
401
|
if new_id
|
390
402
|
draw_screen :refresh => true
|
391
403
|
else
|
@@ -412,11 +424,13 @@ class BufferManager
|
|
412
424
|
## a little tricky because we can't just delete_at id because ids
|
413
425
|
## are relative (they're positions into the array).
|
414
426
|
def clear id
|
415
|
-
@
|
416
|
-
|
417
|
-
id.
|
418
|
-
|
419
|
-
|
427
|
+
@minibuf_mutex.synchronize do
|
428
|
+
@minibuf_stack[id] = nil
|
429
|
+
if id == @minibuf_stack.length - 1
|
430
|
+
id.downto(0) do |i|
|
431
|
+
break if @minibuf_stack[i]
|
432
|
+
@minibuf_stack.delete_at i
|
433
|
+
end
|
420
434
|
end
|
421
435
|
end
|
422
436
|
|
data/lib/sup/imap.rb
CHANGED
@@ -48,7 +48,7 @@ class IMAP < Source
|
|
48
48
|
@ids = []
|
49
49
|
@labels = [:unread]
|
50
50
|
@labels << :inbox unless archived?
|
51
|
-
@labels << mailbox.intern unless mailbox =~ /inbox/i
|
51
|
+
@labels << mailbox.intern unless mailbox =~ /inbox/i
|
52
52
|
@mutex = Mutex.new
|
53
53
|
end
|
54
54
|
|
@@ -122,7 +122,10 @@ class IMAP < Source
|
|
122
122
|
|
123
123
|
def host; @parsed_uri.host; end
|
124
124
|
def port; @parsed_uri.port || (ssl? ? 993 : 143); end
|
125
|
-
def mailbox
|
125
|
+
def mailbox
|
126
|
+
x = @parsed_uri.path[1..-1]
|
127
|
+
x.nil? || x.empty? ? 'INBOX' : x
|
128
|
+
end
|
126
129
|
def ssl?; @parsed_uri.scheme == 'imaps' end
|
127
130
|
|
128
131
|
def load_header id
|
@@ -149,6 +152,7 @@ class IMAP < Source
|
|
149
152
|
end
|
150
153
|
|
151
154
|
def get_imap_field id, field
|
155
|
+
retries = 0
|
152
156
|
f = nil
|
153
157
|
imap_id = @imap_ids[id] or raise SourceError, "Unknown message id #{id}. It is likely that messages have been deleted from this IMAP mailbox."
|
154
158
|
begin
|
@@ -157,6 +161,12 @@ class IMAP < Source
|
|
157
161
|
raise SourceError, "IMAP message mismatch: requested #{id}, got #{got_id}. It is likely the IMAP mailbox has been modified." unless got_id == id
|
158
162
|
rescue Net::IMAP::Error => e
|
159
163
|
raise SourceError, e.message
|
164
|
+
rescue Errno::EPIPE
|
165
|
+
if (retries += 1) <= 3
|
166
|
+
@imap = nil
|
167
|
+
connect
|
168
|
+
retry
|
169
|
+
end
|
160
170
|
end
|
161
171
|
raise SourceError, "null IMAP field '#{field}' for message with id #{id} imap id #{imap_id}" if f.nil?
|
162
172
|
|
data/lib/sup/mbox/loader.rb
CHANGED
@@ -37,6 +37,7 @@ class Loader < Source
|
|
37
37
|
@f.seek offset
|
38
38
|
l = @f.gets
|
39
39
|
unless l =~ BREAK_RE
|
40
|
+
Redwood::log "#{to_s}: offset mismatch in mbox file offset #{offset.inspect}: #{l.inspect}"
|
40
41
|
self.broken_msg = "offset mismatch in mbox file offset #{offset.inspect}: #{l.inspect}. Run 'sup-import --rebuild #{to_s}' to correct this."
|
41
42
|
raise SourceError, self.broken_msg
|
42
43
|
end
|
data/lib/sup/mbox/ssh-file.rb
CHANGED
@@ -75,6 +75,15 @@ class Buffer
|
|
75
75
|
def to_s; empty? ? "<empty>" : "[#{start}, #{endd})"; end # for debugging
|
76
76
|
end
|
77
77
|
|
78
|
+
## sharing a ssh connection to one machines between sources seems to
|
79
|
+
## create lots of broken situations: commands returning bizarre (large
|
80
|
+
## positive integer) return codes despite working; commands
|
81
|
+
## occasionally not working, etc. i suspect this is because of the
|
82
|
+
## fragile nature of the ssh syncshell.
|
83
|
+
##
|
84
|
+
## at any rate, we now open up one ssh connection per file, which is
|
85
|
+
## probably silly in the extreme case.
|
86
|
+
|
78
87
|
## the file-like interface to a remote file
|
79
88
|
class SSHFile
|
80
89
|
MAX_BUF_SIZE = 1024 * 1024 # bytes
|
@@ -82,9 +91,6 @@ class SSHFile
|
|
82
91
|
REASONABLE_TRANSFER_SIZE = 1024 * 32
|
83
92
|
SIZE_CHECK_INTERVAL = 60 * 1 # seconds
|
84
93
|
|
85
|
-
@@shells = {}
|
86
|
-
@@shells_mutex = Mutex.new
|
87
|
-
|
88
94
|
def initialize host, fn, ssh_opts={}
|
89
95
|
@buf = Buffer.new
|
90
96
|
@host = host
|
@@ -94,8 +100,11 @@ class SSHFile
|
|
94
100
|
@offset = 0
|
95
101
|
@say_id = nil
|
96
102
|
@broken_msg = nil
|
103
|
+
@shell = nil
|
104
|
+
@shell_mutex = Mutex.new
|
97
105
|
end
|
98
106
|
|
107
|
+
def to_s; "mbox+ssh://#@host/#@fn"; end ## TODO: remove thisis EVILness
|
99
108
|
def broken?; !@broken_msg.nil?; end
|
100
109
|
|
101
110
|
## TODO: share this code with imap
|
@@ -111,33 +120,20 @@ class SSHFile
|
|
111
120
|
|
112
121
|
def connect
|
113
122
|
raise SSHFileError, @broken_msg if broken?
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
if @@shells.member? key
|
120
|
-
returning(@@shells[key]) do |shell|
|
121
|
-
say "Checking for #@fn..."
|
122
|
-
begin
|
123
|
-
raise Errno::ENOENT, @fn unless shell.test("-e #@fn").status == 0
|
124
|
-
ensure
|
125
|
-
shutup
|
126
|
-
end
|
127
|
-
end
|
128
|
-
else
|
123
|
+
|
124
|
+
@shell_mutex.synchronize do
|
125
|
+
return if @shell
|
126
|
+
|
127
|
+
begin
|
129
128
|
say "Opening SSH connection to #{@host}..."
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
ensure
|
139
|
-
shutup
|
140
|
-
end
|
129
|
+
#raise SSHFileError, "simulated SSH file error"
|
130
|
+
session = Net::SSH.start @host, @ssh_opts
|
131
|
+
say "Starting SSH shell..."
|
132
|
+
@shell = session.shell.sync
|
133
|
+
say "Checking for #@fn..."
|
134
|
+
raise Errno::ENOENT, @fn unless @shell.test("-e #@fn").status == 0
|
135
|
+
ensure
|
136
|
+
shutup
|
141
137
|
end
|
142
138
|
end
|
143
139
|
end
|
@@ -178,10 +174,16 @@ private
|
|
178
174
|
# MBox::debug "sending command: #{cmd.inspect}"
|
179
175
|
begin
|
180
176
|
result = @shell.send_command cmd
|
181
|
-
raise SSHFileError, "Failure during remote command #{cmd.inspect}: #{result.stderr[0 .. 100]}" unless result.status == 0
|
177
|
+
raise SSHFileError, "Failure during remote command #{cmd.inspect}: #{(result.stderr || result.stdout || "")[0 .. 100]}" unless result.status == 0
|
182
178
|
rescue Net::SSH::Exception # these happen occasionally for no apparent reason. gotta love that nondeterminism!
|
183
|
-
retry if (retries += 1)
|
179
|
+
retry if (retries += 1) <= 3
|
184
180
|
raise
|
181
|
+
rescue Errno::EPIPE
|
182
|
+
if (retries += 1) <= e
|
183
|
+
@shell = nil
|
184
|
+
connect
|
185
|
+
retry
|
186
|
+
end
|
185
187
|
end
|
186
188
|
rescue Net::SSH::Exception, SSHFileError, Errno::ENOENT => e
|
187
189
|
@broken_msg = e.message
|
@@ -60,7 +60,7 @@ class ContactListMode < LineCursorMode
|
|
60
60
|
def multi_search people
|
61
61
|
mode = PersonSearchResultsMode.new people
|
62
62
|
BufferManager.spawn "personal search results", mode
|
63
|
-
mode.load_more_threads mode.buffer.content_height
|
63
|
+
mode.load_more_threads :num => mode.buffer.content_height
|
64
64
|
end
|
65
65
|
|
66
66
|
def search
|
data/lib/sup/modes/inbox-mode.rb
CHANGED
@@ -28,19 +28,21 @@ class InboxMode < ThreadIndexMode
|
|
28
28
|
|
29
29
|
def is_relevant? m; m.has_label? :inbox; end
|
30
30
|
|
31
|
-
def load_more_threads
|
31
|
+
def load_more_threads opts={}
|
32
|
+
n = opts[:num] || ThreadIndexMode::LOAD_MORE_THREAD_NUM
|
32
33
|
load_n_threads_background n, :label => :inbox,
|
33
34
|
:load_killed => false,
|
34
35
|
:load_spam => false,
|
35
|
-
:when_done => lambda
|
36
|
+
:when_done => (lambda do |num|
|
37
|
+
opts[:when_done].call if opts[:when_done]
|
36
38
|
BufferManager.flash "Added #{num} threads."
|
37
|
-
|
39
|
+
end)
|
38
40
|
end
|
39
41
|
|
40
42
|
def reload
|
41
43
|
drop_all_threads
|
42
44
|
BufferManager.draw_screen
|
43
|
-
load_more_threads buffer.content_height
|
45
|
+
load_more_threads :num => buffer.content_height
|
44
46
|
end
|
45
47
|
end
|
46
48
|
|
@@ -12,11 +12,13 @@ class LabelSearchResultsMode < ThreadIndexMode
|
|
12
12
|
|
13
13
|
def is_relevant? m; @labels.all? { |l| m.has_label? l }; end
|
14
14
|
|
15
|
-
def load_more_threads
|
15
|
+
def load_more_threads opts={}
|
16
|
+
n = opts[:num] || ThreadIndexMode::LOAD_MORE_THREAD_NUM
|
16
17
|
load_n_threads_background n, :labels => @labels,
|
17
18
|
:load_killed => true,
|
18
19
|
:load_spam => false,
|
19
20
|
:when_done =>(lambda do |num|
|
21
|
+
opts[:when_done].call if opts[:when_done]
|
20
22
|
if num > 0
|
21
23
|
BufferManager.flash "Found #{num} threads"
|
22
24
|
else
|
@@ -12,11 +12,13 @@ class PersonSearchResultsMode < ThreadIndexMode
|
|
12
12
|
|
13
13
|
def is_relevant? m; @people.any? { |p| m.from == p }; end
|
14
14
|
|
15
|
-
def load_more_threads
|
15
|
+
def load_more_threads opts={}
|
16
|
+
n = opts[:num] || ThreadIndexMode::LOAD_MORE_THREAD_NUM
|
16
17
|
load_n_threads_background n, :participants => @people,
|
17
18
|
:load_killed => true,
|
18
19
|
:load_spam => false,
|
19
20
|
:when_done =>(lambda do |num|
|
21
|
+
opts[:when_done].call if opts[:when_done]
|
20
22
|
if num > 0
|
21
23
|
BufferManager.flash "Found #{num} threads"
|
22
24
|
else
|
@@ -13,11 +13,13 @@ class SearchResultsMode < ThreadIndexMode
|
|
13
13
|
## TODO: think about this
|
14
14
|
def is_relevant? m; super; end
|
15
15
|
|
16
|
-
def load_more_threads
|
16
|
+
def load_more_threads opts={}
|
17
|
+
n = opts[:num] || ThreadIndexMode::LOAD_MORE_THREAD_NUM
|
17
18
|
load_n_threads_background n, :qobj => @qobj,
|
18
19
|
:load_killed => true,
|
19
20
|
:load_spam => false,
|
20
21
|
:when_done =>(lambda do |num|
|
22
|
+
opts[:when_done].call if opts[:when_done]
|
21
23
|
if num > 0
|
22
24
|
BufferManager.flash "Found #{num} threads"
|
23
25
|
else
|
data/lib/sup/source.rb
CHANGED
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.11
|
|
3
3
|
specification_version: 1
|
4
4
|
name: sup
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.0.
|
7
|
-
date: 2007-01-
|
6
|
+
version: 0.0.4
|
7
|
+
date: 2007-01-03 00:00:00 -08:00
|
8
8
|
summary: A console-based email client with the best features of GMail, mutt, and emacs. Features full text search, labels, tagged operations, multiple buffers, recent contacts, and more.
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -39,6 +39,7 @@ files:
|
|
39
39
|
- doc/FAQ.txt
|
40
40
|
- doc/Philosophy.txt
|
41
41
|
- doc/TODO
|
42
|
+
- doc/UserGuide.txt
|
42
43
|
- lib/sup.rb
|
43
44
|
- lib/sup/account.rb
|
44
45
|
- lib/sup/buffer.rb
|
@@ -136,3 +137,12 @@ dependencies:
|
|
136
137
|
- !ruby/object:Gem::Version
|
137
138
|
version: 0.0.0
|
138
139
|
version:
|
140
|
+
- !ruby/object:Gem::Dependency
|
141
|
+
name: net-ssh
|
142
|
+
version_requirement:
|
143
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
144
|
+
requirements:
|
145
|
+
- - ">"
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: 0.0.0
|
148
|
+
version:
|