tpope-pickler 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
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: