pivotal-slacker 1.6.0 → 1.7.0

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