kosmas58-pickler 0.0.9.3 → 0.0.9.10
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 +31 -0
- data/bin/pickler +1 -0
- data/bin/piv +8 -0
- data/lib/pickler.rb +88 -16
- data/lib/pickler/feature.rb +24 -9
- data/lib/pickler/runner.rb +123 -31
- data/lib/pickler/tracker.rb +25 -15
- data/lib/pickler/tracker/iteration.rb +9 -2
- data/lib/pickler/tracker/note.rb +5 -1
- data/lib/pickler/tracker/project.rb +13 -1
- data/lib/pickler/tracker/story.rb +38 -7
- data/pickler.gemspec +6 -5
- metadata +20 -6
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
data/bin/piv
ADDED
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.
|
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.
|
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
|
data/lib/pickler/feature.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
class Pickler
|
2
2
|
class Feature
|
3
|
-
URL_REGEX = %r{\
|
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)[
|
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
|
-
|
68
|
-
story
|
69
|
-
|
70
|
-
|
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[
|
100
|
+
@id = if id = local_body.to_s[/(?:#\s*|@[[:punct:]]?)#{URL_REGEX}/,1]
|
86
101
|
id.to_i
|
87
102
|
end
|
88
103
|
end
|
data/lib/pickler/runner.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
210
|
-
|
211
|
-
|
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 |
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
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
|
-
|
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[:
|
548
|
+
"accepted" => COLORS[:blue],
|
457
549
|
"delivered" => COLORS[:yellow],
|
458
550
|
"unscheduled" => COLORS[:white],
|
459
|
-
"started" => COLORS[:
|
551
|
+
"started" => COLORS[:green],
|
460
552
|
"finished" => COLORS[:cyan],
|
461
|
-
"unstarted" => COLORS[:
|
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" => "
|
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[:
|
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
|
data/lib/pickler/tracker.rb
CHANGED
@@ -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/
|
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)
|
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
|
-
|
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"]
|
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)
|
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
|
-
|
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))
|
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
|
data/lib/pickler/tracker/note.rb
CHANGED
@@ -3,7 +3,11 @@ class Pickler
|
|
3
3
|
class Note < Abstract
|
4
4
|
attr_reader :story
|
5
5
|
reader :text, :author
|
6
|
-
date_reader :
|
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"]
|
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, :
|
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 = "#
|
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
|
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["
|
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["
|
170
|
+
if response["story"]
|
152
171
|
initialize(project, response["story"])
|
153
172
|
true
|
154
173
|
else
|
155
|
-
|
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.
|
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", "
|
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("
|
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.
|
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
|
-
-
|
9
|
+
- Kamal Fariz Mahyuddin
|
10
|
+
- Kosmas Schuetz
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
14
|
|
15
|
-
date: 2009-
|
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:
|
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:
|
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
|