ki_pivotal 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -1
- data/bin/ki_pivotal +49 -24
- data/lib/ki_pivotal/api.rb +36 -14
- data/lib/ki_pivotal/interactive.rb +70 -0
- data/lib/ki_pivotal/kiseru.rb +27 -6
- data/lib/ki_pivotal/session.rb +1 -0
- data/lib/ki_pivotal/version.rb +1 -1
- data/lib/ki_pivotal.rb +1 -0
- data/spec/integration/api_spec.rb +10 -3
- data/spec/spec_helper.rb +3 -0
- metadata +24 -23
data/.gitignore
CHANGED
data/bin/ki_pivotal
CHANGED
@@ -19,16 +19,14 @@ require 'ki_pivotal'
|
|
19
19
|
require 'colored'
|
20
20
|
include GLI
|
21
21
|
|
22
|
-
program_desc 'A command-line interface for Pivotal Tracker'
|
22
|
+
program_desc 'A Kiseru command-line interface for Pivotal Tracker'
|
23
23
|
|
24
24
|
version KiPivotal::VERSION
|
25
25
|
|
26
|
-
desc '
|
27
|
-
command :
|
26
|
+
desc 'Initialise config files'
|
27
|
+
command :init do |c|
|
28
28
|
c.action do |global_options,options,args|
|
29
|
-
|
30
|
-
token = $stdin.gets.chomp
|
31
|
-
Kiseru::Config[:ki_pivotal].write('api_token', token)
|
29
|
+
Kiseru::Config[:ki_pivotal].write('api_token', KiPivotal::Interactive.repeatedly_ask("Please enter your Pivotal Tracker API token.\n This can be found at the bottom of your 'Profile' page.\n Token"))
|
32
30
|
end
|
33
31
|
end
|
34
32
|
|
@@ -36,33 +34,60 @@ desc 'Get projects'
|
|
36
34
|
command :projects do |c|
|
37
35
|
c.action do |global_options,options,args|
|
38
36
|
pretty = $stdout.tty?
|
39
|
-
$stdout.puts KiPivotal::Api.projects(:pretty => pretty)
|
37
|
+
$stdout.puts KiPivotal::Api.new.projects(:pretty => pretty)
|
40
38
|
end
|
41
39
|
end
|
42
40
|
|
43
|
-
desc 'Create a
|
44
|
-
command :
|
45
|
-
c.
|
41
|
+
desc 'Create a story'
|
42
|
+
command :new_story do |c|
|
43
|
+
c.desc "The project ID"
|
44
|
+
c.flag [:p, :project]
|
45
|
+
c.desc "A file containing the JSON for a story"
|
46
46
|
c.flag [:f, :file]
|
47
|
+
c.desc "Interactive mode"
|
48
|
+
c.switch [:i, :interactive]
|
47
49
|
c.action do |global_options,options,args|
|
50
|
+
# When we have received JSON via a pipe/file
|
51
|
+
# interactive mode should allow a missing
|
52
|
+
# project ID to be specified.
|
53
|
+
if options[:f] || !$stdin.tty?
|
54
|
+
story_json = options[:f] ? File.read(options[:f]) : $stdin.read
|
55
|
+
unless project_id = options[:project]
|
56
|
+
projects = MultiJson.load(KiPivotal::Api.new.projects)
|
57
|
+
if options[:i]
|
58
|
+
$stdin = STDIN.reopen('/dev/tty')
|
59
|
+
project_id = KiPivotal::Interactive.select_project
|
60
|
+
else
|
61
|
+
$stderr.puts "\n A story cannot be created without specifying which project to create it under:"
|
62
|
+
$stderr.puts " * The ID of a chosen project may be specified with --project=<ID>"
|
63
|
+
$stderr.puts " * A project may be chosen interactively by using the switch -i."
|
64
|
+
$stderr.puts "\n Available projects:"
|
48
65
|
|
49
|
-
|
50
|
-
projects = KiPivotal::Api.projects
|
66
|
+
projects.each { |project| $stderr.puts " #{project['id']} ".green + " #{project['name']}" }
|
51
67
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
68
|
+
$stderr.puts
|
69
|
+
raise "No project given"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
# When there is no JSON provided
|
73
|
+
# interactive mode should allow the
|
74
|
+
# entire story to be created.
|
75
|
+
else
|
76
|
+
if options[:i]
|
77
|
+
project_id = KiPivotal::Interactive.select_project
|
78
|
+
story_json = KiPivotal::Interactive.new_story
|
79
|
+
else
|
80
|
+
$stderr.puts "\n Input must be given to this tool to create a story."
|
81
|
+
$stderr.puts "\n You must do one of the following:"
|
82
|
+
$stderr.puts " * Provide an input file with --file=<file> or -f=<file>."
|
83
|
+
$stderr.puts " * Pipe compatible JSON representing a story to this tool."
|
84
|
+
$stderr.puts " * Use the interactive switch -i to create a story via the command line."
|
85
|
+
$stderr.puts
|
86
|
+
raise "No input given."
|
87
|
+
end
|
59
88
|
end
|
60
89
|
|
61
|
-
|
62
|
-
story_json = File.read(options[:f])
|
63
|
-
end
|
64
|
-
story_json ||= $stdin.read
|
65
|
-
$stdout.puts KiPivotal::Api.create_story(project_id, story_json)
|
90
|
+
$stdout.puts KiPivotal::Api.new.new_story(project_id, story_json)
|
66
91
|
end
|
67
92
|
end
|
68
93
|
|
data/lib/ki_pivotal/api.rb
CHANGED
@@ -1,14 +1,20 @@
|
|
1
1
|
module KiPivotal
|
2
2
|
|
3
|
+
class ApiError < StandardError; end;
|
4
|
+
|
3
5
|
class Api
|
4
6
|
|
7
|
+
def initialize(config = Kiseru::Config[:ki_pivotal])
|
8
|
+
@config = config
|
9
|
+
end
|
10
|
+
|
5
11
|
# TODO - this is only used internally, unlike the other methods
|
6
12
|
# It also doesn't return JSON because it is used internally
|
7
|
-
def
|
13
|
+
def get_token(username, password)
|
8
14
|
PivotalTracker::Client.token(username, password)
|
9
15
|
end
|
10
16
|
|
11
|
-
def
|
17
|
+
def projects(opts = {})
|
12
18
|
with_session do
|
13
19
|
pretty = opts.fetch(:pretty) { true }
|
14
20
|
|
@@ -25,7 +31,7 @@ module KiPivotal
|
|
25
31
|
end
|
26
32
|
end
|
27
33
|
|
28
|
-
def
|
34
|
+
def new_story(project_id, json)
|
29
35
|
from_and_to_json(json, true) do |data|
|
30
36
|
with_session do
|
31
37
|
data.delete('ki_type')
|
@@ -53,28 +59,44 @@ module KiPivotal
|
|
53
59
|
# * interact with PivotalTracker gem
|
54
60
|
# * convert PivotalTracker gem response (object) into data
|
55
61
|
# * convert data back into JSON
|
56
|
-
def
|
57
|
-
|
62
|
+
def add_story_to_project(project_id, data)
|
63
|
+
begin
|
64
|
+
project = PivotalTracker::Project.find(project_id)
|
65
|
+
rescue RestClient::ResourceNotFound => e
|
66
|
+
raise ApiError, "Invalid project ID #{project_id}"
|
67
|
+
end
|
68
|
+
|
58
69
|
story = project.stories.create(data)
|
59
70
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
71
|
+
if story.errors.empty?
|
72
|
+
created_story = {}
|
73
|
+
story.instance_variables.each do |instance_var|
|
74
|
+
string_name = instance_var.to_s.gsub('@','')
|
75
|
+
created_story[string_name] = story.instance_variable_get(instance_var)
|
76
|
+
end
|
77
|
+
created_story
|
78
|
+
else
|
79
|
+
all_errors = story.errors.errors.last
|
80
|
+
raise ApiError, "JSON for story was invalid: #{all_errors}"
|
64
81
|
end
|
65
|
-
created_story
|
66
82
|
end
|
67
83
|
|
68
|
-
def
|
84
|
+
def from_and_to_json(json, pretty=false)
|
69
85
|
data = MultiJson.load(json)
|
70
86
|
new_data = yield data
|
71
87
|
MultiJson.dump(new_data, :pretty => pretty)
|
72
88
|
end
|
73
89
|
|
74
|
-
def
|
75
|
-
session = Session.new(
|
90
|
+
def with_session
|
91
|
+
session = Session.new(@config)
|
76
92
|
session.init
|
77
|
-
|
93
|
+
begin
|
94
|
+
yield
|
95
|
+
rescue PivotalTracker::Client::NoToken => e
|
96
|
+
raise ApiError, "Authentication failed. Check your API token."
|
97
|
+
rescue RestClient::Unauthorized => e
|
98
|
+
raise ApiError, "Not authorized. Check your API token."
|
99
|
+
end
|
78
100
|
end
|
79
101
|
|
80
102
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module KiPivotal
|
2
|
+
|
3
|
+
class Interactive
|
4
|
+
# "Resistance is futile!", "We will get an answer!"
|
5
|
+
def self.repeatedly_ask(question)
|
6
|
+
response = ""
|
7
|
+
while response == ""
|
8
|
+
response = simple_query(question)
|
9
|
+
end
|
10
|
+
response
|
11
|
+
end
|
12
|
+
# "We have ways of making you talk!"
|
13
|
+
def self.repeatedly_choose_from(things, question)
|
14
|
+
index = nil
|
15
|
+
while index.nil?
|
16
|
+
index = choose_from_many(things, question)
|
17
|
+
end
|
18
|
+
index
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.choose_from_many(things, question)
|
22
|
+
$stderr.puts "\n "+question
|
23
|
+
|
24
|
+
things.each_with_index { |thing, index| $stderr.puts " #{index+1} ".green + " #{thing}" }
|
25
|
+
|
26
|
+
index = ( (simple_query( "Enter an index (#{(1..things.size)})" ).to_i) - 1 )
|
27
|
+
index = ( index >= 0 && index < things.size ) ? index : nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.simple_query(prompt)
|
31
|
+
if prompt.nil?
|
32
|
+
$stderr.print "\n> "
|
33
|
+
else
|
34
|
+
$stderr.print "\n #{prompt}: "
|
35
|
+
end
|
36
|
+
$stdin.gets.chomp
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.select_project
|
40
|
+
projects = MultiJson.load(KiPivotal::Api.new.projects)
|
41
|
+
|
42
|
+
if projects.size > 1
|
43
|
+
index = repeatedly_choose_from(projects.map { |p| p['name'] }, "Which project should this story be created under?")
|
44
|
+
else
|
45
|
+
index = 0
|
46
|
+
end
|
47
|
+
|
48
|
+
$stderr.puts "\n Story will be created under the project, '#{projects[index]['name']}'"
|
49
|
+
projects[index]['id']
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.new_story
|
53
|
+
name = repeatedly_ask("Name of story")
|
54
|
+
|
55
|
+
description = repeatedly_ask("Description of story")
|
56
|
+
|
57
|
+
types = ["bug","feature","release","chore"]
|
58
|
+
type = types[repeatedly_choose_from(types, "What type of story is this?")]
|
59
|
+
|
60
|
+
{
|
61
|
+
name: name,
|
62
|
+
description: description,
|
63
|
+
story_type: type,
|
64
|
+
requested_by: 'Kiseru',
|
65
|
+
ki_type: 'pivotal_story'
|
66
|
+
}.to_json
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
data/lib/ki_pivotal/kiseru.rb
CHANGED
@@ -1,18 +1,23 @@
|
|
1
1
|
require 'pathname'
|
2
|
+
require 'fileutils'
|
2
3
|
|
3
4
|
module Kiseru
|
4
5
|
|
6
|
+
class ConfigError < StandardError; end;
|
7
|
+
|
5
8
|
class ConfigDir
|
6
9
|
|
7
|
-
|
10
|
+
NAME = '.kiseru'
|
8
11
|
|
9
12
|
attr_reader :path
|
10
13
|
|
11
14
|
def initialize(opts = {})
|
15
|
+
name = opts.fetch(:name) { NAME }
|
12
16
|
root = Pathname.new(File.expand_path(opts.fetch(:root) { '~' }))
|
13
|
-
@path = root +
|
17
|
+
@path = root + name
|
14
18
|
unless File.directory?(@path)
|
15
19
|
Dir.mkdir(@path)
|
20
|
+
FileUtils.chmod(0700, @path)
|
16
21
|
end
|
17
22
|
end
|
18
23
|
|
@@ -24,18 +29,34 @@ module Kiseru
|
|
24
29
|
|
25
30
|
class Config
|
26
31
|
|
32
|
+
attr_reader :path
|
33
|
+
|
27
34
|
def self.[](key)
|
28
35
|
ConfigDir.new.config(key)
|
29
36
|
end
|
30
37
|
|
31
38
|
def initialize(root_path, app_name)
|
32
|
-
|
33
|
-
|
39
|
+
@path = (root_path + "#{app_name}.yml").to_s
|
40
|
+
unless File.exists?(@path)
|
41
|
+
FileUtils.touch(@path)
|
42
|
+
FileUtils.chmod(0600, @path)
|
43
|
+
end
|
44
|
+
@store = YAML::Store.new(@path)
|
45
|
+
@store.transaction do
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def ensure_present(*keys)
|
50
|
+
keys.each do |key|
|
51
|
+
if read(key).nil?
|
52
|
+
raise ConfigError, "'#{key}' is not defined in config"
|
53
|
+
end
|
54
|
+
end
|
34
55
|
end
|
35
56
|
|
36
|
-
def write(key,
|
57
|
+
def write(key, value)
|
37
58
|
@store.transaction do
|
38
|
-
@store[key] =
|
59
|
+
@store[key] = value
|
39
60
|
end
|
40
61
|
end
|
41
62
|
|
data/lib/ki_pivotal/session.rb
CHANGED
data/lib/ki_pivotal/version.rb
CHANGED
data/lib/ki_pivotal.rb
CHANGED
@@ -4,8 +4,15 @@ include KiPivotal
|
|
4
4
|
|
5
5
|
describe KiPivotal::Api do
|
6
6
|
|
7
|
+
let(:config) do
|
8
|
+
Kiseru::ConfigDir.new(:root => SPEC_DIR,
|
9
|
+
:name => SPEC_CONFIG_DIR).config(:ki_pivotal)
|
10
|
+
end
|
11
|
+
|
12
|
+
|
7
13
|
it "returns projects", :vcr do
|
8
|
-
|
14
|
+
api = Api.new(config)
|
15
|
+
json = api.projects
|
9
16
|
projects = MultiJson.load(json)
|
10
17
|
|
11
18
|
projects.each do |project|
|
@@ -15,7 +22,6 @@ describe KiPivotal::Api do
|
|
15
22
|
end
|
16
23
|
|
17
24
|
it "creates bugs", :vcr do
|
18
|
-
# uses open-source kiseru tracker project
|
19
25
|
name = "Something is broken"
|
20
26
|
|
21
27
|
input_json = MultiJson.dump({ 'ki_type' => 'pivotal_story',
|
@@ -23,7 +29,8 @@ describe KiPivotal::Api do
|
|
23
29
|
'requested_by' => 'Kiseru',
|
24
30
|
'story_type' => 'bug'
|
25
31
|
})
|
26
|
-
|
32
|
+
api = Api.new(config)
|
33
|
+
json = api.new_story(617613, input_json)
|
27
34
|
story = MultiJson.load(json)
|
28
35
|
|
29
36
|
story['id'].should_not be_nil
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ki_pivotal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -14,7 +14,7 @@ date: 2012-08-15 00:00:00.000000000Z
|
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: gli
|
17
|
-
requirement: &
|
17
|
+
requirement: &70107004966400 !ruby/object:Gem::Requirement
|
18
18
|
none: false
|
19
19
|
requirements:
|
20
20
|
- - ! '>='
|
@@ -22,10 +22,10 @@ dependencies:
|
|
22
22
|
version: '0'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
|
-
version_requirements: *
|
25
|
+
version_requirements: *70107004966400
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: multi_json
|
28
|
-
requirement: &
|
28
|
+
requirement: &70107004965980 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
31
31
|
- - ! '>='
|
@@ -33,10 +33,10 @@ dependencies:
|
|
33
33
|
version: '0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
|
-
version_requirements: *
|
36
|
+
version_requirements: *70107004965980
|
37
37
|
- !ruby/object:Gem::Dependency
|
38
38
|
name: pivotal-tracker
|
39
|
-
requirement: &
|
39
|
+
requirement: &70107004965560 !ruby/object:Gem::Requirement
|
40
40
|
none: false
|
41
41
|
requirements:
|
42
42
|
- - ! '>='
|
@@ -44,10 +44,10 @@ dependencies:
|
|
44
44
|
version: '0'
|
45
45
|
type: :runtime
|
46
46
|
prerelease: false
|
47
|
-
version_requirements: *
|
47
|
+
version_requirements: *70107004965560
|
48
48
|
- !ruby/object:Gem::Dependency
|
49
49
|
name: colored
|
50
|
-
requirement: &
|
50
|
+
requirement: &70107004965140 !ruby/object:Gem::Requirement
|
51
51
|
none: false
|
52
52
|
requirements:
|
53
53
|
- - ! '>='
|
@@ -55,10 +55,10 @@ dependencies:
|
|
55
55
|
version: '0'
|
56
56
|
type: :runtime
|
57
57
|
prerelease: false
|
58
|
-
version_requirements: *
|
58
|
+
version_requirements: *70107004965140
|
59
59
|
- !ruby/object:Gem::Dependency
|
60
60
|
name: debugger
|
61
|
-
requirement: &
|
61
|
+
requirement: &70107004994920 !ruby/object:Gem::Requirement
|
62
62
|
none: false
|
63
63
|
requirements:
|
64
64
|
- - ! '>='
|
@@ -66,10 +66,10 @@ dependencies:
|
|
66
66
|
version: '0'
|
67
67
|
type: :development
|
68
68
|
prerelease: false
|
69
|
-
version_requirements: *
|
69
|
+
version_requirements: *70107004994920
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: rake
|
72
|
-
requirement: &
|
72
|
+
requirement: &70107004994500 !ruby/object:Gem::Requirement
|
73
73
|
none: false
|
74
74
|
requirements:
|
75
75
|
- - ! '>='
|
@@ -77,10 +77,10 @@ dependencies:
|
|
77
77
|
version: '0'
|
78
78
|
type: :development
|
79
79
|
prerelease: false
|
80
|
-
version_requirements: *
|
80
|
+
version_requirements: *70107004994500
|
81
81
|
- !ruby/object:Gem::Dependency
|
82
82
|
name: simplecov
|
83
|
-
requirement: &
|
83
|
+
requirement: &70107004994080 !ruby/object:Gem::Requirement
|
84
84
|
none: false
|
85
85
|
requirements:
|
86
86
|
- - ! '>='
|
@@ -88,10 +88,10 @@ dependencies:
|
|
88
88
|
version: '0'
|
89
89
|
type: :development
|
90
90
|
prerelease: false
|
91
|
-
version_requirements: *
|
91
|
+
version_requirements: *70107004994080
|
92
92
|
- !ruby/object:Gem::Dependency
|
93
93
|
name: rspec
|
94
|
-
requirement: &
|
94
|
+
requirement: &70107004993660 !ruby/object:Gem::Requirement
|
95
95
|
none: false
|
96
96
|
requirements:
|
97
97
|
- - ! '>='
|
@@ -99,10 +99,10 @@ dependencies:
|
|
99
99
|
version: '0'
|
100
100
|
type: :development
|
101
101
|
prerelease: false
|
102
|
-
version_requirements: *
|
102
|
+
version_requirements: *70107004993660
|
103
103
|
- !ruby/object:Gem::Dependency
|
104
104
|
name: test-construct
|
105
|
-
requirement: &
|
105
|
+
requirement: &70107004993240 !ruby/object:Gem::Requirement
|
106
106
|
none: false
|
107
107
|
requirements:
|
108
108
|
- - ! '>='
|
@@ -110,10 +110,10 @@ dependencies:
|
|
110
110
|
version: '0'
|
111
111
|
type: :development
|
112
112
|
prerelease: false
|
113
|
-
version_requirements: *
|
113
|
+
version_requirements: *70107004993240
|
114
114
|
- !ruby/object:Gem::Dependency
|
115
115
|
name: vcr
|
116
|
-
requirement: &
|
116
|
+
requirement: &70107004992820 !ruby/object:Gem::Requirement
|
117
117
|
none: false
|
118
118
|
requirements:
|
119
119
|
- - ! '>='
|
@@ -121,10 +121,10 @@ dependencies:
|
|
121
121
|
version: '0'
|
122
122
|
type: :development
|
123
123
|
prerelease: false
|
124
|
-
version_requirements: *
|
124
|
+
version_requirements: *70107004992820
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
126
|
name: webmock
|
127
|
-
requirement: &
|
127
|
+
requirement: &70107004992400 !ruby/object:Gem::Requirement
|
128
128
|
none: false
|
129
129
|
requirements:
|
130
130
|
- - ! '>='
|
@@ -132,7 +132,7 @@ dependencies:
|
|
132
132
|
version: '0'
|
133
133
|
type: :development
|
134
134
|
prerelease: false
|
135
|
-
version_requirements: *
|
135
|
+
version_requirements: *70107004992400
|
136
136
|
description: Kiseru Pivotal Tracker client
|
137
137
|
email:
|
138
138
|
- ben@freeagent.com
|
@@ -152,6 +152,7 @@ files:
|
|
152
152
|
- ki_pivotal.gemspec
|
153
153
|
- lib/ki_pivotal.rb
|
154
154
|
- lib/ki_pivotal/api.rb
|
155
|
+
- lib/ki_pivotal/interactive.rb
|
155
156
|
- lib/ki_pivotal/kiseru.rb
|
156
157
|
- lib/ki_pivotal/session.rb
|
157
158
|
- lib/ki_pivotal/version.rb
|