tpope-pickler 0.0.5 → 0.0.6

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
@@ -2,6 +2,10 @@
2
2
 
3
3
  Synchronize user stories in Pivotal Tracker with Cucumber features.
4
4
 
5
+ If you aren't using Cucumber, you can still use pickler as a Pivotal Tracker
6
+ command line client, provided you humor it with a features/ directory
7
+ containing a tracker.yml file.
8
+
5
9
  == Getting started
6
10
 
7
11
  gem install tpope-pickler --source=http://gems.github.com
@@ -13,6 +13,9 @@ class Pickler
13
13
  @story = identifier
14
14
  @id = @story.id
15
15
 
16
+ when Integer
17
+ @id = identifier
18
+
16
19
  when /^#{URL_REGEX}$/, /^(\d+)$/
17
20
  @id = $1.to_i
18
21
 
@@ -54,7 +57,9 @@ class Pickler
54
57
 
55
58
  def start(default = nil)
56
59
  story.transition!("started") if story.startable?
57
- pull(default)
60
+ if filename || default
61
+ pull(default)
62
+ end
58
63
  end
59
64
 
60
65
  def push
@@ -64,9 +69,13 @@ class Pickler
64
69
  end
65
70
 
66
71
  def finish
67
- story.current_state = "finished" unless story.complete?
68
- story.to_s = local_body
69
- story.save
72
+ if filename
73
+ story.finish
74
+ story.to_s = local_body
75
+ story.save
76
+ else
77
+ story.finish!
78
+ end
70
79
  end
71
80
 
72
81
  def id
@@ -118,20 +118,48 @@ class Pickler
118
118
  end
119
119
  end
120
120
 
121
+ def colorize(code, string)
122
+ if color?
123
+ "\e[#{code}m#{string}\e[00m"
124
+ else
125
+ string.to_s
126
+ end
127
+ end
128
+
121
129
  def puts_summary(story)
122
130
  summary = "%6d " % story.id
123
131
  type = story.estimate || TYPE_SYMBOLS[story.story_type]
124
132
  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
133
+ summary << colorize("3#{STATE_COLORS[story.current_state]}", state) << ' '
134
+ summary << colorize("01;3#{TYPE_COLORS[story.story_type]}", type) << ' '
131
135
  summary << story.name
132
136
  puts summary
133
137
  end
134
138
 
139
+ def puts_full(story)
140
+ puts colorize("01;3#{TYPE_COLORS[story.story_type]}", story.name)
141
+ puts "Type: #{story.story_type}".rstrip
142
+ if story.story_type == "release"
143
+ puts "Deadline: #{story.deadline}".rstrip
144
+ else
145
+ puts "Estimate: #{story.estimate}".rstrip
146
+ end
147
+ puts "State: #{story.current_state}".rstrip
148
+ puts "Labels: #{story.labels.join(', ')}".rstrip
149
+ puts "Requester: #{story.requested_by}".rstrip
150
+ puts "Owner: #{story.owned_by}".rstrip
151
+ puts "URL: #{story.url}".rstrip
152
+ puts unless story.description.blank?
153
+ story.description_lines.each do |line|
154
+ puts " #{line}".rstrip
155
+ end
156
+ story.notes.each do |note|
157
+ puts
158
+ puts " #{colorize('01', note.author)} (#{note.date})"
159
+ puts *note.lines(72).map {|l| " #{l}".rstrip}
160
+ end
161
+ end
162
+
135
163
  def paginated_output
136
164
  stdout = $stdout
137
165
  if @tty && pager = pickler.config["pager"]
@@ -144,6 +172,7 @@ class Pickler
144
172
  else
145
173
  yield
146
174
  end
175
+ rescue Errno::EPIPE
147
176
  ensure
148
177
  $stdout = stdout
149
178
  end
@@ -177,9 +206,9 @@ class Pickler
177
206
  when 0
178
207
  puts "#{pickler.project_id} #{pickler.project.name}"
179
208
  when 1
180
- story = pickler.project.story(args.first)
209
+ story = pickler.story(args.first)
181
210
  paginated_output do
182
- puts story
211
+ puts_full story
183
212
  end
184
213
  else
185
214
  too_many
@@ -200,17 +229,28 @@ class Pickler
200
229
  end
201
230
  end
202
231
  [:requester, :owner, :mywork].each do |o|
203
- on "--#{o} USERNAME" do |value|
204
- modifications[o] = value
232
+ on "--#{o}[=USERNAME]" do |value|
233
+ modifications[o] = value || pickler.real_name
205
234
  end
206
235
  end
207
236
  on "--[no-]includedone", "include accepted stories" do |value|
208
237
  modifications[:includedone] = value
238
+ @iterations ||= []
239
+ @iterations << :done?
240
+ end
241
+
242
+ on "-b", "--backlog", "filter results to future iterations" do |c|
243
+ @iterations ||= []
244
+ @iterations << :backlog?
209
245
  end
210
246
 
211
- attr_writer :current
212
247
  on "-c", "--current", "filter results to current iteration" do |b|
213
- self.current = b
248
+ @iterations ||= []
249
+ @iterations << :current?
250
+ end
251
+
252
+ on "--[no-]full", "show full story, not a summary line" do |b|
253
+ @full = b
214
254
  end
215
255
 
216
256
  process do |*argv|
@@ -221,10 +261,19 @@ class Pickler
221
261
  else
222
262
  stories = pickler.project.stories(*argv)
223
263
  end
224
- stories.reject! {|s| !s.current?} if argv.empty? || @current
264
+ if @iterations && @iterations != [:done?]
265
+ stories.reject! {|s| !@iterations.any? {|i| s.send(i)}}
266
+ end
225
267
  paginated_output do
268
+ first = true
226
269
  stories.each do |story|
227
- puts_summary story
270
+ if @full
271
+ puts unless first
272
+ puts_full story
273
+ else
274
+ puts_summary story
275
+ end
276
+ first = false
228
277
  end
229
278
  end
230
279
  end
@@ -237,6 +286,13 @@ class Pickler
237
286
  Upload the given story or all features with a tracker url in a comment on the
238
287
  first line.
239
288
  EOF
289
+
290
+ process do |*args|
291
+ args.replace(pickler.local_features) if args.empty?
292
+ args.each do |arg|
293
+ pickler.feature(arg).push
294
+ end
295
+ end
240
296
  end
241
297
 
242
298
  command :pull do
@@ -247,6 +303,13 @@ Download the given story or all well formed stories to the features/ directory.
247
303
  Previously unseen stories will be given a numeric filename that you are
248
304
  encouraged to change.
249
305
  EOF
306
+
307
+ process do |*args|
308
+ args.replace(pickler.scenario_features) if args.empty?
309
+ args.each do |arg|
310
+ pickler.feature(arg).pull
311
+ end
312
+ end
250
313
  end
251
314
 
252
315
  command :start do
@@ -254,12 +317,11 @@ encouraged to change.
254
317
  summary "Pull a story and mark it started"
255
318
  description <<-EOF
256
319
  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.
320
+ and no local file exists, features/basename.feature will be created.
259
321
  EOF
260
322
 
261
- process do |story_id, *args|
262
- pickler.start(story_id, args.first)
323
+ process do |story, *args|
324
+ pickler.feature(story).start(args.first)
263
325
  end
264
326
  end
265
327
 
@@ -267,8 +329,8 @@ of features/id.feature.
267
329
  banner_arguments "<story>"
268
330
  summary "Push a story and mark it finished"
269
331
 
270
- process do |story_id|
271
- super
332
+ process do |story|
333
+ pickler.feature(story).finish
272
334
  end
273
335
  end
274
336
 
@@ -288,6 +350,34 @@ of features/id.feature.
288
350
  end
289
351
  end
290
352
 
353
+ command :unstart do
354
+ banner_arguments "[story] ..."
355
+ summary "Mark stories unstarted"
356
+ on "--all-started", "unstart all started stories" do
357
+ @all = true
358
+ end
359
+ process do |*args|
360
+ if @all
361
+ pickler.project.stories(:state => "started").each do |story|
362
+ story.transition!('unstarted')
363
+ end
364
+ end
365
+ args.each do |arg|
366
+ pickler.story(arg).transition!('unstarted')
367
+ end
368
+ end
369
+ end
370
+
371
+ command :unschedule do
372
+ banner_arguments "[story] ..."
373
+ summary "Move stories to icebox"
374
+ process do |*args|
375
+ args.each do |arg|
376
+ pickler.story(arg).transition!('unscheduled')
377
+ end
378
+ end
379
+ end
380
+
291
381
  command :browse do
292
382
  banner_arguments "[story]"
293
383
  summary "Open a story in the web browser"
@@ -324,6 +414,15 @@ Requires launchy (gem install launchy).
324
414
  end
325
415
  end
326
416
 
417
+ command :comment do
418
+ banner_arguments "<story> <paragraph> ..."
419
+ summary "Post a comment to a story"
420
+
421
+ process do |story, *paragraphs|
422
+ pickler.story(story).comment!(paragraphs.join("\n\n"))
423
+ end
424
+ end
425
+
327
426
  def initialize(argv)
328
427
  @argv = argv
329
428
  end
@@ -29,6 +29,10 @@ class Pickler
29
29
  def inspect
30
30
  "#<#{self.class.inspect}:#{number.inspect} (#{range.inspect})>"
31
31
  end
32
+
33
+ def to_s
34
+ "#{number} (#{start}...#{finish})"
35
+ end
32
36
  end
33
37
  end
34
38
  end
@@ -18,6 +18,10 @@ class Pickler
18
18
  "#<#{self.class.inspect}:#{id.inspect}, story_id: #{story.id.inspect}, date: #{date.inspect}, author: #{author.inspect}, text: #{text.inspect}>"
19
19
  end
20
20
 
21
+ def lines(width = 79)
22
+ text.scan(/(?:.{0,#{width}}|\S+?)(?:\s|$)/).map! {|line| line.strip}[0..-2]
23
+ end
24
+
21
25
  end
22
26
  end
23
27
  end
@@ -5,22 +5,27 @@ class Pickler
5
5
  TYPES = %w(bug feature chore release)
6
6
  STATES = %w(unscheduled unstarted started finished delivered rejected accepted)
7
7
 
8
- attr_reader :project, :iteration
9
- reader :url, :labels
8
+ attr_reader :project, :iteration, :labels
9
+ reader :url
10
10
  date_reader :created_at, :accepted_at, :deadline
11
11
  accessor :current_state, :name, :description, :owned_by, :requested_by, :story_type
12
12
 
13
13
  def initialize(project, attributes = {})
14
14
  @project = project
15
- @iteration = Iteration.new(project, attributes["iteration"]) if attributes["iteration"]
16
15
  super(attributes)
16
+ @iteration = Iteration.new(project, @attributes["iteration"]) if @attributes["iteration"]
17
+ @labels = normalize_labels(@attributes["labels"])
18
+ end
19
+
20
+ def labels=(value)
21
+ @labels = normalize_labels(value)
17
22
  end
18
23
 
19
24
  def transition!(state)
20
25
  raise Pickler::Tracker::Error, "Invalid state #{state}", caller unless STATES.include?(state)
21
26
  self.current_state = state
22
27
  if id
23
- xml = "<story><current-state>#{state}</current-state></story>"
28
+ xml = "<story><current_state>#{state}</current_state></story>"
24
29
  error = tracker.request_xml(:put, resource_url, xml).fetch("errors",{})["error"] || true
25
30
  else
26
31
  error = save
@@ -28,10 +33,33 @@ class Pickler
28
33
  raise Pickler::Tracker::Error, Array(error).join("\n"), caller unless error == true
29
34
  end
30
35
 
36
+ def finish
37
+ case story_type
38
+ when "bug", "feature"
39
+ self.current_state = "finished" unless complete?
40
+ when "chore", "release"
41
+ self.current_state = "accepted"
42
+ end
43
+ current_state
44
+ end
45
+
46
+ def finish!
47
+ transition!(finish)
48
+ end
49
+
50
+ def backlog?(as_of = Date.today)
51
+ iteration && iteration.start >= as_of
52
+ end
53
+
31
54
  def current?(as_of = Date.today)
32
55
  iteration && iteration.include?(as_of)
33
56
  end
34
57
 
58
+ # In a previous iteration
59
+ def done?(as_of = Date.today)
60
+ iteration && iteration.finish <= as_of
61
+ end
62
+
35
63
  def complete?
36
64
  %w(finished delivered accepted).include?(current_state)
37
65
  end
@@ -87,22 +115,27 @@ class Pickler
87
115
  end
88
116
 
89
117
  def comment!(body)
90
- raise ArgumentError if body.strip.empty? || body.size > 5000
91
118
  response = tracker.request_xml(:post, "#{resource_url}/notes",{:text => body}.to_xml(:dasherize => false, :root => 'note'))
92
- Note.new(self, response["note"])
119
+ if response["note"]
120
+ Note.new(self, response["note"])
121
+ else
122
+ raise Pickler::Tracker::Error, Array(response["errors"]["error"]).join("\n"), caller
123
+ end
93
124
  end
94
125
 
95
- def to_xml
126
+ def to_xml(force_labels = true)
96
127
  hash = @attributes.reject do |k,v|
97
128
  !%w(current_state deadline description estimate name owned_by requested_by story_type).include?(k)
98
129
  end
99
- hash["labels"] = Array(@attributes["labels"]).join(", ")
130
+ if force_labels || !id || normalize_labels(@attributes["labels"]) != labels
131
+ hash["labels"] = labels.join(", ")
132
+ end
100
133
  hash.to_xml(:dasherize => false, :root => "story")
101
134
  end
102
135
 
103
136
  def destroy
104
137
  if id
105
- response = tracker.request_xml(:delete, "/projects/#{project.id}/stories/#{id}", to_xml)
138
+ response = tracker.request_xml(:delete, "/projects/#{project.id}/stories/#{id}", "")
106
139
  raise Error, response["message"], caller if response["success"] != "true"
107
140
  @attributes["id"] = nil
108
141
  self
@@ -114,7 +147,7 @@ class Pickler
114
147
  end
115
148
 
116
149
  def save
117
- response = tracker.request_xml(id ? :put : :post, resource_url, to_xml)
150
+ response = tracker.request_xml(id ? :put : :post, resource_url, to_xml(false))
118
151
  if response["success"] == "true"
119
152
  initialize(project, response["story"])
120
153
  true
@@ -123,6 +156,11 @@ class Pickler
123
156
  end
124
157
  end
125
158
 
159
+ private
160
+ def normalize_labels(value)
161
+ Array(value).join(", ").strip.split(/\s*,\s*/)
162
+ end
163
+
126
164
  end
127
165
  end
128
166
  end
data/lib/pickler.rb CHANGED
@@ -47,6 +47,41 @@ class Pickler
47
47
  self.class.config.merge(@config)
48
48
  end
49
49
 
50
+ def real_name
51
+ config["real_name"] || (require 'etc'; Etc.getpwuid.gecos.split(',').first)
52
+ end
53
+
54
+ def new_story(attributes = {}, &block)
55
+ attributes = attributes.inject('requested_by' => real_name) do |h,(k,v)|
56
+ h.update(k.to_s => v)
57
+ end
58
+ project.new_story(attributes, &block)
59
+ end
60
+
61
+ def stories(*args)
62
+ project.stories(*args)
63
+ end
64
+
65
+ def name
66
+ project.name
67
+ end
68
+
69
+ def iteration_length
70
+ project.iteration_length
71
+ end
72
+
73
+ def point_scale
74
+ project.point_scale
75
+ end
76
+
77
+ def week_start_day
78
+ project.week_start_day
79
+ end
80
+
81
+ def deliver_all_finished_stories
82
+ project.deliver_all_finished_stories
83
+ end
84
+
50
85
  def parser
51
86
  require 'cucumber'
52
87
  require "cucumber/treetop_parser/feature_#@lang"
@@ -75,51 +110,26 @@ class Pickler
75
110
  Cucumber.language['scenario']
76
111
  end
77
112
 
78
- def features(*args)
79
- if args.any?
80
- args.map {|a| feature(a)}
81
- else
82
- Dir[features_path('**','*.feature')].map {|f|feature(f)}.select {|f|f.id}
113
+ def local_features
114
+ Dir[features_path('**','*.feature')].map {|f|feature(f)}.select {|f|f.id}
115
+ end
116
+
117
+ def scenario_features
118
+ project.stories(scenario_word, :includedone => true).reject do |s|
119
+ %(unscheduled unstarted).include?(s.current_state)
120
+ end.select do |s|
121
+ s.to_s =~ /^\s*#{Regexp.escape(scenario_word)}:/ && parser.parse(s.to_s)
83
122
  end
84
123
  end
85
124
 
86
125
  def feature(string)
87
- Feature.new(self,string)
126
+ string.kind_of?(Feature) ? string : Feature.new(self,string)
88
127
  end
89
128
 
90
129
  def story(string)
91
130
  feature(string).story
92
131
  end
93
132
 
94
- def pull(*args)
95
- if args.empty?
96
- args = project.stories(scenario_word, :includedone => true).reject do |s|
97
- %(unscheduled unstarted).include?(s.current_state)
98
- end.select do |s|
99
- s.to_s =~ /^\s*#{Regexp.escape(scenario_word)}:/ && parser.parse(s.to_s)
100
- end
101
- end
102
- args.each do |arg|
103
- feature(arg).pull
104
- end
105
- end
106
-
107
- def start(arg, default = nil)
108
- feature(arg).start(default)
109
- end
110
-
111
- def push(*args)
112
- features(*args).each do |feature|
113
- feature.push
114
- end
115
- end
116
-
117
- def finish(*args)
118
- features(*args).each do |feature|
119
- feature.finish
120
- end
121
- end
122
-
123
133
  protected
124
134
 
125
135
  end
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.5"
3
+ s.version = "0.0.6"
4
4
 
5
5
  s.summary = "PIvotal traCKer Liaison to cucumbER"
6
6
  s.description = "Synchronize between Cucumber and Pivotal Tracker"
@@ -25,4 +25,6 @@ Gem::Specification.new do |s|
25
25
  ]
26
26
  s.add_dependency("activesupport", [">= 2.0.0"])
27
27
  s.add_dependency("cucumber", [">= 0.1.9"])
28
+ s.add_dependency("builder")
29
+ s.add_dependency("xml-simple")
28
30
  end
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.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Pope
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-10-27 00:00:00 -07:00
12
+ date: 2008-12-15 00:00:00 -08:00
13
13
  default_executable: pickler
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -30,6 +30,24 @@ dependencies:
30
30
  - !ruby/object:Gem::Version
31
31
  version: 0.1.9
32
32
  version:
33
+ - !ruby/object:Gem::Dependency
34
+ name: builder
35
+ version_requirement:
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: "0"
41
+ version:
42
+ - !ruby/object:Gem::Dependency
43
+ name: xml-simple
44
+ version_requirement:
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
33
51
  description: Synchronize between Cucumber and Pivotal Tracker
34
52
  email: ruby@tpope.info
35
53
  executables: