blend 0.1.4
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/Rakefile +38 -0
- data/bin/blend +34 -0
- data/lib/blend.rb +28 -0
- data/lib/blend/chatbot/bot.rb +352 -0
- data/lib/blend/cli.rb +38 -0
- data/lib/blend/cli/github.rb +140 -0
- data/lib/blend/cli/heroku.rb +232 -0
- data/lib/blend/cli/hipchat.rb +61 -0
- data/lib/blend/cli/juice.rb +493 -0
- data/lib/blend/client.rb +63 -0
- data/lib/blend/client/github_client.rb +106 -0
- data/lib/blend/client/heroku_client.rb +87 -0
- data/lib/blend/client/hipchat_client.rb +78 -0
- data/lib/blend/client/juice_client.rb +400 -0
- data/lib/blend/core_ext/fixnum.rb +5 -0
- data/lib/blend/core_ext/object.rb +13 -0
- data/lib/blend/core_ext/string.rb +9 -0
- data/lib/blend/exceptions.rb +16 -0
- data/lib/blend/status/domain.rb +225 -0
- data/lib/blend/status/environment.rb +167 -0
- data/lib/blend/status/project.rb +227 -0
- data/lib/blend/status/project_resolver.rb +183 -0
- data/lib/blend/status/repo.rb +51 -0
- data/lib/blend/status/team.rb +29 -0
- data/lib/blend/version.rb +10 -0
- metadata +225 -0
@@ -0,0 +1,493 @@
|
|
1
|
+
module Blend
|
2
|
+
module CLI
|
3
|
+
class Juice < Thor
|
4
|
+
desc "login", "Login to juice"
|
5
|
+
def login
|
6
|
+
puts "Logged in." if client.login
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "logout", "Clear juice credentials"
|
10
|
+
def logout
|
11
|
+
puts "Juice credentials cleared." if client.logout
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "open PROJECT", "Open up juice"
|
15
|
+
def open( name )
|
16
|
+
project_id = project_id_from_name name
|
17
|
+
if( project_id.nil? )
|
18
|
+
puts "#{name} not found"
|
19
|
+
return
|
20
|
+
end
|
21
|
+
|
22
|
+
system "open http://happyfunjuice.com/projects/#{project_id}"
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "settings PROJECT", "Open up juice settings"
|
26
|
+
def settings(name)
|
27
|
+
project_id = project_id_from_name name
|
28
|
+
if( project_id.nil? )
|
29
|
+
puts "#{name} not found"
|
30
|
+
return
|
31
|
+
end
|
32
|
+
|
33
|
+
system "open http://happyfunjuice.com/projects/#{project_id}/overview"
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "create PROJECT", "Create a juice project"
|
37
|
+
def create( name )
|
38
|
+
puts "TIP: you can also run check #{name} to set up everything"
|
39
|
+
client.create_project( name )
|
40
|
+
end
|
41
|
+
|
42
|
+
desc "projects", "List juice projects"
|
43
|
+
def projects
|
44
|
+
puts
|
45
|
+
printf "%-25s %-10s %-10s %-25s\n".blue, '', 'Commits', 'Deploys', '', ''
|
46
|
+
printf "%-25s %-10s %-10s %-25s\n".blue, '', 'this', 'this', '', ''
|
47
|
+
printf "%-25s %-10s %-10s %-25s %s\n".underline.blue, 'Name', 'wk', 'wk', 'Hipchat room', 'Teams'
|
48
|
+
client.summary.sort{|a,b| b['name'].to_i <=> a['name'].to_i}.each_with_index do |project,i|
|
49
|
+
printf "%-25s %-10s %-10s %-25s %s\n".try{|x| i%4==3 ? x.underline : x},
|
50
|
+
#project['id'],
|
51
|
+
project['name'].projectize,
|
52
|
+
(project['has_source_feeds'] ? project['commits_this_week'] : ''),
|
53
|
+
(project['has_server_feeds'] ? project['deploys_this_week'] : ''),
|
54
|
+
project['blend_config']['hipchat_room'],
|
55
|
+
project['blend_config']['teams'].join( ',' )
|
56
|
+
end
|
57
|
+
puts
|
58
|
+
end
|
59
|
+
|
60
|
+
desc "organizations", "Get a list of your organizations"
|
61
|
+
def organizations
|
62
|
+
puts
|
63
|
+
printf "%-5s %-25s %-25s\n".underline.blue, 'ID', 'Name', 'Domain name'
|
64
|
+
client.organizations.each do |o|
|
65
|
+
printf "%-5s %-25s %-25s\n", o['id'], o['name'], o['domain_name']
|
66
|
+
end
|
67
|
+
puts
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
desc "feeds PROJECT", "Show the configured juice feeds"
|
72
|
+
def feeds(name)
|
73
|
+
puts
|
74
|
+
printf "%-30s %-20s %-30s\n".blue.underline, 'Feed', 'Environment', 'Namespace'
|
75
|
+
client.feeds( project_id_from_name( name ) ).sort do
|
76
|
+
|a,b| a['feed_name'] <=> b['feed_name']
|
77
|
+
end.each do |feed|
|
78
|
+
printf "%-30s %-20s %-30s\n",
|
79
|
+
feed['feed_name'],
|
80
|
+
(feed['environment'] || {})['name'],
|
81
|
+
[feed['namespace'],feed['name']].select {|x| x}.join( '/' )
|
82
|
+
end
|
83
|
+
puts
|
84
|
+
end
|
85
|
+
|
86
|
+
desc "users [PROJECT]", "Get a list of users"
|
87
|
+
def users( name=nil )
|
88
|
+
|
89
|
+
if name.nil?
|
90
|
+
_users = client.organization_users( 1 ) # Hard-code hfc org here
|
91
|
+
else
|
92
|
+
_users = client.project_users( project_id_from_name( name ) )
|
93
|
+
end
|
94
|
+
|
95
|
+
puts
|
96
|
+
printf "%-25s %35s %35s %35s %35s\n".blue.underline, 'Name', 'Email', 'Personal email', 'Heroku', 'Github'
|
97
|
+
_users.each do |u|
|
98
|
+
printf "%-25s %35s %35s %35s %35s\n", u['name'], u['email'], u['personal_email'], u['heroku_handle'], u['github_handle']
|
99
|
+
end
|
100
|
+
puts
|
101
|
+
end
|
102
|
+
|
103
|
+
desc "add_team PROJECT TEAM", "Add a github team to a project"
|
104
|
+
def add_team( name, team )
|
105
|
+
client.project_add_team( project_id_from_name( name ), team )
|
106
|
+
info( name )
|
107
|
+
end
|
108
|
+
|
109
|
+
desc "add_hipchat PROJECT ROOM", "Add a hipchat room to a project"
|
110
|
+
def add_hipchat( name, room )
|
111
|
+
client.project_add_hipchat( project_id_from_name( name ), room )
|
112
|
+
info( name )
|
113
|
+
end
|
114
|
+
|
115
|
+
desc "lookup_user NAME", "Looks up a user by email address"
|
116
|
+
def lookup_user( query )
|
117
|
+
pp client.lookup_user( query )
|
118
|
+
end
|
119
|
+
|
120
|
+
desc "search_users QUERY", "Look up a user by name, email, github, heroku, etc."
|
121
|
+
def search_users( query )
|
122
|
+
puts
|
123
|
+
printf "%-25s %35s %35s %35s %35s\n".blue.underline, 'Name', 'Email', 'Personal email', 'Heroku', 'Github'
|
124
|
+
client.search_users(query).each do |u|
|
125
|
+
printf "%-25s %35s %35s %35s %35s\n", u['name'], u['email'], u['personal_email'], u['heroku_handle'], u['github_handle']
|
126
|
+
end
|
127
|
+
puts
|
128
|
+
end
|
129
|
+
|
130
|
+
# desc "user_set FIELD VALUE", "Set the value of a particular field for a user"
|
131
|
+
# def user_set( field, value )
|
132
|
+
# client
|
133
|
+
# end
|
134
|
+
|
135
|
+
desc "heroku_api TOKEN", "Sets the organization heroku token"
|
136
|
+
def heroku_api( token )
|
137
|
+
client.heroku_api token
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
desc "hipchat_api TOKEN", "Sets the organization hipchat token"
|
142
|
+
def hipchat_api( token )
|
143
|
+
client.hipchat_api token
|
144
|
+
end
|
145
|
+
|
146
|
+
desc "all_projects [--resolve]", "Show all project status"
|
147
|
+
option :resolve
|
148
|
+
def all_projects
|
149
|
+
client.projects.each do |p|
|
150
|
+
project( p['name'] )
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
desc "project NAME [--resolve]", "Get project status"
|
155
|
+
option :resolve
|
156
|
+
def project( name )
|
157
|
+
status = Blend::Status::Project.new( name, options[:resolve] )
|
158
|
+
|
159
|
+
status.header "#{name}: Juice Configuration"
|
160
|
+
status.check "Project Exists", :project_found
|
161
|
+
status.check "Hipchat Room", :hipchat
|
162
|
+
status.check "Github Teams", :github_teams
|
163
|
+
status.check "Repos Configured", :repos_setup
|
164
|
+
status.check "Source Control", :source_control
|
165
|
+
status.check "Bug Tracking", :bugtracking
|
166
|
+
|
167
|
+
status.header "#{name}: Environments"
|
168
|
+
status.check "Production", :production
|
169
|
+
status.check "Staging", :staging
|
170
|
+
|
171
|
+
status.header "#{name}: Team Configuration (#{status.github_teams.join(',')})" if status.github_teams.length > 0
|
172
|
+
|
173
|
+
status.check "Members", :github_members
|
174
|
+
|
175
|
+
status.check "User Matchup", :juice_users_synced
|
176
|
+
|
177
|
+
status.github_members.each do |m|
|
178
|
+
member = m[:name]
|
179
|
+
access = m[:access]
|
180
|
+
# puts member
|
181
|
+
juice_user = client.user_from_github_user member
|
182
|
+
if access == :read
|
183
|
+
printf "%-20s %15s".yellow, member, "readonly"
|
184
|
+
else
|
185
|
+
printf "%-20s %15s".green, member, "fullaccess"
|
186
|
+
end
|
187
|
+
|
188
|
+
if juice_user
|
189
|
+
printf " %-20s %s\n", juice_user['name'], juice_user['email']
|
190
|
+
else
|
191
|
+
puts " Unknown to juice".red
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
status.repo_status.each do |repo|
|
196
|
+
status.header "#{repo.name} Configuration"
|
197
|
+
|
198
|
+
repo.check "Private", :private?
|
199
|
+
repo.check "Hipchat Deployhook", :hipchat_hook
|
200
|
+
end
|
201
|
+
|
202
|
+
status.environment_status.each do |env|
|
203
|
+
status.header "Server: #{env.server} Configuration"
|
204
|
+
|
205
|
+
env.check "Dyno Redundancy", :dyno_redundancy
|
206
|
+
env.check "Database", :database
|
207
|
+
env.check "Backups", :backups
|
208
|
+
env.check "Stack", :stack
|
209
|
+
env.check "Exception Handling", :exception_handling
|
210
|
+
env.check "Deploy Hooks", :deployhooks
|
211
|
+
env.check "Log Monitoring", :log_monitoring
|
212
|
+
env.check "App Monitoring", :app_monitoring
|
213
|
+
env.check "SSL Addon", :ssl
|
214
|
+
end
|
215
|
+
|
216
|
+
status.domains_status.each do |domain|
|
217
|
+
status.header "DNS: #{domain.domain} configuration"
|
218
|
+
|
219
|
+
domain.check "Registered?", :registered?
|
220
|
+
domain.check "Expires", :expires
|
221
|
+
domain.check "Owner", :owner
|
222
|
+
domain.check "SSL Cert", :ssl_exists?
|
223
|
+
domain.check "SSL Expires", :ssl_valid_until
|
224
|
+
domain.check "SSL Common Name", :ssl_common_name
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
=begin
|
229
|
+
|
230
|
+
|
231
|
+
|
232
|
+
|
233
|
+
desc "check [PROJECT]", "Check a project config (all the checks)"
|
234
|
+
def check( name )
|
235
|
+
check_project( name )
|
236
|
+
puts
|
237
|
+
check_hipchat( name )
|
238
|
+
puts
|
239
|
+
check_team( name )
|
240
|
+
puts
|
241
|
+
check_hooks( name )
|
242
|
+
puts
|
243
|
+
info( name )
|
244
|
+
end
|
245
|
+
|
246
|
+
desc "check_hooks [PROJECT]", "Check to see if the hooks are configured"
|
247
|
+
def check_hooks( name )
|
248
|
+
puts "Looking for github hooks".bold
|
249
|
+
##
|
250
|
+
# Github Hooks
|
251
|
+
##
|
252
|
+
project_id = project_id_from_name name
|
253
|
+
return if project_id.nil?
|
254
|
+
|
255
|
+
data = client.project project_id
|
256
|
+
config = data['blend_config']
|
257
|
+
|
258
|
+
teams = config['teams']
|
259
|
+
|
260
|
+
if teams.count > 0
|
261
|
+
teams.each do |team|
|
262
|
+
puts "Looking at repos for #{team}".bold
|
263
|
+
github_client.list_team_repos( team ).each do |repo|
|
264
|
+
printf "%-40s %s\n", repo['full_name'], repo['description']
|
265
|
+
found = {}
|
266
|
+
github_client.list_hooks( repo['full_name'] ).each do |hook|
|
267
|
+
puts "Found #{hook["name"]}"
|
268
|
+
found[hook['name']] = true
|
269
|
+
end
|
270
|
+
|
271
|
+
unless found['hipchat']
|
272
|
+
puts "Missing hipchat hook"
|
273
|
+
if config['hipchat_room']
|
274
|
+
puts "Adding Hipchat Hook"
|
275
|
+
Blend::CLI::Github.new.add_hipchat( repo['full_name'], config['hipchat_room'])
|
276
|
+
else
|
277
|
+
puts "Missing hipchat room"
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
else
|
283
|
+
puts "Teams must be set up correctly in order to monitor hooks.".red
|
284
|
+
end
|
285
|
+
|
286
|
+
apps = client.heroku_apps( project_id )
|
287
|
+
|
288
|
+
|
289
|
+
if( apps['production'] )
|
290
|
+
puts "Production".blue
|
291
|
+
apps['production'].each do |app|
|
292
|
+
Blend::CLI::Heroku.new.check app['name']
|
293
|
+
a = heroku_client.addons(app['name'], /deployhooks/)
|
294
|
+
|
295
|
+
if( a.nil? || a.length == 0 )
|
296
|
+
puts "Adding deploy hook".yellow
|
297
|
+
system( "echo heroku addons:add deployhooks:hipchat --auth_token=#{client.hipchat_api} --room=\"#{config['hipchat_room']} --app #{app['name']}\"")
|
298
|
+
system( "heroku addons:add deployhooks:hipchat --auth_token=#{client.hipchat_api} --room=\"#{config['hipchat_room']}\" --app #{app['name']}")
|
299
|
+
Blend::Client::hipchat_client.post_message config['hipchat_room'], "Heroku app: #{app['name']} commit hook now added"
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
if( apps['staging'] )
|
304
|
+
puts "Staging".blue
|
305
|
+
apps['staging'].each do |app|
|
306
|
+
Blend::CLI::Heroku.new.check app['name']
|
307
|
+
a = heroku_client.addons(app['name'], /deployhooks/)
|
308
|
+
|
309
|
+
if( a.nil? || a.length == 0 )
|
310
|
+
puts "Adding deploy hook".yellow
|
311
|
+
system( "echo heroku addons:add deployhooks:hipchat --auth_token=#{client.hipchat_api} --room=\"#{config['hipchat_room']} --app #{app['name']}\"")
|
312
|
+
system( "heroku addons:add deployhooks:hipchat --auth_token=#{client.hipchat_api} --room=\"#{config['hipchat_room']} --app #{app['name']}\"")
|
313
|
+
Blend::Client::hipchat_client.post_message config['hipchat_room'], "Heroku app: #{app['name']} commit hook now added"
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|
319
|
+
=end
|
320
|
+
|
321
|
+
desc "hipchat_check", "Prints out all the rooms not assigned to rooms"
|
322
|
+
def hipchat_check
|
323
|
+
begin
|
324
|
+
client.hipchat_check.each do |x|
|
325
|
+
printf "%-30s %s\n", x[:room], x[:projects].join( "," )
|
326
|
+
end
|
327
|
+
rescue Exceptions::HipchatAuthenticationFailure
|
328
|
+
puts "Unable to connect to Hipchat. Is your key valid?".red
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
desc "github_team_check", "Prints out all the teams not assigned to projects"
|
333
|
+
option :fix
|
334
|
+
def github_team_check
|
335
|
+
client.github_team_check.each do |team,projects|
|
336
|
+
next if team == 'Owners'
|
337
|
+
next if projects.size > 0
|
338
|
+
puts "#{team} has no juice project".red
|
339
|
+
# printf "%-30s %s\n", team, projects.collect {|x| x['name'] }.join( "," )
|
340
|
+
|
341
|
+
if( options[:fix] )
|
342
|
+
choices = client.projects.collect { |x| x['name'] }
|
343
|
+
|
344
|
+
project = choose do |menu|
|
345
|
+
menu.header = "Select a github team action"
|
346
|
+
menu.prompt = "Please choose a unassigned juice project to associate #{team} with:"
|
347
|
+
|
348
|
+
menu.choice "Ignore" do
|
349
|
+
"ignore"
|
350
|
+
end
|
351
|
+
|
352
|
+
menu.choice "Create a project called #{team}" do
|
353
|
+
"create"
|
354
|
+
end
|
355
|
+
|
356
|
+
menu.choices *choices
|
357
|
+
end
|
358
|
+
|
359
|
+
if( project == "ignore" )
|
360
|
+
puts "Skip it"
|
361
|
+
else
|
362
|
+
if( project == "create" )
|
363
|
+
puts "TODO: Create a new project called #{team}"
|
364
|
+
else
|
365
|
+
add_team project, team
|
366
|
+
# puts "Attaching to: #{project}"
|
367
|
+
end
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
desc "activity PROJECT", "Shows recent project activity in last week or (default) current week [--lastweek] [--thisweek]"
|
374
|
+
option :lastweek
|
375
|
+
option :thisweek
|
376
|
+
def activity( name )
|
377
|
+
puts
|
378
|
+
project_id = project_id_from_name name
|
379
|
+
return if project_id.nil?
|
380
|
+
|
381
|
+
now = DateTime.now.to_date + 1
|
382
|
+
after = now - now.wday
|
383
|
+
before = Time.now
|
384
|
+
|
385
|
+
if( options[:lastweek] )
|
386
|
+
after -= 7
|
387
|
+
before = after + 7
|
388
|
+
end
|
389
|
+
|
390
|
+
summary = client.activities( project_id, after.to_time, before.to_time )
|
391
|
+
|
392
|
+
project = client.project( project_id )
|
393
|
+
|
394
|
+
puts "#{project['name']} activity".bold + " for #{summary[:after].strftime( "%Y-%m-%d %H:%M" )} (#{((Time.now-summary[:after])/3600/24).round(1)} days ago) - #{summary[:before].strftime( "%Y-%m-%d %H:%M" )} (now)"
|
395
|
+
puts
|
396
|
+
puts "Activity Summary".underline.blue
|
397
|
+
summary[:type].keys.sort.each do |x|
|
398
|
+
printf "%-6s %s\n", summary[:type][x].count, x
|
399
|
+
end
|
400
|
+
|
401
|
+
puts
|
402
|
+
puts "Activity Breakdown".underline.blue
|
403
|
+
summary[:actors_activites].keys.sort.each do |x|
|
404
|
+
summary[:actors_activites][x].keys.select { |x| x}.sort.each do |type|
|
405
|
+
printf "%-6s %-25s %s\n", summary[:actors_activites][x][type].count, type.strip_email, x
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
puts
|
410
|
+
puts "New Tickets".underline.blue
|
411
|
+
(summary[:type]['bugtracking:openticket'] || []).each do |activity|
|
412
|
+
printf "%-15s %-100s\n", activity['actor_identifier'], activity['description'][0..100].gsub( /\n/, " " )
|
413
|
+
#puts activity['description'][0..100].gsub( /\n/, " " )
|
414
|
+
end
|
415
|
+
|
416
|
+
puts
|
417
|
+
puts "Closed Tickets".underline.blue
|
418
|
+
(summary[:type]['bugtracking:closedticket'] || []).each do |activity|
|
419
|
+
printf "%-15s %-100s\n", activity['actor_identifier'], activity['description'][0..100].gsub( /\n/, " " )
|
420
|
+
#puts activity['description'][0..100].gsub( /\n/, " " )
|
421
|
+
end
|
422
|
+
|
423
|
+
puts
|
424
|
+
puts "Active Tickets".underline.blue
|
425
|
+
summary[:type].keys.select do |x|
|
426
|
+
x =~ /bugtracking/
|
427
|
+
end.collect do |x|
|
428
|
+
summary[:type][x].collect do |a|
|
429
|
+
a['description']
|
430
|
+
end
|
431
|
+
end.flatten.sort.uniq.each do |x|
|
432
|
+
puts x[0..100].gsub( /\n/, " " ) unless x =~ /^\[Changeset\]/
|
433
|
+
end
|
434
|
+
|
435
|
+
puts
|
436
|
+
puts "Commits".underline.blue
|
437
|
+
(summary[:type]['sourcecontrol:commit'] || []).each do |x|
|
438
|
+
printf "%-30s %s\n", x['actor_identifier'].strip_email, x['description'][0..100].gsub( /\n/, " " )
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
desc "report NAME", "Report for an individual project"
|
443
|
+
option :lastweek
|
444
|
+
def report(name)
|
445
|
+
info name
|
446
|
+
activity name
|
447
|
+
end
|
448
|
+
|
449
|
+
desc "report_dump", "Write out weekly reports"
|
450
|
+
option :lastweek
|
451
|
+
def report_dump
|
452
|
+
puts "Loading projects"
|
453
|
+
|
454
|
+
system "mkdir -p /tmp/juice_reports"
|
455
|
+
client.projects.each do |project|
|
456
|
+
File.open( "/tmp/juice_reports/#{project['name']}.txt", "w" ) do |out|
|
457
|
+
puts "Running report for #{project['name']}"
|
458
|
+
$stdout = out
|
459
|
+
info project['name']
|
460
|
+
activities project['name']
|
461
|
+
$stdout = STDOUT
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
no_commands do
|
467
|
+
def client
|
468
|
+
@client ||= Blend::Client.juice_client
|
469
|
+
end
|
470
|
+
|
471
|
+
def hipchat_client
|
472
|
+
Blend::Client.hipchat_client
|
473
|
+
end
|
474
|
+
|
475
|
+
def github_client
|
476
|
+
Blend::Client.github_client
|
477
|
+
end
|
478
|
+
|
479
|
+
def heroku_client
|
480
|
+
Blend::Client.heroku_client
|
481
|
+
end
|
482
|
+
|
483
|
+
def project_id_from_name( name )
|
484
|
+
client.project_id_from_name( name )
|
485
|
+
end
|
486
|
+
|
487
|
+
def set( s )
|
488
|
+
!s.nil? && s != ""
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
493
|
+
end
|