kosmas58-pickler 0.0.9.3 → 0.0.9.10

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -10,11 +10,13 @@ containing a tracker.yml file.
10
10
 
11
11
  gem install tpope-pickler --source=http://gems.github.com
12
12
  echo "api_token: ..." > ~/.tracker.yml
13
+ echo "username: ..." > ~/.tracker.yml
13
14
  echo "project_id: ..." > ~/my/app/features/tracker.yml
14
15
  echo "ssl: [true|false]" >> ~/my/app/features/tracker.yml
15
16
  pickler --help
16
17
 
17
18
  "ssl" defaults to false if not configured in the yml file.
19
+ "username" is the keyword to use in 'todo' search, your name or initials.
18
20
 
19
21
  For details about the Pivotal Tracker API, including where to find your API
20
22
  token and project id, see http://www.pivotaltracker.com/help/api .
@@ -23,6 +25,14 @@ The pull and push commands map the story's name into the "Feature: ..." line
23
25
  and the story's description with an additional two space indent into the
24
26
  feature's body. Keep this in mind when entering stories into Pivotal Tracker.
25
27
 
28
+ == Writing stories
29
+
30
+ In order for pickler to pick up your stories from tracker, they need to meet the following criteria:
31
+
32
+ * They must be wellformed cucumber stories
33
+ * They must contain the word 'Scenario' (or the equivalent in your localized stories)
34
+ * Their status must be set to 'started' at least
35
+
26
36
  == Usage
27
37
 
28
38
  pickler pull
@@ -45,6 +55,18 @@ Pull a given feature and change its state to started.
45
55
 
46
56
  Push a given feature and change its state to finished.
47
57
 
58
+ pickler todo
59
+
60
+ List all stories assigned to your username.
61
+
62
+ pickler bug Something
63
+
64
+ Opensi a bug with "Something" as title. Try "chore" too.
65
+
66
+ pickler bug
67
+
68
+ If you don`t provide a title, pickler will open your $EDITOR.
69
+
48
70
  pickler --help
49
71
 
50
72
  Full list of commands.
@@ -53,6 +75,15 @@ Full list of commands.
53
75
 
54
76
  Further help for a given command.
55
77
 
78
+ === Support of multiple languages
79
+
80
+ By setting the [lang] option in the "tracker.yml" file, you can set which language is used in the current project.
81
+ Default is cucumbers's default language "en".
82
+
83
+ Alias for your fingers sake.
84
+
85
+ piv <command>
86
+
56
87
  == Disclaimer
57
88
 
58
89
  No warranties, expressed or implied.
data/bin/pickler CHANGED
@@ -4,4 +4,5 @@ $:.unshift(File.join(File.dirname(File.dirname(__FILE__)),'lib'))
4
4
  begin; require 'rubygems'; rescue LoadError; end
5
5
  require 'pickler'
6
6
 
7
+ ARGV << "started" if ARGV.empty?
7
8
  Pickler.run(ARGV)
data/bin/piv ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # TODO: better way to "alias" a bin?
3
+ $:.unshift(File.join(File.dirname(File.dirname(__FILE__)),'lib'))
4
+ begin; require 'rubygems'; rescue LoadError; end
5
+ require 'pickler'
6
+
7
+ ARGV << "started" if ARGV.empty?
8
+ Pickler.run(ARGV)
data/lib/pickler.rb CHANGED
@@ -23,8 +23,7 @@ class Pickler
23
23
 
24
24
  attr_reader :directory
25
25
 
26
- def initialize(path = '.')
27
- #@lang = 'en'
26
+ def initialize(path = '.')
28
27
  @directory = File.expand_path(path)
29
28
  until File.directory?(File.join(@directory,'features'))
30
29
  if @directory == File.dirname(@directory)
@@ -44,7 +43,6 @@ class Pickler
44
43
 
45
44
  def config
46
45
  @config ||= File.exist?(config_file) && YAML.load_file(config_file) || {}
47
- @lang = @config["lang"] || "en"
48
46
  self.class.config.merge(@config)
49
47
  end
50
48
 
@@ -83,13 +81,6 @@ class Pickler
83
81
  project.deliver_all_finished_stories
84
82
  end
85
83
 
86
- def parser
87
- require 'cucumber'
88
- require "cucumber/treetop_parser/feature_#@lang"
89
- Cucumber.load_language(@lang)
90
- @parser ||= Cucumber::TreetopParser::FeatureParser.new
91
- end
92
-
93
84
  def project_id
94
85
  config["project_id"] || (self.class.config["projects"]||{})[File.basename(@directory)]
95
86
  end
@@ -106,22 +97,26 @@ class Pickler
106
97
  Tracker.new(token, ssl).project(id)
107
98
  end
108
99
  end
109
-
100
+
110
101
  def scenario_word
111
102
  parser
112
- Cucumber.language['scenario']
103
+ Cucumber.keyword_hash['scenario']
113
104
  end
114
105
 
106
+ def format
107
+ (config['format'] || :comment).to_sym
108
+ end
109
+
115
110
  def local_features
116
- Dir[features_path('**','*.feature')].map {|f|feature(f)}.select {|f|f.id}
111
+ Dir[features_path('**','*.feature')].map {|f|feature(f)}.select {|f|f.pushable?}
117
112
  end
118
-
113
+
119
114
  def scenario_features(includes)
120
115
  ignored_states = %w(unscheduled unstarted) - Array(includes)
121
- project.stories(scenario_word, :includedone => true).reject do |s|
116
+ project.stories(project.scenario_word, :includedone => true).reject do |s|
122
117
  ignored_states.include?(s.current_state)
123
118
  end.select do |s|
124
- s.to_s =~ /^\s*#{Regexp.escape(scenario_word)}:/ && parser.parse(s.to_s)
119
+ s.to_s =~ /^\s*#{Regexp.escape(project.scenario_word)}:/ && project.parser.parse(s.to_s)
125
120
  end
126
121
  end
127
122
 
@@ -133,6 +128,83 @@ class Pickler
133
128
  feature(string).story
134
129
  end
135
130
 
131
+ #
132
+ # Stolen from GHI - git://github.com/stephencelis/ghi.git
133
+ module Editor
134
+ def launch_editor(file)
135
+ system "#{editor} #{file.path}"
136
+ end
137
+
138
+ def gets_from_editor(type)
139
+ if windows?
140
+ warn "Windows fail => Please supply the message appended on the command"
141
+ exit 1
142
+ end
143
+ File.open message_path, "a+", &file_proc(type)
144
+ return @message.shift.strip, @message
145
+ end
146
+
147
+ def delete_message
148
+ File.delete message_path
149
+ rescue Errno::ENOENT, TypeError
150
+ nil
151
+ end
152
+
153
+ def message_path
154
+ File.join in_repo? ? gitdir : "/tmp", message_filename
155
+ end
156
+
157
+ def edit_format(type)
158
+ l = []
159
+ l << ""
160
+ l << "# Editing #{type}."
161
+ l << "# The first line will be the 'title', subsequent ones 'description'."
162
+ l << "# Lines beginning '#' will be ignored and empty messages discarded."
163
+ end
164
+
165
+ private
166
+
167
+ def editor
168
+ ENV["PICKLER_EDITOR"] || ENV["VISUAL"] || ENV["EDITOR"] || "vi"
169
+ end
170
+
171
+ def message_filename
172
+ @message_filename ||= "PICKLER_#{Time.now.to_i}_MESSAGE"
173
+ end
174
+
175
+ def file_proc(type)
176
+ lambda do |file|
177
+ file << edit_format(type).join("\n") if File.zero? file.path
178
+ file.rewind
179
+ launch_editor file
180
+ @message = File.readlines(file.path).find_all { |l| !l.match(/^#/) }
181
+
182
+ if @message.to_s =~ /\A\s*\Z/
183
+ raise Pickler::Error, "Aborted."
184
+ end
185
+ end
186
+ end
187
+
188
+ def gitdir
189
+ @gitdir ||= `git rev-parse --git-dir 2>/dev/null`.chomp
190
+ end
191
+
192
+ def in_repo?
193
+ !gitdir.empty?
194
+ end
195
+
196
+ def puts(*args)
197
+ rescue NoMethodError
198
+ # Do nothing.
199
+ ensure
200
+ $stdout.puts(*args)
201
+ end
202
+
203
+ def windows?
204
+ RUBY_PLATFORM.include?("mswin")
205
+ end
206
+ end
207
+
136
208
  protected
137
209
 
138
210
  end
@@ -1,6 +1,6 @@
1
1
  class Pickler
2
2
  class Feature
3
- URL_REGEX = %r{\bhttp://www\.pivotaltracker\.com/\S*/(\d+)\b}
3
+ URL_REGEX = %r{\bhttps?://www\.pivotaltracker\.com/\S*/(\d+)\b}
4
4
  attr_reader :pickler
5
5
 
6
6
  def initialize(pickler, identifier)
@@ -39,20 +39,20 @@ class Pickler
39
39
  def filename
40
40
  unless defined?(@filename)
41
41
  @filename = Dir[pickler.features_path("**","*.feature")].detect do |f|
42
- File.read(f)[/#\s*#{URL_REGEX}/,1].to_i == @id
42
+ File.read(f)[/(?:#\s*|@[[:punct:]]?)#{URL_REGEX}/,1].to_i == @id
43
43
  end
44
44
  end
45
45
  @filename
46
46
  end
47
47
 
48
48
  def to_s
49
- local_body || story.to_s
49
+ local_body || story.to_s(pickler.format)
50
50
  end
51
51
 
52
52
  def pull(default = nil)
53
53
  filename = filename() || pickler.features_path("#{default||id}.feature")
54
54
  story = story() # force the read into local_body before File.open below blows it away
55
- File.open(filename,'w') {|f| f.puts story}
55
+ File.open(filename,'w') {|f| f.puts story.to_s(pickler.format)}
56
56
  @filename = filename
57
57
  end
58
58
 
@@ -63,11 +63,26 @@ class Pickler
63
63
  end
64
64
  end
65
65
 
66
+ def pushable?
67
+ id || local_body =~ %r{\A(?:#\s*|@[[:punct:]]?(?:http://www\.pivotaltracker\.com/story/new)?[[:punct:]]?(?:\s+@\S+)*\s*)\n[[:upper:]][[:lower:]]+:} ? true : false
68
+ end
69
+
66
70
  def push
67
- return if story.to_s == local_body.to_s
68
- story.to_s = local_body
69
- story.save
70
- self
71
+ body = local_body
72
+ if story
73
+ return if story.to_s(pickler.format) == body.to_s
74
+ story.to_s = body
75
+ story.save!
76
+ else
77
+ unless pushable?
78
+ raise Error, "To create a new story, make the first line an empty comment"
79
+ end
80
+ story = pickler.new_story
81
+ story.to_s = body
82
+ @story = story.save!
83
+ body.sub!(/\A(?:#.*\n)?/,"# #{story.url}\n")
84
+ File.open(filename,'w') {|f| f.write body}
85
+ end
71
86
  end
72
87
 
73
88
  def finish
@@ -82,7 +97,7 @@ class Pickler
82
97
 
83
98
  def id
84
99
  unless defined?(@id)
85
- @id = if id = local_body.to_s[/#\s*#{URL_REGEX}/,1]
100
+ @id = if id = local_body.to_s[/(?:#\s*|@[[:punct:]]?)#{URL_REGEX}/,1]
86
101
  id.to_i
87
102
  end
88
103
  end
@@ -1,9 +1,10 @@
1
+ # -*- coding: utf-8 -*-
1
2
  require 'optparse'
2
3
 
3
4
  class Pickler
4
5
  class Runner
5
-
6
6
  class Base
7
+ include Editor
7
8
  attr_reader :argv
8
9
 
9
10
  def initialize(argv)
@@ -127,13 +128,15 @@ class Pickler
127
128
  end
128
129
 
129
130
  def puts_summary(story)
130
- summary = "%6d " % story.id
131
+ summary = "%6d " % story.id
131
132
  type = story.estimate || TYPE_SYMBOLS[story.story_type]
132
- state = STATE_SYMBOLS[story.current_state]
133
- summary << colorize("3#{STATE_COLORS[story.current_state]}", state) << ' '
134
133
  summary << colorize("01;3#{TYPE_COLORS[story.story_type]}", type) << ' '
135
- summary << story.name
136
- puts summary
134
+ summary << story.name << ' '
135
+ if story.owned_by
136
+ initials = story.owned_by.split(" ").map { |o| o[0].chr }
137
+ summary << colorize("01;30", "(#{initials})")
138
+ end
139
+ puts " " + summary
137
140
  end
138
141
 
139
142
  def puts_full(story)
@@ -201,14 +204,28 @@ class Pickler
201
204
  banner_arguments "<story>"
202
205
  summary "Show details for a story"
203
206
 
207
+ on "--full", "default format" do
208
+ @format = :full
209
+ end
210
+
211
+ on "--raw", "same as the .feature" do
212
+ @format = :raw
213
+ end
214
+
204
215
  process do |*args|
205
216
  case args.size
206
217
  when 0
207
218
  puts "#{pickler.project_id} #{pickler.project.name}"
208
219
  when 1
209
- story = pickler.story(args.first)
210
- paginated_output do
211
- puts_full story
220
+ feature = pickler.feature(args.first)
221
+ story = feature.story
222
+ case @format
223
+ when :raw
224
+ puts feature.story.to_s(pickler.format) if feature.story
225
+ else
226
+ paginated_output do
227
+ puts_full feature.story
228
+ end
212
229
  end
213
230
  else
214
231
  too_many
@@ -266,25 +283,43 @@ class Pickler
266
283
  end
267
284
  paginated_output do
268
285
  first = true
269
- stories.each do |story|
270
- if @full
271
- puts unless first
272
- puts_full story
273
- else
274
- puts_summary story
286
+ stories.group_by(&:current_state).each do |state, state_stories|
287
+ state_icon = STATE_SYMBOLS[state]
288
+ print colorize("01;3#{STATE_COLORS[state]}", "= #{state.capitalize} #{state_icon}")
289
+ sum = state_stories.sum {|s| s.estimate || 0 }
290
+ len = state_stories.length
291
+ puts colorize("01;30", " (#{sum} points, #{len} #{len > 1 ? 'stories' : 'story' })")
292
+ for story in state_stories
293
+ if @full
294
+ puts unless first
295
+ puts_full story
296
+ else
297
+ puts_summary story
298
+ end
299
+ first = false
275
300
  end
276
- first = false
301
+ puts
277
302
  end
278
303
  end
279
304
  end
280
305
  end
281
306
 
307
+ command :list do
308
+ banner_arguments "[query]"
309
+ summary "Same as search"
310
+
311
+ process do |*argv|
312
+ Pickler.run([:search, argv])
313
+ end
314
+ end
315
+
282
316
  command :push do
283
317
  banner_arguments "[story] ..."
284
318
  summary "Upload stories"
285
319
  description <<-EOF
286
320
  Upload the given story or all features with a tracker url in a comment on the
287
- first line.
321
+ first line. Features with a blank comment in the first line will created as
322
+ new stories.
288
323
  EOF
289
324
 
290
325
  process do |*args|
@@ -435,6 +470,63 @@ Requires launchy (gem install launchy).
435
470
  end
436
471
  end
437
472
 
473
+ command :open do
474
+ banner_arguments "<type> <story> <paragraph> ..."
475
+ summary "Create a new story"
476
+
477
+ process do |type, title, *paragraphs|
478
+ st = Pickler::Tracker::Story.new(pickler.project)
479
+ st.story_type = type
480
+ st.name = title
481
+ st.description = paragraphs.join
482
+ puts st.save ? "#{type.capitalize} created." : "Problems..."
483
+ end
484
+ end
485
+
486
+ command :bug do
487
+ banner_arguments "<story> <paragraph> ..."
488
+ summary "Create a new bug"
489
+
490
+ process do |*paragraphs|
491
+ title, paragraphs = gets_from_editor("bug") unless title = paragraphs.delete_at(0)
492
+ Pickler.run ["open", "bug", title, *paragraphs]
493
+ end
494
+ end
495
+
496
+ command :chore do
497
+ banner_arguments "<story> <paragraph> ..."
498
+ summary "Create a new chore"
499
+
500
+ process do |*paragraphs|
501
+ title, paragraphs = gets_from_editor("chore") unless title = paragraphs.delete_at(0)
502
+ Pickler.run ["open", "chore", title, *paragraphs]
503
+ end
504
+ end
505
+
506
+ command :started do
507
+ summary "Started stories."
508
+ description <<-EOF
509
+ Show only started stories.
510
+ EOF
511
+ process do
512
+ Pickler.run(["search", "-c"])
513
+ end
514
+ end
515
+
516
+ command :todo do
517
+ summary "Show my stories."
518
+ description <<-EOF
519
+ Show only stories assigned to yourself. Note: You must set 'username'
520
+ on ~/.tracker.yml.
521
+ EOF
522
+ process do
523
+ unless username = pickler.config['username']
524
+ raise Error, "echo username: <your-initials> ~/.tracker.yml"
525
+ end
526
+ Pickler.run(["search", "--mywork=#{username}"])
527
+ end
528
+ end
529
+
438
530
  def initialize(argv)
439
531
  @argv = argv
440
532
  end
@@ -453,36 +545,36 @@ Requires launchy (gem install launchy).
453
545
  STATE_COLORS = {
454
546
  nil => COLORS[:black],
455
547
  "rejected" => COLORS[:red],
456
- "accepted" => COLORS[:green],
548
+ "accepted" => COLORS[:blue],
457
549
  "delivered" => COLORS[:yellow],
458
550
  "unscheduled" => COLORS[:white],
459
- "started" => COLORS[:magenta],
551
+ "started" => COLORS[:green],
460
552
  "finished" => COLORS[:cyan],
461
- "unstarted" => COLORS[:blue]
553
+ "unstarted" => COLORS[:magenta]
462
554
  }
463
555
 
464
556
  STATE_SYMBOLS = {
465
- "unscheduled" => " ",
466
- "unstarted" => ":|",
467
- "started" => ":/",
468
- "finished" => ":)",
469
- "delivered" => ";)",
470
- "rejected" => ":(",
471
- "accepted" => ":D"
557
+ "unscheduled" => "..",
558
+ "unstarted" => "=|",
559
+ "started" => "=/",
560
+ "finished" => "=)",
561
+ "delivered" => "=D",
562
+ "rejected" => "=(",
563
+ "accepted" => "xD"
472
564
  }
473
565
 
474
566
  TYPE_COLORS = {
475
567
  'chore' => COLORS[:blue],
476
- 'feature' => COLORS[:magenta],
568
+ 'feature' => COLORS[:green],
477
569
  'bug' => COLORS[:red],
478
570
  'release' => COLORS[:cyan]
479
571
  }
480
572
 
481
573
  TYPE_SYMBOLS = {
482
574
  "feature" => "*",
483
- "chore" => "%",
484
- "release" => "!",
485
- "bug" => "/"
575
+ "chore" => "Ø",
576
+ "release" => "",
577
+ "bug" => "¤"
486
578
  }
487
579
 
488
580
  def run
@@ -1,20 +1,28 @@
1
1
  require 'date'
2
+ require 'cgi'
2
3
 
3
4
  class Pickler
4
5
  class Tracker
5
6
 
6
7
  ADDRESS = 'www.pivotaltracker.com'
7
- BASE_PATH = '/services/v1'
8
+ BASE_PATH = '/services/v2'
8
9
  SEARCH_KEYS = %w(label type state requester owner mywork id includedone)
9
10
 
10
11
  class Error < Pickler::Error; end
11
12
 
12
13
  attr_reader :token
13
14
 
14
- def initialize(token, ssl = false)
15
+ def initialize(token, ssl = false, language="en")
15
16
  require 'active_support'
17
+ # Use nokogiri when avaiable
18
+ begin
19
+ require 'nokogiri'
20
+ ActiveSupport::XmlMini.backend = 'Nokogiri'
21
+ rescue MissingSourceFile
22
+ end
16
23
  @token = token
17
24
  @ssl = ssl
25
+ @lang = language
18
26
  end
19
27
 
20
28
  def ssl?
@@ -50,23 +58,19 @@ class Pickler
50
58
  def request_xml(method, path, *args)
51
59
  response = request(method,path,*args)
52
60
  raise response.inspect if response["Content-type"].split(/; */).first != "application/xml"
53
- Hash.from_xml(response.body)["response"]
61
+ hash = Hash.from_xml(response.body)
62
+ if hash["message"] && (response.code.to_i >= 400 || hash["success"] == "false")
63
+ raise Error, hash["message"], caller
64
+ end
65
+ hash
54
66
  end
55
67
 
56
68
  def get_xml(path)
57
- response = request_xml(:get, path)
58
- unless response["success"] == "true"
59
- if response["message"]
60
- raise Error, response["message"], caller
61
- else
62
- raise "#{path}: #{response.inspect}"
63
- end
64
- end
65
- response
69
+ request_xml(:get, path)
66
70
  end
67
71
 
68
72
  def project(id)
69
- Project.new(self,get_xml("/projects/#{id}")["project"].merge("id" => id.to_i))
73
+ Project.new(self,get_xml("/projects/#{id}")["project"])
70
74
  end
71
75
 
72
76
  class Abstract
@@ -86,7 +90,10 @@ class Pickler
86
90
 
87
91
  def self.date_reader(*methods)
88
92
  methods.each do |method|
89
- define_method(method) { value = @attributes[method.to_s] and Date.parse(value) }
93
+ define_method(method) do
94
+ value = @attributes[method.to_s]
95
+ value.kind_of?(String) ? Date.parse(value) : value
96
+ end
90
97
  end
91
98
  end
92
99
 
@@ -96,7 +103,10 @@ class Pickler
96
103
  define_method("#{method}=") { |v| @attributes[method.to_s] = v }
97
104
  end
98
105
  end
99
- reader :id
106
+
107
+ def id
108
+ id = @attributes['id'] and Integer(id)
109
+ end
100
110
 
101
111
  def to_xml(options = nil)
102
112
  @attributes.to_xml({:dasherize => false, :root => self.class.name.split('::').last.downcase}.merge(options||{}))
@@ -2,13 +2,20 @@ class Pickler
2
2
  class Tracker
3
3
  class Iteration < Abstract
4
4
  attr_reader :project
5
- date_reader :start, :finish
6
5
 
7
6
  def initialize(project, attributes = {})
8
7
  @project = project
9
8
  super(attributes)
10
9
  end
11
10
 
11
+ def start
12
+ Date.parse(@attributes['start'].to_s)
13
+ end
14
+
15
+ def finish
16
+ Date.parse(@attributes['finish'].to_s)
17
+ end
18
+
12
19
  def number
13
20
  @attributes['number'].to_i
14
21
  end
@@ -23,7 +30,7 @@ class Pickler
23
30
  end
24
31
 
25
32
  def succ
26
- self.class.new(project, 'number' => number.succ.to_s, 'start' => @attributes['finish'], 'finish' => (finish + (finish - start)).strftime("%b %d, %Y"))
33
+ self.class.new(project, 'number' => number.succ.to_s, 'start' => @attributes['finish'], 'finish' => (finish + (finish - start)))
27
34
  end
28
35
 
29
36
  def inspect
@@ -3,7 +3,11 @@ class Pickler
3
3
  class Note < Abstract
4
4
  attr_reader :story
5
5
  reader :text, :author
6
- date_reader :date
6
+ date_reader :noted_at
7
+
8
+ def date
9
+ noted_at && Date.new(noted_at.year, noted_at.mon, noted_at.day)
10
+ end
7
11
 
8
12
  def initialize(story, attributes = {})
9
13
  @story = story
@@ -20,7 +20,7 @@ class Pickler
20
20
  path = "/projects/#{id}/stories"
21
21
  path << "?filter=#{CGI.escape(filter)}" if filter
22
22
  response = tracker.get_xml(path)
23
- [response["stories"]["story"]].flatten.compact.map {|s| Story.new(self,s)}
23
+ [response["stories"]].flatten.compact.map {|s| Story.new(self,s)}
24
24
  end
25
25
 
26
26
  def new_story(attributes = {}, &block)
@@ -29,6 +29,18 @@ class Pickler
29
29
 
30
30
  def deliver_all_finished_stories
31
31
  request_xml(:put,"/projects/#{id}/stories_deliver_all_finished")
32
+ end
33
+
34
+ def parser
35
+ require 'cucumber'
36
+ require "cucumber/treetop_parser/feature_#@tracker.lang"
37
+ Cucumber.load_language(@tracker.lang)
38
+ @parser ||= Cucumber::TreetopParser::FeatureParser.new
39
+ end
40
+
41
+ def scenario_word
42
+ parser
43
+ Cucumber.language['scenario']
32
44
  end
33
45
 
34
46
  private
@@ -5,7 +5,7 @@ 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, :labels
8
+ attr_reader :project, :labels
9
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
@@ -17,6 +17,13 @@ class Pickler
17
17
  @labels = normalize_labels(@attributes["labels"])
18
18
  end
19
19
 
20
+ def iteration
21
+ unless current_state == 'unscheduled' || defined?(@iteration)
22
+ @iteration = project.stories(:id => id, :includedone => true).first.iteration
23
+ end
24
+ @iteration
25
+ end
26
+
20
27
  def labels=(value)
21
28
  @labels = normalize_labels(value)
22
29
  end
@@ -72,16 +79,28 @@ class Pickler
72
79
  project.tracker
73
80
  end
74
81
 
75
- def to_s
76
- to_s = "# #{url}\n#{story_type.capitalize}: #{name}\n"
82
+ def to_s(format = :comment)
83
+ to_s = "#{header(format)}\n#{story_type.capitalize}: #{name}\n"
77
84
  description_lines.each do |line|
78
85
  to_s << " #{line}".rstrip << "\n"
79
86
  end
80
87
  to_s
81
88
  end
82
89
 
90
+ def header(format = :comment)
91
+ case format
92
+ when :tag
93
+ "@#{url}#{labels.map {|l| " @#{l.tr('_,',' _')}"}.join}"
94
+ else
95
+ "# #{url}"
96
+ end
97
+ end
98
+
83
99
  def to_s=(body)
84
- body = body.sub(/\A# .*\n/,'')
100
+ if body =~ /\A@https?\b\S*(\s+@\S+)*\s*$/
101
+ self.labels = body[/\A@.*/].split(/\s+/)[1..-1].map {|l| l[1..-1].tr(' _','_,')}
102
+ end
103
+ body = body.sub(/\A(?:[@#].*\n)+/,'')
85
104
  if body =~ /\A(\w+): (.*)/
86
105
  self.story_type = $1.downcase
87
106
  self.name = $2
@@ -136,7 +155,7 @@ class Pickler
136
155
  def destroy
137
156
  if id
138
157
  response = tracker.request_xml(:delete, "/projects/#{project.id}/stories/#{id}", "")
139
- raise Error, response["message"], caller if response["success"] != "true"
158
+ raise Error, response["message"], caller if response["message"]
140
159
  @attributes["id"] = nil
141
160
  self
142
161
  end
@@ -148,12 +167,24 @@ class Pickler
148
167
 
149
168
  def save
150
169
  response = tracker.request_xml(id ? :put : :post, resource_url, to_xml(false))
151
- if response["success"] == "true"
170
+ if response["story"]
152
171
  initialize(project, response["story"])
153
172
  true
154
173
  else
155
- Array(response["errors"]["error"])
174
+ if !response["errors"].nil?
175
+ Array(response["errors"]["error"])
176
+ else
177
+ [ "Received bad reply from web, but no explicit error message." ]
178
+ end
179
+ end
180
+ end
181
+
182
+ def save!
183
+ errors = save
184
+ if errors != true
185
+ raise Pickler::Tracker::Error, Array(errors).join("\n"), caller
156
186
  end
187
+ self
157
188
  end
158
189
 
159
190
  private
data/pickler.gemspec CHANGED
@@ -1,18 +1,18 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "pickler"
3
- s.version = "0.0.9.3"
4
-
3
+ s.version = "0.0.9.10"
5
4
  s.summary = "PIvotal traCKer Liaison to cucumbER"
6
5
  s.description = "Synchronize between Cucumber and Pivotal Tracker"
7
- s.authors = ["Tim Pope", "Kamal Fariz Mahyuddin", "Liam Morley", "Kosmas Sch�tz"]
6
+ s.authors = ["Tim Pope", "Liam Morley", "Kamal Fariz Mahyuddin", "Kosmas Schuetz"]
8
7
  s.email = "ruby@tpope.i"+'nfo'
9
8
  s.homepage = "http://github.com/tpope/pickler"
10
9
  s.default_executable = "pickler"
11
- s.executables = ["pickler"]
10
+ s.executables = ["piv", "pickler"]
12
11
  s.files = [
13
12
  "README.rdoc",
14
13
  "MIT-LICENSE",
15
14
  "pickler.gemspec",
15
+ "bin/piv",
16
16
  "bin/pickler",
17
17
  "lib/pickler.rb",
18
18
  "lib/pickler/feature.rb",
@@ -24,5 +24,6 @@ Gem::Specification.new do |s|
24
24
  "lib/pickler/tracker/note.rb"
25
25
  ]
26
26
  s.add_dependency("activesupport", [">= 2.0.0"])
27
- s.add_dependency("kosmas58-cucumber", [">= 0.1.19"])
27
+ s.add_dependency("cucumber", [">= 0.3.0"])
28
+ s.add_dependency("nokogiri", [">= 1.3.1"])
28
29
  end
metadata CHANGED
@@ -1,22 +1,23 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kosmas58-pickler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9.3
4
+ version: 0.0.9.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Pope
8
- - Kamal Fariz Mahyuddin
9
8
  - Liam Morley
10
- - "Kosmas Sch\xEF\xBF\xBDtz"
9
+ - Kamal Fariz Mahyuddin
10
+ - Kosmas Schuetz
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
14
 
15
- date: 2009-02-05 00:00:00 -08:00
15
+ date: 2009-05-16 00:00:00 -07:00
16
16
  default_executable: pickler
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: activesupport
20
+ type: :runtime
20
21
  version_requirement:
21
22
  version_requirements: !ruby/object:Gem::Requirement
22
23
  requirements:
@@ -25,17 +26,29 @@ dependencies:
25
26
  version: 2.0.0
26
27
  version:
27
28
  - !ruby/object:Gem::Dependency
28
- name: kosmas58-cucumber
29
+ name: cucumber
30
+ type: :runtime
31
+ version_requirement:
32
+ version_requirements: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 0.3.0
37
+ version:
38
+ - !ruby/object:Gem::Dependency
39
+ name: nokogiri
40
+ type: :runtime
29
41
  version_requirement:
30
42
  version_requirements: !ruby/object:Gem::Requirement
31
43
  requirements:
32
44
  - - ">="
33
45
  - !ruby/object:Gem::Version
34
- version: 0.1.19
46
+ version: 1.3.1
35
47
  version:
36
48
  description: Synchronize between Cucumber and Pivotal Tracker
37
49
  email: ruby@tpope.info
38
50
  executables:
51
+ - piv
39
52
  - pickler
40
53
  extensions: []
41
54
 
@@ -45,6 +58,7 @@ files:
45
58
  - README.rdoc
46
59
  - MIT-LICENSE
47
60
  - pickler.gemspec
61
+ - bin/piv
48
62
  - bin/pickler
49
63
  - lib/pickler.rb
50
64
  - lib/pickler/feature.rb