ptt 1.0.1

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.
@@ -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