tpope-pickler 0.0.4 → 0.0.5

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.
data/README.rdoc CHANGED
@@ -7,6 +7,7 @@ Synchronize user stories in Pivotal Tracker with Cucumber features.
7
7
  gem install tpope-pickler --source=http://gems.github.com
8
8
  echo "api_token: ..." > ~/.tracker.yml
9
9
  echo "project_id: ..." > ~/my/app/features/tracker.yml
10
+ pickler --help
10
11
 
11
12
  For details about the Pivotal Tracker API, including where to find your API
12
13
  token and project id, see http://www.pivotaltracker.com/help/api .
@@ -19,9 +20,7 @@ feature's body. Keep this in mind when entering stories into Pivotal Tracker.
19
20
 
20
21
  pickler pull
21
22
 
22
- Download all well formed stories to the features/ directory. Previously
23
- unseen stories will be given a numeric filename that you are encouraged to
24
- change.
23
+ Download all well formed stories to the features/ directory.
25
24
 
26
25
  pickler push
27
26
 
@@ -31,19 +30,21 @@ Upload all features with a tracker url in a comment on the first line.
31
30
 
32
31
  List all stories matching the given query.
33
32
 
34
- pickler show <id>
33
+ pickler start <story>
35
34
 
36
- Show details for the story referenced by the id.
35
+ Pull a given feature and change its state to started.
37
36
 
38
- pickler start <id> [basename]
37
+ pickler finish <story>
39
38
 
40
- Pull a given feature and change its state to started. If basename is given
41
- and no local file exists, features/basename.feature will be created in lieu
42
- of features/id.feature.
39
+ Push a given feature and change its state to finished.
43
40
 
44
- pickler finish <id>
41
+ pickler --help
45
42
 
46
- Push a given feature and change its state to finished.
43
+ Full list of commands.
44
+
45
+ pickler <command> --help
46
+
47
+ Further help for a given command.
47
48
 
48
49
  == Disclaimer
49
50
 
@@ -47,10 +47,8 @@ class Pickler
47
47
  end
48
48
 
49
49
  def pull(default = nil)
50
- body = "# http://www.pivotaltracker.com/story/show/#{id}\n" <<
51
- normalize_feature(story.to_s)
52
50
  filename = filename() || pickler.features_path("#{default||id}.feature")
53
- File.open(filename,'w') {|f| f.puts body}
51
+ File.open(filename,'w') {|f| f.puts story}
54
52
  @filename = filename
55
53
  end
56
54
 
@@ -84,24 +82,5 @@ class Pickler
84
82
  @story ||= @pickler.project.story(id) if id
85
83
  end
86
84
 
87
- protected
88
-
89
- def normalize_feature(body)
90
- return body unless ast = pickler.parser.parse(body)
91
- feature = ast.compile
92
- new = ''
93
- (feature.header.chomp << "\n").each_line do |l|
94
- new << ' ' unless new.empty?
95
- new << l.strip << "\n"
96
- end
97
- feature.scenarios.each do |scenario|
98
- new << "\n Scenario: #{scenario.name}\n"
99
- scenario.steps.each do |step|
100
- new << " #{step.keyword} #{step.name}\n"
101
- end
102
- end
103
- new
104
- end
105
-
106
85
  end
107
86
  end
@@ -1,15 +1,331 @@
1
+ require 'optparse'
2
+
1
3
  class Pickler
2
4
  class Runner
3
5
 
4
- attr_reader :argv
6
+ class Base
7
+ attr_reader :argv
8
+
9
+ def initialize(argv)
10
+ @argv = argv
11
+ @tty = $stdout.tty?
12
+ @opts = OptionParser.new
13
+ @opts.version = "0.0"
14
+ @opts.banner = "Usage: pickler #{self.class.command_name} #{self.class.banner_arguments}"
15
+ @opts.base.long["help"] = OptionParser::Switch::NoArgument.new do
16
+ help = @opts.help.chomp.chomp + "\n"
17
+ help += "\n#{self.class.description}" if self.class.description
18
+ puts help
19
+ @exit = 0
20
+ end
21
+ @opts.separator("")
22
+ end
23
+
24
+ def self.options
25
+ @options ||= []
26
+ end
27
+
28
+ def self.on(*args, &block)
29
+ options << args
30
+ define_method("option_#{args.object_id}", &block)
31
+ end
32
+
33
+ def self.banner_arguments(value = nil)
34
+ if value
35
+ @banner_arguments = value
36
+ else
37
+ @banner_arguments || (arity.zero? ? "" : "...")
38
+ end
39
+ end
40
+
41
+ def self.summary(value = nil)
42
+ if value
43
+ @summary = value
44
+ else
45
+ @summary
46
+ end
47
+ end
48
+
49
+ def self.description(value = nil)
50
+ if value
51
+ @description = value
52
+ else
53
+ @description || "#@summary."
54
+ end
55
+ end
56
+
57
+ def self.command_name
58
+ name.split('::').last.gsub(/(.)([A-Z])/) {"#$1-#$2"}.downcase
59
+ end
60
+
61
+ def self.method_name
62
+ command_name.gsub('-','_')
63
+ end
64
+
65
+ def self.process(&block)
66
+ define_method(:process, &block)
67
+ end
68
+
69
+ def self.arity
70
+ instance_method(:process).arity
71
+ end
72
+
73
+ def arity
74
+ self.class.arity
75
+ end
76
+
77
+ def pickler
78
+ @pickler ||= Pickler.new(Dir.getwd)
79
+ end
80
+
81
+ def abort(message)
82
+ raise Error, message
83
+ end
84
+
85
+ def too_many
86
+ abort "too many arguments"
87
+ end
88
+
89
+ def run
90
+ self.class.options.each do |arguments|
91
+ @opts.on(*arguments, &method("option_#{arguments.object_id}"))
92
+ end
93
+ begin
94
+ @opts.parse!(@argv)
95
+ rescue OptionParser::InvalidOption
96
+ abort $!.message
97
+ end
98
+ return @exit if @exit
99
+ minimum = arity < 0 ? -1 - arity : arity
100
+ if arity >= 0 && arity < @argv.size
101
+ too_many
102
+ elsif minimum > @argv.size
103
+ abort "not enough arguments"
104
+ end
105
+ process(*@argv)
106
+ end
107
+
108
+ def process(*argv)
109
+ pickler.send(self.class.method_name,*argv)
110
+ end
111
+
112
+ def color?
113
+ case pickler.config["color"]
114
+ when "always" then true
115
+ when "never" then false
116
+ else
117
+ @tty && RUBY_PLATFORM !~ /mswin|mingw/
118
+ end
119
+ end
120
+
121
+ def puts_summary(story)
122
+ summary = "%6d " % story.id
123
+ type = story.estimate || TYPE_SYMBOLS[story.story_type]
124
+ state = STATE_SYMBOLS[story.current_state]
125
+ if color?
126
+ summary << "\e[3#{STATE_COLORS[story.current_state]}m#{state}\e[00m "
127
+ summary << "\e[01;3#{TYPE_COLORS[story.story_type]}m#{type}\e[00m "
128
+ else
129
+ summary << "#{state} #{type} "
130
+ end
131
+ summary << story.name
132
+ puts summary
133
+ end
134
+
135
+ def paginated_output
136
+ stdout = $stdout
137
+ if @tty && pager = pickler.config["pager"]
138
+ # Modeled after git
139
+ ENV["LESS"] ||= "FRSX"
140
+ IO.popen(pager,"w") do |io|
141
+ $stdout = io
142
+ yield
143
+ end
144
+ else
145
+ yield
146
+ end
147
+ ensure
148
+ $stdout = stdout
149
+ end
5
150
 
6
- def pickler
7
- @pickler ||= Pickler.new(Dir.getwd)
151
+ end
152
+
153
+ def self.[](command)
154
+ klass_name = command.to_s.capitalize.gsub(/[-_](.)/) { $1.upcase }
155
+ if klass_name =~ /^[A-Z]\w*$/ && const_defined?(klass_name)
156
+ klass = const_get(klass_name)
157
+ if Class === klass && klass < Base
158
+ return klass
159
+ end
160
+ end
161
+ end
162
+
163
+ def self.commands
164
+ constants.map {|c| Runner.const_get(c)}.select {|c| Class === c && c < Runner::Base}.sort_by {|r| r.command_name}.uniq
165
+ end
166
+
167
+ def self.command(name, &block)
168
+ const_set(name.to_s.capitalize.gsub(/[-_](.)/) { $1.upcase },Class.new(Base,&block))
169
+ end
170
+
171
+ command :show do
172
+ banner_arguments "<story>"
173
+ summary "Show details for a story"
174
+
175
+ process do |*args|
176
+ case args.size
177
+ when 0
178
+ puts "#{pickler.project_id} #{pickler.project.name}"
179
+ when 1
180
+ story = pickler.project.story(args.first)
181
+ paginated_output do
182
+ puts story
183
+ end
184
+ else
185
+ too_many
186
+ end
187
+ end
188
+ end
189
+
190
+ command :search do
191
+ banner_arguments "[query]"
192
+ summary "List all stories matching a query"
193
+
194
+ def modifications
195
+ @modifications ||= {}
196
+ end
197
+ [:label, :type, :state].each do |o|
198
+ on "--#{o} #{o.to_s.upcase}" do |value|
199
+ modifications[o] = value
200
+ end
201
+ end
202
+ [:requester, :owner, :mywork].each do |o|
203
+ on "--#{o} USERNAME" do |value|
204
+ modifications[o] = value
205
+ end
206
+ end
207
+ on "--[no-]includedone", "include accepted stories" do |value|
208
+ modifications[:includedone] = value
209
+ end
210
+
211
+ attr_writer :current
212
+ on "-c", "--current", "filter results to current iteration" do |b|
213
+ self.current = b
214
+ end
215
+
216
+ process do |*argv|
217
+ argv << modifications unless modifications.empty?
218
+ if argv == [{:includedone => true}]
219
+ # Bypass the 200 search results limitation
220
+ stories = pickler.project.stories
221
+ else
222
+ stories = pickler.project.stories(*argv)
223
+ end
224
+ stories.reject! {|s| !s.current?} if argv.empty? || @current
225
+ paginated_output do
226
+ stories.each do |story|
227
+ puts_summary story
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ command :push do
234
+ banner_arguments "[story] ..."
235
+ summary "Upload stories"
236
+ description <<-EOF
237
+ Upload the given story or all features with a tracker url in a comment on the
238
+ first line.
239
+ EOF
240
+ end
241
+
242
+ command :pull do
243
+ banner_arguments "[story] ..."
244
+ summary "Download stories"
245
+ description <<-EOF
246
+ Download the given story or all well formed stories to the features/ directory.
247
+ Previously unseen stories will be given a numeric filename that you are
248
+ encouraged to change.
249
+ EOF
250
+ end
251
+
252
+ command :start do
253
+ banner_arguments "<story> [basename]"
254
+ summary "Pull a story and mark it started"
255
+ description <<-EOF
256
+ Pull a given story and change its state to started. If basename is given
257
+ and no local file exists, features/basename.feature will be created in lieu
258
+ of features/id.feature.
259
+ EOF
260
+
261
+ process do |story_id, *args|
262
+ pickler.start(story_id, args.first)
263
+ end
264
+ end
265
+
266
+ command :finish do
267
+ banner_arguments "<story>"
268
+ summary "Push a story and mark it finished"
269
+
270
+ process do |story_id|
271
+ super
272
+ end
273
+ end
274
+
275
+ command :deliver do
276
+ banner_arguments "[story] ..."
277
+ summary "Mark stories delivered"
278
+ on "--all-finished", "deliver all finished stories" do
279
+ @all = true
280
+ end
281
+ process do |*args|
282
+ if @all
283
+ pickler.deliver_all_finished_stories
284
+ end
285
+ args.each do |arg|
286
+ pickler.story(arg).transition!('delivered')
287
+ end
288
+ end
289
+ end
290
+
291
+ command :browse do
292
+ banner_arguments "[story]"
293
+ summary "Open a story in the web browser"
294
+ description <<-EOF
295
+ Open project or a story in the web browser.
296
+
297
+ Requires launchy (gem install launchy).
298
+ EOF
299
+
300
+ on "--dashboard" do
301
+ @special = "dashboard"
302
+ end
303
+ on "--faq" do
304
+ @special = "help"
305
+ end
306
+ on "--profile", "get your API Token here" do
307
+ @special = "profile"
308
+ end
309
+ on "--time", "not publicly available" do
310
+ @special = "time_shifts?project=#{pickler.project_id}"
311
+ end
312
+
313
+ process do |*args|
314
+ too_many if args.size > 1 || @special && args.first
315
+ if args.first
316
+ url = pickler.story(args.first).url
317
+ elsif @special
318
+ url = "http://www.pivotaltracker.com/#@special"
319
+ else
320
+ url = "http://www.pivotaltracker.com/projects/#{pickler.project_id}/stories"
321
+ end
322
+ require 'launchy'
323
+ Launchy.open(url)
324
+ end
8
325
  end
9
326
 
10
327
  def initialize(argv)
11
328
  @argv = argv
12
- @tty = $stdout.tty?
13
329
  end
14
330
 
15
331
  COLORS = {
@@ -58,73 +374,22 @@ class Pickler
58
374
  "bug" => "/"
59
375
  }
60
376
 
61
- def color?
62
- case pickler.config["color"]
63
- when "always" then true
64
- when "never" then false
65
- else
66
- @tty && RUBY_PLATFORM !~ /mswin|mingw/
67
- end
68
- end
69
-
70
- def puts_summary(story)
71
- summary = "%6d " % story.id
72
- type = story.estimate || TYPE_SYMBOLS[story.story_type]
73
- state = STATE_SYMBOLS[story.current_state]
74
- if color?
75
- summary << "\e[3#{STATE_COLORS[story.current_state]}m#{state}\e[00m "
76
- summary << "\e[01;3#{TYPE_COLORS[story.story_type]}m#{type}\e[00m "
77
- else
78
- summary << "#{state} #{type} "
79
- end
80
- summary << story.name
81
- puts summary
82
- end
83
-
84
- def paginated_output
85
- stdout = $stdout
86
- if @tty && pager = pickler.config["pager"]
87
- # Modeled after git
88
- ENV["LESS"] ||= "FRSX"
89
- IO.popen(pager,"w") do |io|
90
- $stdout = io
91
- yield
92
- end
93
- else
94
- yield
95
- end
96
- ensure
97
- $stdout = stdout
98
- end
99
-
100
377
  def run
101
- case first = argv.shift
102
- when 'show', /^\d+$/
103
- story = pickler.project.story(first == 'show' ? argv.shift : first)
104
- paginated_output do
105
- puts story
106
- end
107
- when 'search'
108
- stories = pickler.project.stories(*argv)
109
- stories.reject! {|s| %w(unscheduled unstarted accepted).include?(s.current_state)} if argv.empty?
110
- paginated_output do
111
- stories.each do |story|
112
- puts_summary story
113
- end
378
+ command = @argv.shift
379
+ if klass = self.class[command]
380
+ result = klass.new(@argv).run
381
+ exit result.respond_to?(:to_int) ? result.to_int : 0
382
+ elsif ['help', '--help', '-h', '', nil].include?(command)
383
+ puts "usage: pickler <command> [options] [arguments]"
384
+ puts
385
+ puts "Commands:"
386
+ self.class.commands.each do |command|
387
+ puts " %-19s %s" % [command.command_name, command.summary]
114
388
  end
115
- when 'push'
116
- pickler.push(*argv)
117
- when 'pull'
118
- pickler.pull(*argv)
119
- when 'start'
120
- pickler.start(argv.first,argv[1])
121
- when 'finish'
122
- pickler.finish(argv.first)
123
- when 'help', '--help', '-h', '', nil
124
- puts 'pickler commands: [show|start|finish] <id>, search <query>, push, pull'
389
+ puts
390
+ puts "Run pickler <command> --help for help with a given command"
125
391
  else
126
- $stderr.puts "pickler: unknown command #{first}"
127
- exit 1
392
+ raise Error, "Unknown pickler command #{command}"
128
393
  end
129
394
  rescue Pickler::Error
130
395
  $stderr.puts "#$!"
@@ -18,6 +18,10 @@ class Pickler
18
18
  start...finish
19
19
  end
20
20
 
21
+ def include?(date)
22
+ range.include?(date)
23
+ end
24
+
21
25
  def succ
22
26
  self.class.new(project, 'number' => number.succ.to_s, 'start' => @attributes['finish'], 'finish' => (finish + (finish - start)).strftime("%b %d, %Y"))
23
27
  end
@@ -11,7 +11,7 @@ class Pickler
11
11
  end
12
12
 
13
13
  def to_xml
14
- @attributes.to_xml(:root => 'note')
14
+ @attributes.to_xml(:dasherize => false, :root => 'note')
15
15
  end
16
16
 
17
17
  def inspect
@@ -28,6 +28,10 @@ class Pickler
28
28
  raise Pickler::Tracker::Error, Array(error).join("\n"), caller unless error == true
29
29
  end
30
30
 
31
+ def current?(as_of = Date.today)
32
+ iteration && iteration.include?(as_of)
33
+ end
34
+
31
35
  def complete?
32
36
  %w(finished delivered accepted).include?(current_state)
33
37
  end
@@ -84,14 +88,16 @@ class Pickler
84
88
 
85
89
  def comment!(body)
86
90
  raise ArgumentError if body.strip.empty? || body.size > 5000
87
- response = tracker.request_xml(:post, "#{resource_url}/notes",{:text => body}.to_xml(:root => 'note'))
91
+ response = tracker.request_xml(:post, "#{resource_url}/notes",{:text => body}.to_xml(:dasherize => false, :root => 'note'))
88
92
  Note.new(self, response["note"])
89
93
  end
90
94
 
91
95
  def to_xml
92
- hash = @attributes.except("id","url","iteration","notes","labels")
96
+ hash = @attributes.reject do |k,v|
97
+ !%w(current_state deadline description estimate name owned_by requested_by story_type).include?(k)
98
+ end
93
99
  hash["labels"] = Array(@attributes["labels"]).join(", ")
94
- hash.to_xml(:root => "story")
100
+ hash.to_xml(:dasherize => false, :root => "story")
95
101
  end
96
102
 
97
103
  def destroy
@@ -12,7 +12,8 @@ class Pickler
12
12
  attr_reader :token
13
13
 
14
14
  def initialize(token)
15
- require 'active_support'
15
+ require 'active_support/core_ext/blank'
16
+ require 'active_support/core_ext/hash'
16
17
  @token = token
17
18
  end
18
19
 
@@ -52,8 +53,11 @@ class Pickler
52
53
  end
53
54
 
54
55
  class Abstract
55
- def initialize(attributes)
56
- @attributes = (attributes || {}).stringify_keys
56
+ def initialize(attributes = {})
57
+ @attributes = {}
58
+ (attributes || {}).each do |k,v|
59
+ @attributes[k.to_s] = v
60
+ end
57
61
  yield self if block_given?
58
62
  end
59
63
 
@@ -78,7 +82,7 @@ class Pickler
78
82
  reader :id
79
83
 
80
84
  def to_xml(options = nil)
81
- @attributes.to_xml({:root => self.class.name.split('::').last.downcase}.merge(options||{}))
85
+ @attributes.to_xml({:dasherize => false, :root => self.class.name.split('::').last.downcase}.merge(options||{}))
82
86
  end
83
87
 
84
88
  end
data/lib/pickler.rb CHANGED
@@ -5,6 +5,10 @@ class Pickler
5
5
  class Error < RuntimeError
6
6
  end
7
7
 
8
+ autoload :Runner, 'pickler/runner'
9
+ autoload :Feature, 'pickler/feature'
10
+ autoload :Tracker, 'pickler/tracker'
11
+
8
12
  def self.config
9
13
  @config ||= {'api_token' => ENV["TRACKER_API_TOKEN"]}.merge(
10
14
  if File.exist?(path = File.expand_path('~/.tracker.yml'))
@@ -14,7 +18,6 @@ class Pickler
14
18
  end
15
19
 
16
20
  def self.run(argv)
17
- require 'pickler/runner'
18
21
  Runner.new(argv).run
19
22
  end
20
23
 
@@ -91,7 +94,7 @@ class Pickler
91
94
  def pull(*args)
92
95
  if args.empty?
93
96
  args = project.stories(scenario_word, :includedone => true).reject do |s|
94
- s.current_state == 'unstarted'
97
+ %(unscheduled unstarted).include?(s.current_state)
95
98
  end.select do |s|
96
99
  s.to_s =~ /^\s*#{Regexp.escape(scenario_word)}:/ && parser.parse(s.to_s)
97
100
  end
@@ -120,6 +123,3 @@ class Pickler
120
123
  protected
121
124
 
122
125
  end
123
-
124
- require 'pickler/feature'
125
- require 'pickler/tracker'
data/pickler.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "pickler"
3
- s.version = "0.0.4"
3
+ s.version = "0.0.5"
4
4
 
5
5
  s.summary = "PIvotal traCKer Liaison to cucumbER"
6
6
  s.description = "Synchronize between Cucumber and Pivotal Tracker"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tpope-pickler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Pope