ptt 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,102 @@
1
+ require 'hirb'
2
+ require 'hirb-unicode'
3
+
4
+ module PTT
5
+
6
+ class DataTable
7
+
8
+ extend ::Hirb::Console
9
+
10
+ def initialize(dataset)
11
+ @rows = dataset.map{ |row| DataRow.new(row, dataset) }
12
+ end
13
+
14
+ def print(config={})
15
+ if @rows.empty?
16
+ puts "\n#{'-- empty list --'.center(36)}\n"
17
+ else
18
+
19
+ max_width = Hirb::Util.detect_terminal_size()[0]
20
+ if config[:max_width] && config[:max_width] < max_width
21
+ max_width = config[:max_width]
22
+ end
23
+
24
+ self.class.table @rows, :fields => [:num] + self.class.fields,
25
+ :change_fields => %w{num pt_id},
26
+ :unicode => true, :description => false,
27
+ :max_width => max_width
28
+ end
29
+ end
30
+
31
+ def [](pos)
32
+ pos = pos.to_i
33
+ (pos < 1 || pos > @rows.length) ? nil : @rows[pos-1].record
34
+ end
35
+
36
+ def length
37
+ @rows.length
38
+ end
39
+
40
+ def self.fields
41
+ []
42
+ end
43
+
44
+ end
45
+
46
+
47
+ class ProjectTable < DataTable
48
+
49
+ def self.fields
50
+ [:name]
51
+ end
52
+
53
+ end
54
+
55
+
56
+ class TasksTable < DataTable
57
+
58
+ def self.fields
59
+ [:name, :state, :id]
60
+ end
61
+
62
+ end
63
+
64
+ class MultiUserTasksTable < DataTable
65
+
66
+ def self.fields
67
+ [:owned_by, :name, :state, :id]
68
+ end
69
+
70
+ end
71
+
72
+ class PersonsTable < DataTable
73
+
74
+ def self.fields
75
+ [:name]
76
+ end
77
+
78
+ end
79
+
80
+ class MembersTable < DataTable
81
+
82
+ def self.fields
83
+ [:name]
84
+ end
85
+
86
+ end
87
+
88
+ class TodoTaskTable < DataTable
89
+
90
+ def self.fields
91
+ [:description]
92
+ end
93
+ end
94
+
95
+ class ActionTable < DataTable
96
+
97
+ def self.fields
98
+ [:action]
99
+ end
100
+ end
101
+
102
+ end
@@ -0,0 +1,25 @@
1
+ # Nasty tricks to debug the interaction with the Pivotal Tracker API
2
+
3
+ module RestClient
4
+ class Request
5
+ alias_method :rest_client_execute, :execute
6
+ def execute &block
7
+ puts ""
8
+ puts "Request: #{method.to_s.upcase} #{url}"
9
+ puts ""
10
+ puts "Payload: #{payload}"
11
+ rest_client_execute &block
12
+ end
13
+ end
14
+ end
15
+
16
+ module HappyMapper
17
+ module ClassMethods
18
+ alias_method :pivotal_tracker_parse, :parse
19
+ def parse(xml, options={})
20
+ xml = xml.to_s if xml.is_a?(RestClient::Response)
21
+ puts "\nResponse:\n#{xml}\n"
22
+ pivotal_tracker_parse(xml, options)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ # PivotalTracker gem doesn't update the connection when switching SSL
2
+ # I'll submit a pull request, but in the meantime this patch should solve it
3
+ module PivotalTracker
4
+ class Client
5
+ def self.use_ssl=(val)
6
+ if !@use_ssl == val
7
+ @connection = nil if @connection
8
+ @connections = {} if @connections
9
+ end
10
+
11
+ @use_ssl = val
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,750 @@
1
+ require 'yaml'
2
+ require 'colored'
3
+ require 'highline'
4
+ require 'tempfile'
5
+ require 'uri'
6
+
7
+ class PTT::UI
8
+
9
+ GLOBAL_CONFIG_PATH = ENV['HOME'] + "/.pt"
10
+ LOCAL_CONFIG_PATH = Dir.pwd + '/.pt'
11
+
12
+ def initialize(args)
13
+ require 'pt/debugger' if ARGV.delete('--debug')
14
+ @io = HighLine.new
15
+ @global_config = load_global_config
16
+ @client = PTT::Client.new(@global_config[:api_number])
17
+ @local_config = load_local_config
18
+ @project = @client.get_project(@local_config[:project_id])
19
+ command = args[0].to_sym rescue :my_work
20
+ @params = args[1..-1]
21
+ commands.include?(command.to_sym) ? send(command.to_sym) : help
22
+ end
23
+
24
+ def my_work
25
+ title("My Work for #{user_s} in #{project_to_s}")
26
+ stories = @client.get_my_work(@project, @local_config[:user_name])
27
+ PTT::TasksTable.new(stories).print @global_config
28
+ end
29
+
30
+ def todo
31
+ title("My Work for #{user_s} in #{project_to_s}")
32
+ stories = @client.get_my_work(@project, @local_config[:user_name])
33
+ stories = stories.select { |story| story.current_state == "unscheduled" }
34
+ PTT::TasksTable.new(stories).print @global_config
35
+ end
36
+
37
+ def started
38
+ # find by a single user
39
+ if @params[0]
40
+ stories = @project.stories(filter: "owner:#{@params[0]} state:started")
41
+ PTT::TasksTable.new(stories).print @global_config
42
+ else
43
+ # otherwise show them all
44
+ title("Stories started for #{project_to_s}")
45
+ stories = @project.stories(filter:'state:started')
46
+ PTT::TasksTable.new(stories).print @global_config
47
+ end
48
+ end
49
+
50
+ def list
51
+ if @params[0]
52
+ if @params[0] == "all"
53
+ stories = @client.get_work(@project)
54
+ PTT::TasksTable.new(stories).print @global_config
55
+ else
56
+ stories = @client.get_my_work(@project, @params[0])
57
+ PTT::TasksTable.new(stories).print @global_config
58
+ end
59
+ else
60
+ members = @client.get_members(@project)
61
+ table = PTT::MembersTable.new(members)
62
+ user = select("Please select a member to see his tasks.", table).name
63
+ title("Work for #{user} in #{project_to_s}")
64
+ stories = @client.get_my_work(@project, user)
65
+ PTT::TasksTable.new(stories).print @global_config
66
+ end
67
+ end
68
+
69
+ def recent
70
+ title("Your recent stories from #{project_to_s}")
71
+ stories = @project.stories( ids: @local_config[:recent_tasks].join(',') )
72
+ PTT::MultiUserTasksTable.new(stories).print @global_config
73
+ end
74
+
75
+ def label
76
+
77
+ task = get_task_from_params "Please select a story to show"
78
+ unless task
79
+ message("No matches found for '#{@params[0]}', please use a valid pivotal story Id")
80
+ return
81
+ end
82
+
83
+ if @params[1]
84
+ label = @params[1]
85
+ else
86
+ label = ask("Which label?")
87
+ end
88
+
89
+ @client.add_label( @project, task, label );
90
+ show_task(task_by_id_or_pt_id(task.id))
91
+ end
92
+
93
+ def create
94
+ if @params[0]
95
+ name = @params[0]
96
+ owner = @params[1] || @local_config[:user_name]
97
+ requester = @local_config[:user_name]
98
+ task_type = task_type_or_nil(@params[1]) || task_type_or_nil(@params[2]) || 'feature'
99
+ else
100
+ title("Let's create a new task:")
101
+ name = ask("Name for the new task:")
102
+ end
103
+
104
+ owner = @client.find_member(@project, owner).person.id if owner.kind_of?(String)
105
+
106
+ unless owner
107
+ if ask('Do you want to assign it now? (y/n)').downcase == 'y'
108
+ members = @client.get_members(@project)
109
+ table = PTT::PersonsTable.new(members.map(&:person))
110
+ owner = select("Please select a member to assign him the task.", table).id
111
+ else
112
+ owner = nil
113
+ end
114
+ requester = @local_config[:user_name]
115
+ task_type = ask('Type? (c)hore, (b)ug, anything else for feature)')
116
+ end
117
+
118
+ task_type = case task_type
119
+ when 'c', 'chore'
120
+ 'chore'
121
+ when 'b', 'bug'
122
+ 'bug'
123
+ else
124
+ 'feature'
125
+ end
126
+ result = nil
127
+
128
+ owner_ids = [owner]
129
+ # did you do a -m so you can add a description?
130
+ if ARGV.include? "-m" or ARGV.include? "--m"
131
+ editor = ENV.fetch('EDITOR') { 'vi' }
132
+ temp_path = "/tmp/editor-#{ Process.pid }.txt"
133
+ system "#{ editor } #{ temp_path }"
134
+
135
+ description = File.read(temp_path)
136
+ story = @client.create_task_with_description(@project, name, owner_ids, task_type, description)
137
+
138
+ else
139
+ story = @client.create_task(@project, name, owner_ids, task_type)
140
+ end
141
+ # TODO need result
142
+
143
+ congrats("#{task_type} for #{owner} open #{story.url}")
144
+ end
145
+
146
+ def open
147
+ if @params[0]
148
+ if task = @client.get_task_by_id(@project, @params[0])
149
+ congrats("Opening #{task.name}")
150
+ `open #{task.url}`
151
+ else
152
+ message("Story ##{@params[0]} not found")
153
+ end
154
+ else
155
+ tasks = @client.get_my_open_tasks(@project, @local_config[:user_name])
156
+ table = PTT::TasksTable.new(tasks)
157
+ title("Tasks for #{user_s} in #{project_to_s}")
158
+ task = select("Please select a story to open it in the browser", table)
159
+ end
160
+ end
161
+
162
+ def comment
163
+ if @params[0]
164
+ task = task_by_id_or_pt_id @params[0].to_i
165
+ comment = @params[1]
166
+ title("Adding a comment to #{task.name}")
167
+ else
168
+ tasks = @client.get_my_work(@project, @local_config[:user_name])
169
+ table = PTT::TasksTable.new(tasks)
170
+ title("Tasks for #{user_s} in #{project_to_s}")
171
+ task = select("Please select a story to comment it", table)
172
+ comment = ask("Write your comment")
173
+ end
174
+ if @client.comment_task(@project, task, comment)
175
+ congrats("Comment sent, thanks!")
176
+ save_recent_task( task.id )
177
+ else
178
+ error("Ummm, something went wrong.")
179
+ end
180
+ end
181
+
182
+ def assign
183
+ if @params[0]
184
+ task = task_by_id_or_pt_id @params[0].to_i
185
+ owner = find_owner @params[1]
186
+ else
187
+ title("Tasks for #{user_s} in #{project_to_s}")
188
+ tasks = @client.get_tasks_to_assign(@project)
189
+ table = PTT::TasksTable.new(tasks)
190
+ task = select("Please select a task to assign it an owner", table)
191
+ end
192
+
193
+ unless owner
194
+ members = @client.get_members(@project)
195
+ table = PTT::PersonsTable.new(members.map(&:person))
196
+ owner = select("Please select a member to assign him the task", table)
197
+ end
198
+ @client.assign_task(@project, task, owner)
199
+
200
+ congrats("Task assigned to #{owner.initials}, thanks!")
201
+ end
202
+
203
+ def estimate
204
+ if @params[0]
205
+ task = task_by_id_or_pt_id @params[0].to_i
206
+ title("Estimating '#{task.name}'")
207
+
208
+ if [0,1,2,3].include? @params[1].to_i
209
+ estimation = @params[1]
210
+ end
211
+ else
212
+ tasks = @client.get_my_tasks_to_estimate(@project, @local_config[:user_name])
213
+ table = PTT::TasksTable.new(tasks)
214
+ title("Tasks for #{user_s} in #{project_to_s}")
215
+ task = select("Please select a story to estimate it", table)
216
+ end
217
+
218
+ estimation ||= ask("How many points you estimate for it? (#{@project.point_scale})")
219
+ @client.estimate_task(@project, task, estimation)
220
+ congrats("Task estimated, thanks!")
221
+ end
222
+
223
+ def start
224
+ if @params[0]
225
+ task = task_by_id_or_pt_id @params[0].to_i
226
+ title("Starting '#{task.name}'")
227
+ else
228
+ tasks = @client.get_my_tasks_to_start(@project, @local_config[:user_name])
229
+ table = PTT::TasksTable.new(tasks)
230
+ title("Tasks for #{user_s} in #{project_to_s}")
231
+ task = select("Please select a story to mark it as started", table)
232
+ end
233
+ start_task task
234
+ end
235
+
236
+ def finish
237
+ if @params[0]
238
+ task = task_by_id_or_pt_id @params[0].to_i
239
+ title("Finishing '#{task.name}'")
240
+ else
241
+ tasks = @client.get_my_tasks_to_finish(@project, @local_config[:user_name])
242
+ table = PTT::TasksTable.new(tasks)
243
+ title("Tasks for #{user_s} in #{project_to_s}")
244
+ task = select("Please select a story to mark it as finished", table)
245
+ end
246
+ finish_task task
247
+ end
248
+
249
+ def deliver
250
+ if @params[0]
251
+ task = task_by_id_or_pt_id @params[0].to_i
252
+ title("Delivering '#{task.name}'")
253
+ else
254
+ tasks = @client.get_my_tasks_to_deliver(@project, @local_config[:user_name])
255
+ table = PTT::TasksTable.new(tasks)
256
+ title("Tasks for #{user_s} in #{project_to_s}")
257
+ task = select("Please select a story to mark it as delivered", table)
258
+ end
259
+
260
+ deliver_task task
261
+ end
262
+
263
+ def accept
264
+ if @params[0]
265
+ task = task_by_id_or_pt_id @params[0].to_i
266
+ title("Accepting '#{task.name}'")
267
+ else
268
+ tasks = @client.get_my_tasks_to_accept(@project, @local_config[:user_name])
269
+ table = PTT::TasksTable.new(tasks)
270
+ title("Tasks for #{user_s} in #{project_to_s}")
271
+ task = select("Please select a story to mark it as accepted", table)
272
+ end
273
+ @client.mark_task_as(@project, task, 'accepted')
274
+ congrats("Task accepted, hooray!")
275
+ end
276
+
277
+ def show
278
+ title("Tasks for #{user_s} in #{project_to_s}")
279
+ task = get_task_from_params "Please select a story to show"
280
+ unless task
281
+ message("No matches found for '#{@params[0]}', please use a valid pivotal story Id")
282
+ return
283
+ end
284
+ show_task(task)
285
+ end
286
+
287
+ def tasks
288
+ title("Open story tasks for #{user_s} in #{project_to_s}")
289
+
290
+ unless story = get_task_from_params( "Please select a story to show pending tasks" )
291
+ message("No matches found for '#{@params[0]}', please use a valid pivotal story Id")
292
+ return
293
+ end
294
+
295
+ story_task = get_open_story_task_from_params(story)
296
+
297
+ if story_task.position == -1
298
+ description = ask('Title for new task')
299
+ story.create_task(:description => description)
300
+ congrats("New todo task added to \"#{story.name}\"")
301
+ else
302
+ edit_story_task story_task
303
+ end
304
+ end
305
+
306
+ # takes a comma separated list of ids and prints the collection of tasks
307
+ def show_condensed
308
+ title("Tasks for #{user_s} in #{project_to_s}")
309
+ tasks = []
310
+ if @params[0]
311
+ @params[0].each_line(',') do |line|
312
+ tasks << @client.get_task_by_id(@project, line.to_i)
313
+ end
314
+ table = PTT::TasksTable.new(tasks)
315
+ table.print
316
+ end
317
+ end
318
+
319
+ # TODO implement story notes and comment
320
+ def reject
321
+ title("Tasks for #{user_s} in #{project_to_s}")
322
+ if @params[0]
323
+ task = @client.get_story(@project, @params[0])
324
+ title("Rejecting '#{task.name}'")
325
+ else
326
+ tasks = @client.get_my_tasks_to_reject(@project, @local_config[:user_name])
327
+ table = PTT::TasksTable.new(tasks)
328
+ title("Tasks for #{user_s} in #{project_to_s}")
329
+ task = select("Please select a story to mark it as rejected", table)
330
+ end
331
+
332
+ if @params[1]
333
+ comment = @params[1]
334
+ else
335
+ comment = ask("Please explain why are you rejecting the task.")
336
+ end
337
+
338
+ if @client.comment_task(@project, task, comment)
339
+ result = @client.mark_task_as(@project, task, 'rejected')
340
+ congrats("Task rejected, thanks!")
341
+ else
342
+ error("Ummm, something went wrong.")
343
+ end
344
+ end
345
+
346
+ def done
347
+ if @params[0]
348
+ task = task_by_id_or_pt_id @params[0].to_i
349
+
350
+ #we need this for finding again later
351
+ task_id = task.id
352
+
353
+ if !@params[1] && task.estimate == -1
354
+ error("You need to give an estimate for this task")
355
+ return
356
+ end
357
+
358
+ if @params[1] && task.estimate == -1
359
+ if [0,1,2,3].include? @params[1].to_i
360
+ estimate_task(task, @params[1].to_i)
361
+ end
362
+ if @params[2]
363
+ task = task_by_id_or_pt_id task_id
364
+ @client.comment_task(@project, task, @params[2])
365
+ end
366
+ else
367
+ @client.comment_task(@project, task, @params[1]) if @params[1]
368
+ end
369
+
370
+ task = task_by_id_or_pt_id task_id
371
+ start_task task
372
+
373
+ task = task_by_id_or_pt_id task_id
374
+ finish_task task
375
+
376
+ task = task_by_id_or_pt_id task_id
377
+ deliver_task task
378
+ end
379
+ end
380
+
381
+ def estimate_task task, difficulty
382
+ result = @client.estimate_task(@project, task, difficulty)
383
+ if result.errors.any?
384
+ error(result.errors.errors)
385
+ else
386
+ congrats("Task estimated, thanks!")
387
+ end
388
+ end
389
+
390
+ def start_task task
391
+ @client.mark_task_as(@project, task, 'started')
392
+ congrats("Task started, go for it!")
393
+ end
394
+
395
+ def finish_task task
396
+ if task.story_type == 'chore'
397
+ @client.mark_task_as(@project, task, 'accepted')
398
+ else
399
+ @client.mark_task_as(@project, task, 'finished')
400
+ end
401
+ congrats("Another task bites the dust, yeah!")
402
+ end
403
+
404
+ def deliver_task task
405
+ return if task.story_type == 'chore'
406
+ @client.mark_task_as(@project, task, 'delivered')
407
+ congrats("Task delivered, congrats!")
408
+ end
409
+
410
+ def find
411
+ if (story_id = @params[0].to_i).nonzero?
412
+ if task = task_by_id_or_pt_id(@params[0].to_i)
413
+ return show_task(task)
414
+ else
415
+ message("Task not found by id (#{story_id}), falling back to text search")
416
+ end
417
+ end
418
+
419
+ if @params[0]
420
+ tasks = @client.search_for_story(@project, @params[0])
421
+ tasks.each do |story_task|
422
+ title("--- [#{(tasks.index story_task) + 1 }] -----------------")
423
+ show_task(story_task)
424
+ end
425
+ message("No matches found for '#{@params[0]}'") if tasks.empty?
426
+ else
427
+ message("You need to provide a substring for a tasks title.")
428
+ end
429
+ end
430
+
431
+ def updates
432
+ activities = @client.get_activities(@project, @params[0])
433
+ tasks = @client.get_my_work(@project, @local_config[:user_name])
434
+ title("Recent Activity on #{project_to_s}")
435
+ activities.each do |activity|
436
+ show_activity(activity, tasks)
437
+ end
438
+ end
439
+
440
+
441
+ def help
442
+ if ARGV[0] && ARGV[0] != 'help'
443
+ message("Command #{ARGV[0]} not recognized. Showing help.")
444
+ end
445
+
446
+ title("Command line usage for pt #{PTT::VERSION}")
447
+ puts("ptt # show all available tasks")
448
+ puts("ptt todo # show all unscheduled tasks")
449
+ puts("ptt started <owner> # show all started stories")
450
+ puts("ptt create [title] <owner> <type> -m # create a new task (and include description ala git commit)")
451
+ puts("ptt show [id] # shows detailed info about a task")
452
+ puts("ptt tasks [id] # manage tasks of story")
453
+ puts("ptt open [id] # open a task in the browser")
454
+ puts("ptt assign [id] <owner> # assign owner")
455
+ puts("ptt comment [id] [comment] # add a comment")
456
+ puts("ptt label [id] [label] # add a label")
457
+ puts("ptt estimate [id] [0-3] # estimate a task in points scale")
458
+ puts("ptt start [id] # mark a task as started")
459
+ puts("ptt finish [id] # indicate you've finished a task")
460
+ puts("ptt deliver [id] # indicate the task is delivered");
461
+ puts("ptt accept [id] # mark a task as accepted")
462
+ puts("ptt reject [id] [reason] # mark a task as rejected, explaining why")
463
+ puts("ptt done [id] <0-3> <comment> # lazy mans finish task, opens, assigns to you, estimates, finish & delivers")
464
+ puts("ptt find [query] # looks in your tasks by title and presents it")
465
+ puts("ptt list [owner] or all # list all tasks for another pt user")
466
+ puts("ptt updates # shows number recent activity from your current project")
467
+ puts("ptt recent # shows stories you've recently shown or commented on with pt")
468
+ puts("")
469
+ puts("All commands can be run entirely without arguments for a wizard based UI. Otherwise [required] <optional>.")
470
+ puts("Anything that takes an id will also take the num (index) from the pt command.")
471
+ end
472
+
473
+ protected
474
+
475
+ def commands
476
+ (public_methods - Object.public_methods).map{ |c| c.to_sym}
477
+ end
478
+
479
+ # Config
480
+
481
+ def load_global_config
482
+
483
+ # skip global config if env vars are set
484
+ if ENV['PIVOTAL_EMAIL'] and ENV['PIVOTAL_API_KEY']
485
+ config = {
486
+ :email => ENV['PIVOTAL_EMAIL'],
487
+ :api_number => ENV['PIVOTAL_API_KEY']
488
+ }
489
+ return config
490
+ end
491
+
492
+ config = YAML.load(File.read(GLOBAL_CONFIG_PATH)) rescue {}
493
+ if config.empty?
494
+ message "I can't find info about your Pivotal Tracker account in #{GLOBAL_CONFIG_PATH}."
495
+ while !config[:api_number] do
496
+ config[:api_number] = ask "What is your token?"
497
+ end
498
+ congrats "Thanks!",
499
+ "Your API id is " + config[:api_number],
500
+ "I'm saving it in #{GLOBAL_CONFIG_PATH} so you don't have to log in again."
501
+ save_config(config, GLOBAL_CONFIG_PATH)
502
+ end
503
+ config
504
+ end
505
+
506
+ def get_local_config_path
507
+ # If the local config path does not exist, check to see if we're in a git repo
508
+ # And if so, try the top level of the checkout
509
+ if (!File.exist?(LOCAL_CONFIG_PATH) && system('git rev-parse 2> /dev/null'))
510
+ return `git rev-parse --show-toplevel`.chomp() + '/.pt'
511
+ else
512
+ return LOCAL_CONFIG_PATH
513
+ end
514
+ end
515
+
516
+ def load_local_config
517
+ check_local_config_path
518
+ config = YAML.load(File.read(get_local_config_path())) rescue {}
519
+
520
+ if ENV['PIVOTAL_PROJECT_ID']
521
+
522
+ config[:project_id] = ENV['PIVOTAL_PROJECT_ID']
523
+
524
+ project = @client.get_project(config[:project_id])
525
+ config[:project_name] = project.name
526
+
527
+ membership = @client.get_my_info
528
+ config[:user_name], config[:user_id], config[:user_initials] = membership.name, membership.id, membership.initials
529
+ save_config(config, get_local_config_path())
530
+
531
+ end
532
+
533
+ if config.empty?
534
+ message "I can't find info about this project in #{get_local_config_path()}"
535
+ projects = PTT::ProjectTable.new(@client.get_projects)
536
+ project = select("Please select the project for the current directory", projects)
537
+ config[:project_id], config[:project_name] = project.id, project.name
538
+ project = @client.get_project(project.id)
539
+ membership = @client.get_my_info
540
+ config[:user_name], config[:user_id], config[:user_initials] = membership.name, membership.id, membership.initials
541
+ congrats "Thanks! I'm saving this project's info",
542
+ "in #{get_local_config_path()}: remember to .gitignore it!"
543
+ save_config(config, get_local_config_path())
544
+ end
545
+ config
546
+ end
547
+
548
+ def check_local_config_path
549
+ if GLOBAL_CONFIG_PATH == get_local_config_path()
550
+ error("Please execute .pt inside your project directory and not in your home.")
551
+ exit
552
+ end
553
+ end
554
+
555
+ def save_config(config, path)
556
+ File.new(path, 'w') unless File.exists?(path)
557
+ File.open(path, 'w') {|f| f.write(config.to_yaml) }
558
+ end
559
+
560
+ # I/O
561
+
562
+ def split_lines(text)
563
+ text.respond_to?(:join) ? text.join("\n") : text
564
+ end
565
+
566
+ def title(*msg)
567
+ puts "\n#{split_lines(msg)}".bold
568
+ end
569
+
570
+ def congrats(*msg)
571
+ puts "\n#{split_lines(msg).green.bold}"
572
+ end
573
+
574
+ def message(*msg)
575
+ puts "\n#{split_lines(msg)}"
576
+ end
577
+
578
+ def compact_message(*msg)
579
+ puts "#{split_lines(msg)}"
580
+ end
581
+
582
+ def error(*msg)
583
+ puts "\n#{split_lines(msg).red.bold}"
584
+ end
585
+
586
+ def select(msg, table)
587
+ if table.length > 0
588
+ begin
589
+ table.print @global_config
590
+ row = ask "#{msg} (1-#{table.length}, 'q' to exit)"
591
+ quit if row == 'q'
592
+ selected = table[row]
593
+ error "Invalid selection, try again:" unless selected
594
+ end until selected
595
+ selected
596
+ else
597
+ table.print @global_config
598
+ message "Sorry, there are no options to select."
599
+ quit
600
+ end
601
+ end
602
+
603
+ def quit
604
+ message "bye!"
605
+ exit
606
+ end
607
+
608
+ def ask(msg)
609
+ @io.ask("#{msg.bold}")
610
+ end
611
+
612
+ def ask_secret(msg)
613
+ @io.ask("#{msg.bold}"){ |q| q.echo = '*' }
614
+ end
615
+
616
+ def user_s
617
+ "#{@local_config[:user_name]} (#{@local_config[:user_initials]})"
618
+ end
619
+
620
+ def project_to_s
621
+ "Project #{@local_config[:project_name].upcase}"
622
+ end
623
+
624
+ def task_type_or_nil query
625
+ if (["feature", "bug", "chore"].index query)
626
+ return query
627
+ end
628
+ nil
629
+ end
630
+
631
+ def task_by_id_or_pt_id id
632
+ if id < 1000
633
+ tasks = @client.get_my_work(@project, @local_config[:user_name])
634
+ table = PTT::TasksTable.new(tasks)
635
+ table[id]
636
+ else
637
+ @client.get_task_by_id @project, id
638
+ end
639
+ end
640
+
641
+ def find_task query
642
+ members = @client.get_members(@project)
643
+ members.each do | member |
644
+ if member.name.downcase.index query
645
+ return member.name
646
+ end
647
+ end
648
+ nil
649
+ end
650
+
651
+ def find_owner query
652
+ if query
653
+ member = @client.get_member(@project, query)
654
+ return member ? member.person : nil
655
+ end
656
+ nil
657
+ end
658
+
659
+ def show_task(task)
660
+ title task.name.green
661
+ estimation = [-1, nil].include?(task.estimate) ? "Unestimated" : "#{task.estimate} points"
662
+ message "#{task.current_state.capitalize} #{task.story_type} | #{estimation} | Req: #{task.requested_by.initials} |
663
+ Owners: #{task.owners.map(&:initials).join(',')} | Id: #{task.id}"
664
+
665
+ if (task.labels)
666
+ message "Labels: " + task.labels.map(&:name).join(', ')
667
+ end
668
+ message task.description unless task.description.nil? || task.description.empty?
669
+ message "View on pivotal: #{task.url}"
670
+
671
+ if task.tasks
672
+ title('tasks'.red)
673
+ task.tasks.each{ |t| compact_message "- #{t.complete ? "(v) " : "( )"} #{t.description}" }
674
+ end
675
+
676
+
677
+ task.comments.each do |n|
678
+ title('========================================='.red)
679
+ message ">> #{n.person.initials}: #{n.text} [#{n.file_attachment_ids.size}F]"
680
+ end
681
+ save_recent_task( task.id )
682
+ end
683
+
684
+
685
+ def show_activity(activity, tasks)
686
+ message("#{activity.message}")
687
+ end
688
+
689
+ def get_open_story_task_from_params(task)
690
+ title "Pending tasks for '#{task.name}'"
691
+ task_struct = Struct.new(:description, :position)
692
+
693
+ pending_tasks = [
694
+ task_struct.new('<< Add new task >>', -1)
695
+ ]
696
+
697
+ task.tasks.each{ |t| pending_tasks << t unless t.complete }
698
+ table = PTT::TodoTaskTable.new(pending_tasks)
699
+ select("Pick task to edit, 1 to add new task", table)
700
+ end
701
+
702
+ def get_task_from_params(prompt)
703
+ if @params[0]
704
+ task = task_by_id_or_pt_id(@params[0].to_i)
705
+ else
706
+ tasks = @client.get_all_stories(@project, @local_config[:user_name])
707
+ table = PTT::TasksTable.new(tasks)
708
+ task = select(prompt, table)
709
+ end
710
+ end
711
+
712
+ def edit_story_task(story_task)
713
+ action_class = Struct.new(:action, :key)
714
+
715
+ table = PTT::ActionTable.new([
716
+ action_class.new('Complete', :complete),
717
+ action_class.new('Delete', :delete),
718
+ action_class.new('Edit', :edit)
719
+ # Move?
720
+ ])
721
+ action_to_execute = select('What to do with todo?', table)
722
+
723
+ case action_to_execute.key
724
+ when :complete then
725
+ story_task.update(:complete => true)
726
+ congrats('Todo task completed!')
727
+ when :delete then
728
+ story_task.delete
729
+ congrats('Todo task removed')
730
+ when :edit then
731
+ new_description = ask('New task description')
732
+ story_task.update(:description => new_description)
733
+ congrats("Todo task changed to: \"#{story_task.description}\"")
734
+ end
735
+ end
736
+
737
+ def save_recent_task( task_id )
738
+ # save list of recently accessed tasks
739
+ unless (@local_config[:recent_tasks])
740
+ @local_config[:recent_tasks] = Array.new();
741
+ end
742
+ @local_config[:recent_tasks].unshift( task_id )
743
+ @local_config[:recent_tasks] = @local_config[:recent_tasks].uniq()
744
+ if @local_config[:recent_tasks].length > 10
745
+ @local_config[:recent_tasks].pop()
746
+ end
747
+ save_config( @local_config, get_local_config_path() )
748
+ end
749
+
750
+ end