pivotal-slacker 1.6.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/bin/pivotal-slacker +244 -155
  2. data/lib/formatter.rb +5 -0
  3. metadata +2 -2
data/bin/pivotal-slacker CHANGED
@@ -6,6 +6,8 @@ require "pivotal-tracker"
6
6
  require "rainbow"
7
7
  require "action_view"
8
8
  require "launchy"
9
+ require "readline"
10
+ require "shellwords"
9
11
  require "pp"
10
12
  require "app_config"
11
13
  require "formatter"
@@ -14,9 +16,9 @@ include ActionView::Helpers::DateHelper
14
16
  # ----------------------------------------------------------
15
17
  # Load config
16
18
 
17
- config = nil
19
+ $config = nil
18
20
  begin
19
- config = AppConfig.load
21
+ $config = AppConfig.load
20
22
  rescue Exception => e
21
23
  puts e
22
24
  exit 1
@@ -25,8 +27,11 @@ end
25
27
  # ----------------------------------------------------------
26
28
  # Init
27
29
 
28
- PivotalTracker::Client.token = config.api_key
29
- project = PivotalTracker::Project.find config.project
30
+ PivotalTracker::Client.token = $config.api_key
31
+ $project = PivotalTracker::Project.find $config.project
32
+
33
+ # For `shell` mode, to retain menu number history.
34
+ $menu_story_mapping = {}
30
35
 
31
36
  # ----------------------------------------------------------
32
37
  # Main app code
@@ -34,17 +39,32 @@ project = PivotalTracker::Project.find config.project
34
39
  require "commander/import"
35
40
  require "pivotal-tracker"
36
41
 
37
- program :version, "1.5.0"
42
+ program :version, "1.6.0"
38
43
  program :description, "Pivotal Tracker command line client."
39
44
 
40
45
  # ----------------------------------------------------------
41
46
  # Support functionality
42
47
 
43
- def output_story_list opts={:project => nil, :states => [], :owned_by => nil}
48
+ # Supports looking up a story ID from $menu_story_mapping. If no mapping is found there, defaults
49
+ # to `id` as passed in.
50
+ def normalize_story_id id
51
+ mapped = $menu_story_mapping[id]
52
+
53
+ if mapped != nil
54
+ mapped
55
+ else
56
+ id
57
+ end
58
+ end
59
+
60
+ # Shared functionality for outputting lists of stories (e.g. for `mine`, `started`, etc.).
61
+ # Handles menuing functionality if opts[:menu] is `true`.
62
+ def output_story_list opts={:project => nil, :states => [], :owned_by => nil, :menu => false}
44
63
  puts "Looking for stories owned by #{opts[:owned_by]} in states: #{opts[:states]}".color("#444444")
45
64
 
46
65
  stories = opts[:project].stories.all(:owned_by => opts[:owned_by], :current_state => opts[:states])
47
66
  stories.sort! { |a, b| a.created_at <=> b.created_at }
67
+ menu_number = 1 # Only used when --menu specified.
48
68
  stories.each do |story|
49
69
  id = Formatter.story_id(story.id)
50
70
  name = Formatter.story_name(story.name)
@@ -52,195 +72,264 @@ def output_story_list opts={:project => nil, :states => [], :owned_by => nil}
52
72
  requested_by = Formatter.requested_by(story.requested_by)
53
73
  created_at = Formatter.time_ago(story.created_at)
54
74
  story_type = Formatter.story_type(story.story_type)
75
+
76
+ menu_output = nil
77
+ if opts[:menu]
78
+ $menu_story_mapping[menu_number] = story.id
79
+ menu_output = "#{menu_number}. "
80
+
81
+ menu_number += 1
82
+ end
55
83
 
56
- puts "[#{id}] #{name} ~ #{state} #{story_type}, from #{requested_by}, created #{created_at}"
84
+ puts "#{menu_output}[#{id}] #{name} ~ #{state} #{story_type}, from #{requested_by}, created #{created_at}"
57
85
  end
58
86
  end
59
87
 
60
88
  # ----------------------------------------------------------
61
89
  # Command-line interaction functionality
62
90
 
63
- command :mine do |c|
64
- c.syntax = "pivotal-slacker mine"
65
- c.description = "List your stories in Pivotal Tracker."
66
- c.example "List your open stories", "pivotal-slacker mine"
67
- c.action do |args, options|
68
- states = %w{unstarted started finished rejected}
69
- owned_by = config.user
91
+ def define_commands
92
+ command :mine do |c|
93
+ c.syntax = "pivotal-slacker mine"
94
+ c.description = "List your stories in Pivotal Tracker."
95
+ c.example "List your open stories", "pivotal-slacker mine"
96
+ c.option "--menu", "Print an easily-navigable menu for stories. Only useful for `shell` mode"
97
+ c.action do |args, options|
98
+ states = %w{unstarted started finished rejected}
99
+ owned_by = $config.user
70
100
 
71
- output_story_list :project => project, :states => states, :owned_by => owned_by
101
+ output_story_list :project => $project, :states => states, :owned_by => owned_by, :menu => options.menu
102
+ end
72
103
  end
73
- end
74
104
 
75
- command :accepted do |c|
76
- c.syntax = "pivotal-slacker accepted"
77
- c.description = "List your accepted stories in Pivotal Tracker."
78
- c.example "List your accepted stories", "pivotal-slacker accepted"
79
- c.action do |args, options|
80
- states = %w{accepted}
81
- owned_by = config.user
105
+ command :accepted do |c|
106
+ c.syntax = "pivotal-slacker accepted"
107
+ c.description = "List your accepted stories in Pivotal Tracker."
108
+ c.example "List your accepted stories", "pivotal-slacker accepted"
109
+ c.action do |args, options|
110
+ states = %w{accepted}
111
+ owned_by = $config.user
82
112
 
83
- output_story_list :project => project, :states => states, :owned_by => owned_by
113
+ output_story_list :project => $project, :states => states, :owned_by => owned_by
114
+ end
84
115
  end
85
- end
86
116
 
87
- command :started do |c|
88
- c.syntax = "pivotal-slacker started"
89
- c.description = "List your started stories in Pivotal Tracker."
90
- c.example "List your started stories", "pivotal-slacker started"
91
- c.action do |args, options|
92
- states = %w{started}
93
- owned_by = config.user
117
+ command :started do |c|
118
+ c.syntax = "pivotal-slacker started"
119
+ c.description = "List your started stories in Pivotal Tracker."
120
+ c.example "List your started stories", "pivotal-slacker started"
121
+ c.action do |args, options|
122
+ states = %w{started}
123
+ owned_by = $config.user
94
124
 
95
- output_story_list :project => project, :states => states, :owned_by => owned_by
125
+ output_story_list :project => $project, :states => states, :owned_by => owned_by
126
+ end
96
127
  end
97
- end
98
128
 
99
- command :unstarted do |c|
100
- c.syntax = "pivotal-slacker unstarted"
101
- c.description = "List your unstarted stories in Pivotal Tracker."
102
- c.example "List your unstarted stories", "pivotal-slacker unstarted"
103
- c.action do |args, options|
104
- states = %w{unstarted}
105
- owned_by = config.user
129
+ command :unstarted do |c|
130
+ c.syntax = "pivotal-slacker unstarted"
131
+ c.description = "List your unstarted stories in Pivotal Tracker."
132
+ c.example "List your unstarted stories", "pivotal-slacker unstarted"
133
+ c.action do |args, options|
134
+ states = %w{unstarted}
135
+ owned_by = $config.user
106
136
 
107
- output_story_list :project => project, :states => states, :owned_by => owned_by
137
+ output_story_list :project => $project, :states => states, :owned_by => owned_by
138
+ end
108
139
  end
109
- end
110
140
 
111
- command :open do |c|
112
- c.syntax = "pivotal-slacker open story_id"
113
- c.description = "Open a specific Pivotal Tracker story in the browser."
114
- c.example "Open a story with ID 123", "pivotal-slacker open 123"
115
- c.action do |args, options|
116
- story = project.stories.find(args[0].to_i)
117
- Launchy.open story.url
141
+ command :open do |c|
142
+ c.syntax = "pivotal-slacker open story_id"
143
+ c.description = "Open a specific Pivotal Tracker story in the browser."
144
+ c.example "Open a story with ID 123", "pivotal-slacker open 123"
145
+ c.action do |args, options|
146
+ story = $project.stories.find(args[0].to_i)
147
+ Launchy.open story.url
148
+ end
118
149
  end
119
- end
120
150
 
121
- command :show do |c|
122
- c.syntax = "pivotal-slacker show story_id"
123
- c.description = "Show the details of a given Pivotal Tracker story."
124
- c.example "Show a story with ID 123", "pivotal-slacker show 123"
125
- c.action do |args, options|
126
- story = project.stories.find(args[0].to_i)
127
- id = Formatter.story_id(story.id)
128
- state = Formatter.state(story.current_state)
129
- requested_by = Formatter.requested_by(story.requested_by)
130
- created_at = Formatter.time_ago(story.created_at)
131
- story_type = Formatter.story_type(story.story_type)
151
+ command :show do |c|
152
+ c.syntax = "pivotal-slacker show story_id"
153
+ c.description = "Show the details of a given Pivotal Tracker story."
154
+ c.example "Show a story with ID 123", "pivotal-slacker show 123"
155
+ c.action do |args, options|
156
+ story_id = normalize_story_id(args[0].to_i)
157
+ story = $project.stories.find(story_id)
158
+ id = Formatter.story_id(story.id)
159
+ state = Formatter.state(story.current_state)
160
+ requested_by = Formatter.requested_by(story.requested_by)
161
+ created_at = Formatter.time_ago("created #{story.created_at}")
162
+ story_type = Formatter.story_type(story.story_type)
132
163
 
133
- puts ""
134
- puts "[#{id}] #{Formatter.story_name(story.name, :heading => true)}"
135
- puts "#{state} #{story_type}, from #{requested_by}, created #{created_at}"
136
- puts "☛ #{story.url.color('#0066CC').underline}"
137
- puts ""
138
- puts Formatter.description(story.description)
139
- puts ""
140
-
141
- notes = story.notes.all.sort { |a, b| a.noted_at <=> b.noted_at }
142
- notes.each do |note|
143
- author = Formatter.note_author(note.author)
144
- noted_at = Formatter.time_ago(note.noted_at)
145
- note_text = Formatter.note_text(note.text)
146
- puts "#{author} (#{noted_at}): #{note_text}"
147
- end
148
-
149
- # Extra padding line after notes.
150
- if notes != nil and notes.size > 0
151
164
  puts ""
165
+ puts "[#{id}] #{Formatter.story_name(story.name, :heading => true)}"
166
+ puts "#{state} #{story_type}, from #{requested_by}, #{created_at}"
167
+ puts "☛ #{story.url.color('#0066CC').underline}"
168
+ puts ""
169
+ puts Formatter.description(story.description)
170
+ puts ""
171
+
172
+ notes = story.notes.all.sort { |a, b| a.noted_at <=> b.noted_at }
173
+ notes.each do |note|
174
+ author = Formatter.note_author(note.author)
175
+ noted_at = Formatter.time_ago(note.noted_at)
176
+ note_text = Formatter.note_text(note.text)
177
+ puts "#{author} (#{noted_at}): #{note_text}"
178
+ end
179
+
180
+ # Extra padding line after notes.
181
+ if notes != nil and notes.size > 0
182
+ puts ""
183
+ end
184
+
152
185
  end
186
+ end
187
+
188
+ command :start do |c|
189
+ c.syntax = "pivotal-slacker start story_id"
190
+ c.description = "Mark a given Pivotal Tracker story as \"started\"."
191
+ c.example "Start a story with ID 123", "pivotal-slacker start 123"
192
+ c.action do |args, options|
193
+ story_id = normalize_story_id(args[0].to_i)
153
194
 
195
+ story = $project.stories.find(story_id)
196
+ story.update :current_state => "started"
197
+
198
+ puts Formatter.story_action(Formatter.state("started"), story.id, story.name) + "."
199
+ end
154
200
  end
155
- end
156
201
 
157
- command :start do |c|
158
- c.syntax = "pivotal-slacker start story_id"
159
- c.description = "Mark a given Pivotal Tracker story as \"started\"."
160
- c.example "Start a story with ID 123", "pivotal-slacker start 123"
161
- c.action do |args, options|
162
- story_id = args[0].to_i
202
+ command :comment do |c|
203
+ c.syntax = "pivotal-slacker comment story_id comment"
204
+ c.description = "Comment on a given Pivotal Tracker story."
205
+ c.example "Comment on story 123", "pivotal-slacker comment 123 \"Due to my skills I will destroy this task.\""
206
+ c.action do |args, options|
207
+ story_id = normalize_story_id(args[0].to_i)
208
+ comment = args[1]
163
209
 
164
- story = project.stories.find(story_id)
165
- story.update :current_state => "started"
210
+ story = $project.stories.find(story_id)
211
+ story.notes.create :text => comment
166
212
 
167
- puts Formatter.story_action(Formatter.state("started"), story_id, story.name) + "."
213
+ comment = Formatter.note_text(comment)
214
+ puts "#{Formatter.story_action "Commented on", story.id, story.name}:"
215
+ puts "#{Formatter.note_author($config.user)}: #{comment}"
216
+ end
168
217
  end
169
- end
170
218
 
171
- command :comment do |c|
172
- c.syntax = "pivotal-slacker comment story_id comment"
173
- c.description = "Comment on a given Pivotal Tracker story."
174
- c.example "Comment on story 123", "pivotal-slacker comment 123 \"Due to my skills I will destroy this task.\""
175
- c.action do |args, options|
176
- story_id = args[0].to_i
177
- comment = args[1]
219
+ command :finish do |c|
220
+ c.syntax = "pivotal-slacker finish story_id"
221
+ c.description = "Mark a given Pivotal Tracker story as \"finished\"."
222
+ c.example "Finish a story with ID 123", "pivotal-slacker finish 123"
223
+ c.action do |args, options|
224
+ story_id = normalize_story_id(args[0].to_i)
178
225
 
179
- story = project.stories.find(story_id)
180
- story.notes.create :text => comment
226
+ story = $project.stories.find(story_id)
227
+ story.update :current_state => "finished"
181
228
 
182
- comment = Formatter.note_text(comment)
183
- puts "#{Formatter.story_action "Commented on", story_id, story.name}:"
184
- puts "#{Formatter.note_author(config.user)}: #{comment}"
229
+ puts Formatter.story_action(Formatter.state("finished"), story.id, story.name) + "."
230
+ end
185
231
  end
186
- end
187
232
 
188
- command :finish do |c|
189
- c.syntax = "pivotal-slacker finish story_id"
190
- c.description = "Mark a given Pivotal Tracker story as \"finished\"."
191
- c.example "Finish a story with ID 123", "pivotal-slacker finish 123"
192
- c.action do |args, options|
193
- story_id = args[0].to_i
233
+ command :create do |c|
234
+ c.syntax = "pivotal-slacker create [options]"
235
+ c.description = "Create a new task in Pivotal Tracker."
236
+ c.example "Create a new task", "pivotal-slacker create --chore --owner \"Jimmy Winkerbean\""
237
+ c.option "--feature", "Designate the story as a feature"
238
+ c.option "--bug", "Designate the story as a bug"
239
+ c.option "--chore", "Designate the story as a chore"
240
+ c.option "--release", "Designate the story as a release"
241
+ c.option "--owner STRING", String, "Assigns the story to a user"
242
+ c.option "--name STRING", String, "Name of story"
243
+ c.option "--description STRING", String, "Description of story"
244
+ c.action do |args, options|
245
+ options.default :chore => false, :owner => $config.user, :name => nil, :description => nil
194
246
 
195
- story = project.stories.find(story_id)
196
- story.update :current_state => "finished"
247
+ raise "--name is required" if options.name == nil
248
+ raise "--description is required" if options.description == nil
249
+
250
+ story_type = "feature" if options.feature
251
+ story_type = "bug" if options.bug
252
+ story_type = "chore" if options.chore
253
+ story_type = "release" if options.release
254
+
255
+ puts ""
256
+ puts Formatter.story_name(options.name, :heading => true)
257
+ puts "#{Formatter.attr_descriptor('type is', Formatter.story_type(story_type))}"
258
+ puts "#{Formatter.attr_descriptor('requested by', Formatter.owner($config.user))}"
259
+ puts "#{Formatter.attr_descriptor('owned by', Formatter.owner(options.owner))}"
260
+ puts "#{Formatter.attr_descriptor('description is', Formatter.description(options.description))}"
261
+ puts ""
197
262
 
198
- puts Formatter.story_action(Formatter.state("finished"), story_id, story.name) + "."
263
+ if agree "Really create? (y/n)"
264
+ story = $project.stories.create(
265
+ :name => options.name,
266
+ :story_type => story_type,
267
+ :description => options.description,
268
+ :requested_by => $config.user,
269
+ :owned_by => options.owner
270
+ )
271
+ puts Formatter.story_action("Created", story.id, story.name)
272
+ else
273
+ puts "Didn't create story."
274
+ end
275
+ end
199
276
  end
200
277
  end
201
278
 
202
- command :create do |c|
203
- c.syntax = "pivotal-slacker create [options]"
204
- c.description = "Create a new task in Pivotal Tracker."
205
- c.example "Create a new task", "pivotal-slacker create --chore --owner \"Jimmy Winkerbean\""
206
- c.option "--feature", "Designate the story as a feature"
207
- c.option "--bug", "Designate the story as a bug"
208
- c.option "--chore", "Designate the story as a chore"
209
- c.option "--release", "Designate the story as a release"
210
- c.option "--owner STRING", String, "Assigns the story to a user"
211
- c.option "--name STRING", String, "Name of story"
212
- c.option "--description STRING", String, "Description of story"
279
+ define_commands
280
+
281
+ command :shell do |c|
282
+ c.syntax = "pivotal-slacker shell"
283
+ c.description = %q{
284
+ The pivotal-slacker shell mode allows entry of commands with less effort.
285
+
286
+ In shell mode you can run commands as normal. You can also enter "menu numbers"
287
+ (listed to the left of stories in story listings) instead of story IDs when running
288
+ commands (e.g. `show 3` instead of `show 456123`).
289
+
290
+ Readline support is available, so you can go back through the history of
291
+ commands entered in the current session.
292
+ }
293
+ c.example "Enter the pivotal-slacker shell mode", "pivotal-slacker shell"
213
294
  c.action do |args, options|
214
- options.default :chore => false, :owner => config.user, :name => nil, :description => nil
215
-
216
- raise "--name is required" if options.name == nil
217
- raise "--description is required" if options.description == nil
218
-
219
- story_type = "feature" if options.feature
220
- story_type = "bug" if options.bug
221
- story_type = "chore" if options.chore
222
- story_type = "release" if options.release
223
-
224
- puts ""
225
- puts Formatter.story_name(options.name, :heading => true)
226
- puts "#{Formatter.attr_descriptor('type is', Formatter.story_type(story_type))}"
227
- puts "#{Formatter.attr_descriptor('requested by', Formatter.owner(config.user))}"
228
- puts "#{Formatter.attr_descriptor('owned by', Formatter.owner(options.owner))}"
229
- puts "#{Formatter.attr_descriptor('description is', Formatter.description(options.description))}"
230
- puts ""
231
-
232
- if agree "Really create? (y/n)"
233
- story = project.stories.create(
234
- :name => options.name,
235
- :story_type => story_type,
236
- :description => options.description,
237
- :requested_by => config.user,
238
- :owned_by => options.owner
239
- )
240
- puts Formatter.story_action("Created", story.id, story.name)
241
- else
242
- puts "Didn't create story."
243
- end
295
+ loop do
296
+ input = Readline::readline("pivotal-slacker> ")
297
+ break if input.nil? or input == "q" or input == "quit"
298
+ next if input.strip == ""
299
+
300
+ # Add input to Readinput history.
301
+ Readline::HISTORY.push(input)
302
+
303
+ # Turn output into an ARGV array.
304
+ argv = Shellwords.shellwords input
305
+
306
+ # Shift the command name off the ARGV array; Commander doesn't want it in there.
307
+ command_name = argv.shift
308
+ command = command(command_name.to_sym)
309
+
310
+ if command.nil?
311
+ puts Formatter.err "No such command, \"#{command_name}\""
312
+ next
313
+ end
314
+
315
+ # Tell all commands to be in "menu" mode (print list numbers, save list number history, etc.)
316
+ # Only do this if the command supports --menu.
317
+ command.options.each do |option|
318
+ args = option[:args]
319
+ if (not args.nil?) and args.include? "--menu"
320
+ argv.push "--menu"
321
+
322
+ # Reset the menu mapping, since the command we're about to call will update them,
323
+ # and we want this mapping to not have any stale data in it.
324
+ menu_story_mapping = {}
325
+ end
326
+ end
327
+
328
+ command.run *argv
329
+
330
+ # Commander doesn't act right when you try to run the same command more than once,
331
+ # so we have to redefine them after we use them.
332
+ define_commands
333
+ end
244
334
  end
245
335
  end
246
-
data/lib/formatter.rb CHANGED
@@ -62,4 +62,9 @@ class Formatter
62
62
  def self.description description
63
63
  description
64
64
  end
65
+
66
+ # Format error messages.
67
+ def self.err errmsg
68
+ errmsg.background('#990000').color(:white)
69
+ end
65
70
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pivotal-slacker
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.7.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-02-04 00:00:00.000000000 Z
12
+ date: 2013-02-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: pivotal-tracker