gradesfirst 0.2.0

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/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ *.gem
2
+ *.swp
3
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rake'
4
+ gem 'minitest', '~> 4.7.0'
5
+ gem 'thor', '~> 0.14.6'
6
+ gem 'webmock', '~> 1.16.0'
7
+ gem 'mocha'
8
+ gem 'pry'
9
+ gem 'pry-debugger'
data/Gemfile.lock ADDED
@@ -0,0 +1,45 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ addressable (2.3.5)
5
+ coderay (1.1.0)
6
+ columnize (0.3.6)
7
+ crack (0.4.1)
8
+ safe_yaml (~> 0.9.0)
9
+ debugger (1.6.3)
10
+ columnize (>= 0.3.1)
11
+ debugger-linecache (~> 1.2.0)
12
+ debugger-ruby_core_source (~> 1.2.4)
13
+ debugger-linecache (1.2.0)
14
+ debugger-ruby_core_source (1.2.4)
15
+ metaclass (0.0.1)
16
+ method_source (0.8.2)
17
+ minitest (4.7.5)
18
+ mocha (0.14.0)
19
+ metaclass (~> 0.0.1)
20
+ pry (0.9.12.4)
21
+ coderay (~> 1.0)
22
+ method_source (~> 0.8)
23
+ slop (~> 3.4)
24
+ pry-debugger (0.2.2)
25
+ debugger (~> 1.3)
26
+ pry (~> 0.9.10)
27
+ rake (10.0.2)
28
+ safe_yaml (0.9.7)
29
+ slop (3.4.7)
30
+ thor (0.14.6)
31
+ webmock (1.16.0)
32
+ addressable (>= 2.2.7)
33
+ crack (>= 0.3.2)
34
+
35
+ PLATFORMS
36
+ ruby
37
+
38
+ DEPENDENCIES
39
+ minitest (~> 4.7.0)
40
+ mocha
41
+ pry
42
+ pry-debugger
43
+ rake
44
+ thor (~> 0.14.6)
45
+ webmock (~> 1.16.0)
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 GradesFirst
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1 @@
1
+
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new(:test) do |test|
4
+ test.libs << 'test'
5
+ test.test_files = FileList["test/**/*.rb"].exclude("test/test_helper.rb")
6
+ test.verbose = false
7
+ test.warning = false
8
+ end
data/bin/gf ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/ruby
2
+
3
+ if File.exists?(File.join(File.expand_path('../..', __FILE__), '.git'))
4
+ lib_path = File.expand_path('../../lib', __FILE__)
5
+ $LOAD_PATH.unshift(lib_path)
6
+ end
7
+
8
+ require 'rubygems'
9
+ require 'gradesfirst'
10
+ require 'gradesfirst/cli'
11
+
12
+ GradesFirst::CLI.start
@@ -0,0 +1,27 @@
1
+ Gem::Specification.new do |s|
2
+ s.platform = Gem::Platform::RUBY
3
+ s.name = "gradesfirst"
4
+ s.version = "0.2.0"
5
+ s.summary = "GradesFirst command line utility for developers."
6
+ s.description = "This utility will help manage the various tasks developers need to do on their workstation such as database updates."
7
+ s.license = "MIT"
8
+
9
+ s.required_ruby_version = ">= 1.8.7"
10
+ s.required_rubygems_version = ">= 1.3.6"
11
+
12
+ s.author = "GradesFirst"
13
+ s.email = "tech@gradesfirst.com"
14
+ s.homepage = "http://www.gradesfirst.com"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test}/*`.split("\n")
18
+
19
+ s.bindir = "bin"
20
+ s.executables = ["gf"]
21
+ s.default_executable = "gf"
22
+
23
+ s.require_paths = ['lib']
24
+
25
+ s.add_dependency('thor', '~> 0.14.6')
26
+ s.add_development_dependency('minitest', '~> 4.7.0')
27
+ end
@@ -0,0 +1,2 @@
1
+ module GradesFirst
2
+ end
@@ -0,0 +1,59 @@
1
+ require 'gradesfirst/command'
2
+
3
+ module GradesFirst
4
+
5
+ # Implemenation of a Thor command for enhancing git branch to include
6
+ # PivotalTracker information such as the story state, name and url.
7
+ class BranchCommand < GradesFirst::Command
8
+
9
+ # Description of the gf branch Thor command that will be used in the
10
+ # commandline help.
11
+ def self.description
12
+ 'List git branches with related PivotalTracker story information.'
13
+ end
14
+
15
+ def initialize
16
+ @enhanced_branches = []
17
+ end
18
+
19
+ # Performs the gf branch Thor command.
20
+ def execute
21
+ git_response = git_branch
22
+ git_response.each_line do |branch|
23
+ @enhanced_branches << enhanced_branch(branch.rstrip)
24
+ end
25
+ nil
26
+ end
27
+
28
+ # Output response of the gf branch Thor command that contains the
29
+ # PivotalTracker enhanced branch names.
30
+ def response
31
+ @enhanced_branches.join("\n") + "\n"
32
+ end
33
+
34
+ private
35
+
36
+ def enhanced_branch(branch)
37
+ story = find_story_by_branch(branch)
38
+ if story
39
+ status = story['current_state'].capitalize
40
+ "#{branch} [#{status}] #{story['name']} (#{story['url']})"
41
+ else
42
+ branch
43
+ end
44
+ end
45
+
46
+ def find_story_by_branch(branch)
47
+ if story_id(branch)
48
+ PivotalTracker.stories[story_id(branch)].get
49
+ else
50
+ nil
51
+ end
52
+ end
53
+
54
+ def git_branch
55
+ `git branch`
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,58 @@
1
+ require 'thor'
2
+
3
+ require 'gradesfirst/command'
4
+ require 'gradesfirst/branch_command'
5
+ require 'gradesfirst/commit_message_command'
6
+ require 'gradesfirst/task_command'
7
+
8
+ require 'pivotal_tracker'
9
+
10
+ module GradesFirst
11
+ class CLI < Thor
12
+
13
+ desc 'branch', GradesFirst::BranchCommand.description
14
+ def branch
15
+ set_pivotal_tracker_api_token
16
+ execute(GradesFirst::BranchCommand.new)
17
+ end
18
+
19
+ desc 'commit-message', GradesFirst::CommitMessageCommand.description
20
+ def commit_message
21
+ set_pivotal_tracker_api_token
22
+ execute(GradesFirst::CommitMessageCommand.new)
23
+ end
24
+
25
+ desc 'task', GradesFirst::TaskCommand.description
26
+ def task
27
+ set_pivotal_tracker_api_token
28
+ execute(GradesFirst::TaskCommand.new)
29
+ end
30
+
31
+ private
32
+
33
+ def execute(command)
34
+ command.execute
35
+ say command.response
36
+ end
37
+
38
+ def set_pivotal_tracker_api_token
39
+ file_name = "#{Dir.home}/.pivotal_tracker_api_key"
40
+ if File.exists?(file_name)
41
+ api_token = File.read(file_name)
42
+ else
43
+ api_token = ask('Enter your PivotalTracker API token:')
44
+ File.open(file_name, 'w') do |file|
45
+ file.write(api_token)
46
+ end
47
+ end
48
+
49
+ PivotalTracker.api_token = api_token
50
+ end
51
+
52
+ Signal.trap('SIGINT') do
53
+ print "\n"
54
+ exit 1
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,22 @@
1
+ module GradesFirst
2
+
3
+ # Base class for all command objects.
4
+ class Command
5
+
6
+ private
7
+
8
+ # Retrieves the current git branch.
9
+ def current_branch
10
+ `git rev-parse --abbrev-ref HEAD`
11
+ end
12
+
13
+ # Extracts a PivotalTracker story id from a branch if one is present.
14
+ def story_id(branch = current_branch)
15
+ if branch =~ /[0-9]+$/
16
+ branch.match(/[0-9]+$/)[0]
17
+ else
18
+ nil
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ require 'gradesfirst/command'
2
+
3
+ module GradesFirst
4
+
5
+ # Implementation of a Thor command for creating a git commit message that
6
+ # already includes the information from the related PivotalTracker story
7
+ # associated with the current branch.
8
+ class CommitMessageCommand < GradesFirst::Command
9
+
10
+ # Description of the gf commit-message Thor command that well be usewd in
11
+ # the commandline help.
12
+ def self.description
13
+ 'Generate a git commit message in the standard format.'
14
+ end
15
+
16
+ # Performs the gf commit-message Thor command.
17
+ def execute
18
+ @story = PivotalTracker.stories[story_id].get
19
+ end
20
+
21
+ # Output response of the gf commit-message Thor command in the standard
22
+ # format.
23
+ def response
24
+ if @story.nil?
25
+ ''
26
+ else
27
+ message = [
28
+ "[\##{@story['id']}]",
29
+ "",
30
+ @story['name'],
31
+ @story['url']
32
+ ]
33
+ message.join("\n") + "\n"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,47 @@
1
+ require 'gradesfirst/command'
2
+
3
+ module GradesFirst
4
+
5
+ # Implementation of a Thor command for managing tasks related to a story.
6
+ class TaskCommand < GradesFirst::Command
7
+
8
+ # Description of the gf branch Thor command that will be used in the
9
+ # command line help.
10
+ def self.description
11
+ 'List the tasks related to a PivotalTracker story.'
12
+ end
13
+
14
+ # Performs the gf tasks Thor command.
15
+ def execute
16
+ @story = PivotalTracker.stories[story_id].get
17
+ if @story
18
+ project_id = @story['project_id']
19
+ @tasks = PivotalTracker.projects[project_id].stories[story_id].tasks.get
20
+ end
21
+ end
22
+
23
+ # Generates the comand line output response. The output of the task command
24
+ # is a list of the tasks associated with the PivotalTracker story associated
25
+ # with the current branch.
26
+ def response
27
+ if @tasks.nil?
28
+ error_message
29
+ else
30
+ task_list = @tasks.map do |t|
31
+ " [#{t["complete"] ? 'X' : ' '}] #{t["description"]}"
32
+ end
33
+ story_line + task_list.join("\n") + "\n"
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def error_message
40
+ 'Tasks cannot be retrieved for this branch.'
41
+ end
42
+
43
+ def story_line
44
+ " #{@story['name']} (#{@story['url']})\n\n"
45
+ end
46
+ end
47
+ end
data/lib/http_magic.rb ADDED
@@ -0,0 +1,260 @@
1
+ require 'net/http'
2
+ require 'json'
3
+
4
+ # A magical class that interacts with HTTP resources with a minimal amount of
5
+ # configuration.
6
+ #
7
+ # Assuming an api where the url http://www.example.com/foo/99 responds with
8
+ # the following.
9
+ #
10
+ # Header:
11
+ #
12
+ # Content-Type: application/json
13
+ #
14
+ # Body:
15
+ #
16
+ # {
17
+ # "name": "Foo Bar"
18
+ # }
19
+ #
20
+ # == Example
21
+ #
22
+ # class ExampleApi < HttpMagic
23
+ # url 'www.example.com'
24
+ # end
25
+ #
26
+ # ExampleApi.foo[99].get['name']
27
+ # => "Foo Bar"
28
+ #
29
+ class HttpMagic < BasicObject
30
+ # Makes the new method private so that instances of this class and it's
31
+ # children can only be instantiated through the class method method_missing.
32
+ # This is needed to enforce the intended usage of HttpMagic where the class
33
+ # is configured to represent an http location. Once it is configured, the
34
+ # location is interacted with dynamically with chained methods that correspond
35
+ # with parts of URNs found at the location.
36
+ #
37
+ # == Example
38
+ #
39
+ # class ExampleApi < HttpMagic
40
+ # url 'www.example.com'
41
+ # namespace 'api/v1'
42
+ # headers({'X-AuthToken' => 'token'})
43
+ # end
44
+ #
45
+ # ExampleApi.new.uri
46
+ # => "http://www.example.com/api/v1/new"
47
+ #
48
+ # ExampleApi.foo[99].uri
49
+ # => "http://www.example.com/api/v1/foo/99"
50
+ class << self
51
+ private :new
52
+ end
53
+
54
+ # Sets or returns the request headers for an HttpMagic based class. The
55
+ # headers will be passed along to each resource request being made.
56
+ #
57
+ # == Example
58
+ #
59
+ # class ExampleApi < HttpMagic
60
+ # url 'www.example.com'
61
+ # namespace 'api/v1'
62
+ # headers({'X-AuthToken' => 'token'})
63
+ # end
64
+ def self.headers(value = :not_provided)
65
+ unless value == :not_provided
66
+ @headers = value
67
+ end
68
+ @headers
69
+ end
70
+
71
+ # Sets or returns the namespace for an HttpMagic based class. The namespace
72
+ # will be prefixed to the urn for each request made to the url.
73
+ #
74
+ # Assuming an api where each resource is namespaced with 'api/v1' and where
75
+ # the url http://www.example.com/api/v1/foo/99 responds with the following.
76
+ #
77
+ # Header:
78
+ #
79
+ # Content-Type: application/json
80
+ #
81
+ # Body:
82
+ #
83
+ # {
84
+ # "name": "Foo Bar"
85
+ # }
86
+ #
87
+ # == Example
88
+ #
89
+ # class ExampleApi < HttpMagic
90
+ # url 'www.example.com'
91
+ # namespace 'api/v1'
92
+ # end
93
+ #
94
+ # ExampleApi.foo[99].get['name']
95
+ # => "Foo Bar"
96
+ def self.namespace(value = :not_provided)
97
+ unless value == :not_provided
98
+ @namespace = value
99
+ end
100
+ @namespace
101
+ end
102
+
103
+ # Sets or returns the uniform resource locator for the HTTP resource.
104
+ #
105
+ # == Example
106
+ #
107
+ # class ExampleApi < HttpMagic
108
+ # url 'www.example.com'
109
+ # end
110
+ #
111
+ # ExampleApi.url
112
+ # => 'www.example.com'
113
+ def self.url(value = :not_provided)
114
+ unless value == :not_provided
115
+ @url = value
116
+ end
117
+ @url
118
+ end
119
+
120
+ # Class scoped method_missing that starts the magic of creating urns
121
+ # through meta programming by instantiating an instance of the class
122
+ # and delegating the first part of the urn to that instance. This method
123
+ # is an implementation of the Factory Method design pattern.
124
+ def self.method_missing(name, *args, &block)
125
+ new(@url, @namespace, @headers).__send__(name, *args)
126
+ end
127
+
128
+ def initialize(url, namespace, headers)
129
+ @url = url
130
+ @namespace = namespace
131
+ @headers = headers
132
+
133
+ @urn_parts = []
134
+ end
135
+
136
+ # Resource index reference intended to allow for the use of numbers in a urn
137
+ # such as the following 'foo/99' being referenced by ExampleApi.foo[99]. It
138
+ # can also be used to allow for HttpMagic methods to be specified for a urn
139
+ # such as 'foo/get' being referenced by ExampleApi.foo[:get]. Finally, it can
140
+ # be used for urn parts that are not valid Ruby methods such as 'foo/%5B%5D'
141
+ # being referenced by ExampleApi.foo["%5B%5D"].
142
+ #
143
+ # * part - a part of a urn such that 'foo' and 'bar' would be parts of the urn
144
+ # 'foo/bar'.
145
+ #
146
+ # Returns a reference to its instance so that urn parts can be chained
147
+ # together.
148
+ def [](part)
149
+ @urn_parts << part.to_s
150
+ self
151
+ end
152
+
153
+ # Gets a resource from the URI and returns it based on its content type. JSON
154
+ # content will be parsed and returned as equivalent Ruby objects. All other
155
+ # content types will be returned as text.
156
+ #
157
+ # Assuming an api where each resource is namespaced with 'api/v1' and where
158
+ # the url http://www.example.com/api/v1/foo/99 responds with the following.
159
+ #
160
+ # Header:
161
+ #
162
+ # Content-Type: application/json
163
+ #
164
+ # Body:
165
+ #
166
+ # {
167
+ # "name": "Foo Bar"
168
+ # }
169
+ #
170
+ # == Example
171
+ #
172
+ # class ExampleApi < HttpMagic
173
+ # url 'www.example.com'
174
+ # namespace 'api/v1'
175
+ # end
176
+ #
177
+ # ExampleApi.foo[99].get
178
+ # => { "name" => "Foo Bar" }
179
+ def get
180
+ http = ::Net::HTTP.new(url, 443)
181
+ http.use_ssl = true
182
+
183
+ response = http.request_get(urn, @headers || {})
184
+ if response && response.is_a?(::Net::HTTPSuccess)
185
+ if response.content_type == 'application/json'
186
+ ::JSON.parse(response.body)
187
+ else
188
+ response.body
189
+ end
190
+ else
191
+ nil
192
+ end
193
+ end
194
+
195
+ # Uniform resource identifier for a specified request.
196
+ #
197
+ # == Example
198
+ #
199
+ # class ExampleApi < HttpMagic
200
+ # url 'www.example.com'
201
+ # namespace 'api/v1'
202
+ # end
203
+ #
204
+ # ExampleApi.foo[99].uri
205
+ # => "www.example.com/api/v1/foo/99"
206
+ def uri
207
+ "#{url}#{urn}"
208
+ end
209
+
210
+ # Uniform resource locator for all the resources.
211
+ #
212
+ # == Example
213
+ #
214
+ # class ExampleApi < HttpMagic
215
+ # url 'www.example.com'
216
+ # namespace 'api/v1'
217
+ # end
218
+ #
219
+ # ExampleApi.foo[99].url
220
+ # => "www.example.com"
221
+ def url
222
+ @url
223
+ end
224
+
225
+ # Uniform resource name for a resource.
226
+ #
227
+ # == Example
228
+ #
229
+ # class ExampleApi < HttpMagic
230
+ # url 'www.example.com'
231
+ # namespace 'api/v1'
232
+ # end
233
+ #
234
+ # ExampleApi.foo[99].urn
235
+ # => "api/v1/foo/99"
236
+ def urn
237
+ resource_name = [@namespace, @urn_parts].flatten.compact.join('/')
238
+ "/#{resource_name}"
239
+ end
240
+
241
+ # Instance scoped method_missing that accumulates the URN parts used in
242
+ # forming the URI. It returns a reference to the current instance so that
243
+ # method and [] based parts can be chained together to from the URN. In the
244
+ # case of ExampleApi.foo[99] the 'foo' method is accumlated as a URN part
245
+ # that will form the resulting URI.
246
+ #
247
+ # == Example
248
+ #
249
+ # class ExampleApi < HttpMagic
250
+ # url 'www.example.com'
251
+ # namespace 'api/v1'
252
+ # end
253
+ #
254
+ # ExampleApi.foo[99].uri
255
+ # => "www.example.com/api/v1/foo/99"
256
+ def method_missing(part, *args, &block)
257
+ @urn_parts << part.to_s
258
+ self
259
+ end
260
+ end
@@ -0,0 +1,16 @@
1
+ require 'http_magic'
2
+
3
+ class PivotalTracker < HttpMagic
4
+ url 'www.pivotaltracker.com'
5
+ namespace 'services/v5'
6
+
7
+ # Specifies the PivotalTracker API token that is needed to authenticate a user
8
+ # to access the api.
9
+ #
10
+ # == Example
11
+ #
12
+ # ExampleApi.api_token = 'token'
13
+ def self.api_token=(value)
14
+ headers({ 'X-TrackerToken' => value })
15
+ end
16
+ end
@@ -0,0 +1,32 @@
1
+ require 'test_helper'
2
+ require 'gradesfirst/branch_command'
3
+
4
+ describe GradesFirst::BranchCommand do
5
+ before do
6
+ stub_pivotal_request(
7
+ :get,
8
+ 'stories/29384793',
9
+ body: fixture_file('story_bar.json')
10
+ )
11
+
12
+ stub_pivotal_request(
13
+ :get,
14
+ 'stories/57348714',
15
+ body: fixture_file('story_foo.json')
16
+ )
17
+
18
+ PivotalTracker.api_token = 'test_token'
19
+ git_branch_output = fixture_file('git_branch.txt')
20
+ @command = GradesFirst::BranchCommand.new
21
+ @command.stub :git_branch, git_branch_output do
22
+ @command.execute
23
+ end
24
+ end
25
+
26
+ describe '#response' do
27
+ it 'should respond with Pivotal enhancements' do
28
+ assert_equal fixture_file('gf_branch.txt'), @command.response
29
+ end
30
+ end
31
+
32
+ end
data/test/cli_test.rb ADDED
@@ -0,0 +1,53 @@
1
+ require 'test_helper'
2
+ require 'gradesfirst/cli'
3
+
4
+ describe GradesFirst::CLI do
5
+ def command_not_found(command)
6
+ "Could not find task \"#{command}\".\n"
7
+ end
8
+
9
+ def executed_command_expectations(command)
10
+ command.any_instance.expects(:execute)
11
+ command.any_instance.expects(:response).returns('')
12
+ end
13
+
14
+ describe 'branch command' do
15
+ before do
16
+ executed_command_expectations(GradesFirst::BranchCommand)
17
+ end
18
+
19
+ it 'should exist' do
20
+ output = capture_io do
21
+ GradesFirst::CLI.start %w{ branch }
22
+ end
23
+ refute_includes output, command_not_found('branch')
24
+ end
25
+ end
26
+
27
+ describe 'commit-message command' do
28
+ before do
29
+ executed_command_expectations(GradesFirst::CommitMessageCommand)
30
+ end
31
+
32
+ it 'should exist' do
33
+ output = capture_io do
34
+ GradesFirst::CLI.start %w{ commit-message }
35
+ end
36
+ refute_includes output, command_not_found('commit-message')
37
+ end
38
+ end
39
+
40
+ describe 'task command' do
41
+ before do
42
+ executed_command_expectations(GradesFirst::TaskCommand)
43
+ end
44
+
45
+ it 'should exist' do
46
+ output = capture_io do
47
+ GradesFirst::CLI.start %w{ task }
48
+ end
49
+ refute_includes output, command_not_found('task')
50
+ end
51
+ end
52
+
53
+ end
@@ -0,0 +1,14 @@
1
+ require 'test_helper'
2
+ require 'gradesfirst/command'
3
+
4
+ describe GradesFirst::Command do
5
+ describe '#story_id' do
6
+ it 'should pull the story id from the branch' do
7
+ assert '123', GradesFirst::Command.new.send(:story_id, 'abc/123')
8
+ end
9
+
10
+ it 'should return nil if not story id' do
11
+ assert_nil GradesFirst::Command.new.send(:story_id, 'abc')
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ require 'test_helper'
2
+ require 'gradesfirst/commit_message_command'
3
+
4
+ describe GradesFirst::CommitMessageCommand do
5
+ before do
6
+ stub_pivotal_request(
7
+ :get,
8
+ 'stories/29384793',
9
+ body: fixture_file('story_bar.json')
10
+ )
11
+
12
+ PivotalTracker.api_token = 'test_token'
13
+ end
14
+
15
+ describe '#response' do
16
+ it 'should repond with formatted commit message' do
17
+ command = GradesFirst::CommitMessageCommand.new
18
+ command.stub :current_branch, 'test/29384793' do
19
+ command.execute
20
+ assert_equal fixture_file('commit_message.txt'), command.response
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,4 @@
1
+ [#29384793]
2
+
3
+ Bring me the passengers
4
+ http://www.pivotaltracker.com/story/show/29384793
@@ -0,0 +1,3 @@
1
+ 29384793 [Started] Bring me the passengers (http://www.pivotaltracker.com/story/show/29384793)
2
+ anthonycrumley/57348714 [Started] This is not the droid (http://www.pivotaltracker.com/story/show/57348714)
3
+ * master
@@ -0,0 +1,3 @@
1
+ 29384793
2
+ anthonycrumley/57348714
3
+ * master
@@ -0,0 +1,66 @@
1
+ [
2
+ {
3
+ "atom_enabled": false,
4
+ "version": 91975,
5
+ "name": "GradesFirst",
6
+ "velocity_averaged_over": 3,
7
+ "kind": "project",
8
+ "created_at": "2009-03-13T21:43:45Z",
9
+ "has_google_domain": false,
10
+ "point_scale_is_custom": false,
11
+ "current_iteration_number": 123,
12
+ "start_date": "2012-11-12",
13
+ "number_of_done_iterations_to_show": 4,
14
+ "updated_at": "2013-11-21T05:39:11Z",
15
+ "enable_incoming_emails": true,
16
+ "initial_velocity": 10,
17
+ "enable_following": true,
18
+ "account_id": 277647,
19
+ "start_time": "2009-03-16T05:00:00Z",
20
+ "enable_tasks": true,
21
+ "enable_planned_mode": true,
22
+ "week_start_day": "Monday",
23
+ "iteration_length": 2,
24
+ "time_zone": {
25
+ "kind": "time_zone",
26
+ "olson_name": "America/Chicago",
27
+ "offset": "-06:00"
28
+ },
29
+ "id": 10688,
30
+ "bugs_and_chores_are_estimatable": true,
31
+ "public": false,
32
+ "point_scale": "0,1,2,3,5,8"
33
+ },
34
+ {
35
+ "atom_enabled": false,
36
+ "version": 91975,
37
+ "name": "Technical",
38
+ "velocity_averaged_over": 3,
39
+ "kind": "project",
40
+ "created_at": "2009-03-13T21:43:45Z",
41
+ "has_google_domain": false,
42
+ "point_scale_is_custom": false,
43
+ "current_iteration_number": 123,
44
+ "start_date": "2012-11-12",
45
+ "number_of_done_iterations_to_show": 4,
46
+ "updated_at": "2013-11-21T05:39:11Z",
47
+ "enable_incoming_emails": true,
48
+ "initial_velocity": 10,
49
+ "enable_following": true,
50
+ "account_id": 277647,
51
+ "start_time": "2009-03-16T05:00:00Z",
52
+ "enable_tasks": true,
53
+ "enable_planned_mode": true,
54
+ "week_start_day": "Monday",
55
+ "iteration_length": 2,
56
+ "time_zone": {
57
+ "kind": "time_zone",
58
+ "olson_name": "America/Chicago",
59
+ "offset": "-06:00"
60
+ },
61
+ "id": 10687,
62
+ "bugs_and_chores_are_estimatable": true,
63
+ "public": false,
64
+ "point_scale": "0,1,2,3,5,8"
65
+ }
66
+ ]
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "Bring me the passengers",
3
+ "kind": "story",
4
+ "description": "ignore the droids",
5
+ "created_at": "2013-11-12T12:00:00Z",
6
+ "id": 29384793,
7
+ "labels":
8
+ [
9
+ ],
10
+ "estimate": 2,
11
+ "project_id": 10687,
12
+ "current_state": "started",
13
+ "url": "http://www.pivotaltracker.com/story/show/29384793",
14
+ "story_type": "feature",
15
+ "requested_by_id": 101,
16
+ "updated_at": "2013-11-12T12:00:00Z"
17
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "This is not the droid",
3
+ "kind": "story",
4
+ "description": "you are looking for.",
5
+ "created_at": "2013-11-12T12:00:00Z",
6
+ "id": 57348714,
7
+ "labels":
8
+ [
9
+ ],
10
+ "estimate": 2,
11
+ "project_id": 99,
12
+ "current_state": "started",
13
+ "url": "http://www.pivotaltracker.com/story/show/57348714",
14
+ "story_type": "feature",
15
+ "requested_by_id": 101,
16
+ "updated_at": "2013-11-12T12:00:00Z"
17
+ }
@@ -0,0 +1,22 @@
1
+ [
2
+ {
3
+ "kind": "task",
4
+ "description": "Port 0",
5
+ "created_at": "2013-12-03T12:00:00Z",
6
+ "position": 1,
7
+ "id": 5,
8
+ "complete": true,
9
+ "story_id": 558,
10
+ "updated_at": "2013-12-03T12:00:00Z"
11
+ },
12
+ {
13
+ "kind": "task",
14
+ "description": "Port 90",
15
+ "created_at": "2013-12-03T12:00:00Z",
16
+ "position": 2,
17
+ "id": 6,
18
+ "complete": false,
19
+ "story_id": 558,
20
+ "updated_at": "2013-12-03T12:00:00Z"
21
+ }
22
+ ]
@@ -0,0 +1,4 @@
1
+ Bring me the passengers (http://www.pivotaltracker.com/story/show/29384793)
2
+
3
+ [X] Port 0
4
+ [ ] Port 90
@@ -0,0 +1,68 @@
1
+ require 'test_helper'
2
+ require 'http_magic'
3
+
4
+ class HttpMagicTest < HttpMagic
5
+ end
6
+
7
+ describe 'HttpMagic#get' do
8
+ before do
9
+ @url = HttpMagicTest.url 'www.example.com'
10
+ HttpMagicTest.namespace nil
11
+ HttpMagicTest.headers nil
12
+ end
13
+
14
+ it 'should return the root response' do
15
+ stub_request(:get, "https://#{@url}:443").
16
+ to_return(body: 'I am root!!')
17
+
18
+ assert_equal(
19
+ 'I am root!!',
20
+ HttpMagicTest.get
21
+ )
22
+ end
23
+
24
+ it 'should return the named resource' do
25
+ stub_request(:get, "https://#{@url}:443/bar").
26
+ to_return(body: 'A bear walked into a bar...')
27
+
28
+ assert_equal(
29
+ 'A bear walked into a bar...',
30
+ HttpMagicTest.bar.get
31
+ )
32
+ end
33
+
34
+ it 'should return the named resource' do
35
+ stub_request(:get, "https://#{@url}:443/bar/84").
36
+ to_return(body: 'Where Everybody Knows Your Name')
37
+
38
+ assert_equal(
39
+ 'Where Everybody Knows Your Name',
40
+ HttpMagicTest.bar[84].get
41
+ )
42
+ end
43
+
44
+ it 'should serialize JSON objects' do
45
+ stub_request(:get, "https://#{@url}:443/bar/99").
46
+ to_return(
47
+ headers: { 'Content-Type' => 'application/json' },
48
+ body: fixture_file('projects.json')
49
+ )
50
+
51
+ assert_equal(
52
+ 'GradesFirst',
53
+ HttpMagicTest.bar[99].get.first['name']
54
+ )
55
+ end
56
+
57
+ it 'should provide specified headers' do
58
+ stub_request(:get, "https://#{@url}:443/bar/84").
59
+ with(headers: { 'X-AuthToken' => 'test_token' }).
60
+ to_return(body: 'Where Everybody Knows Your Name')
61
+
62
+ HttpMagicTest.headers({ 'X-AuthToken' => 'test_token' })
63
+ assert_equal(
64
+ 'Where Everybody Knows Your Name',
65
+ HttpMagicTest.bar[84].get
66
+ )
67
+ end
68
+ end
@@ -0,0 +1,76 @@
1
+ require 'test_helper'
2
+ require 'http_magic'
3
+
4
+ class HttpMagicTest < HttpMagic
5
+ end
6
+
7
+ describe 'HttpMagic#uri' do
8
+ before do
9
+ @url = HttpMagicTest.url 'www.example.com'
10
+ HttpMagicTest.namespace nil
11
+ end
12
+
13
+ it 'should make a good root uri' do
14
+ assert_equal(
15
+ "#{@url}/",
16
+ HttpMagicTest.uri
17
+ )
18
+ end
19
+
20
+ it 'should make a uri that begins with new' do
21
+ assert_equal(
22
+ "#{@url}/new",
23
+ HttpMagicTest.new.uri
24
+ )
25
+ end
26
+
27
+ it 'should make a good single part uri' do
28
+ assert_equal(
29
+ "#{@url}/foos",
30
+ HttpMagicTest.foos.uri
31
+ )
32
+ end
33
+
34
+ it 'should make a good uri with an index' do
35
+ assert_equal(
36
+ "#{@url}/foos/99",
37
+ HttpMagicTest.foos[99].uri
38
+ )
39
+ end
40
+
41
+ it 'should make a good nested uri' do
42
+ assert_equal(
43
+ "#{@url}/foos/99/bars",
44
+ HttpMagicTest.foos[99].bars.uri
45
+ )
46
+ end
47
+
48
+ it 'should make a uri including a keyword at the beginning' do
49
+ assert_equal(
50
+ "#{@url}/get",
51
+ HttpMagicTest[:get].uri
52
+ )
53
+ end
54
+
55
+ it 'should make a uri including a keyword in the middle' do
56
+ assert_equal(
57
+ "#{@url}/foo/get/bar",
58
+ HttpMagicTest.foo[:get].bar.uri
59
+ )
60
+ end
61
+
62
+ it 'should make a uri including a keyword at the end' do
63
+ assert_equal(
64
+ "#{@url}/foo/bar/get",
65
+ HttpMagicTest.foo.bar[:get].uri
66
+ )
67
+ end
68
+
69
+ it 'should make a namespaced uri' do
70
+ HttpMagicTest.namespace 'api/v5'
71
+ assert_equal(
72
+ "#{@url}/api/v5/foo/bar/get",
73
+ HttpMagicTest.foo.bar[:get].uri
74
+ )
75
+ end
76
+ end
@@ -0,0 +1,46 @@
1
+ require 'test_helper'
2
+ require 'pivotal_tracker'
3
+
4
+ describe 'PivotalTracker' do
5
+ it 'should make a good root uri' do
6
+ assert_equal(
7
+ 'www.pivotaltracker.com/services/v5',
8
+ PivotalTracker.uri
9
+ )
10
+ end
11
+
12
+ it 'should make a good project list uri' do
13
+ assert_equal(
14
+ 'www.pivotaltracker.com/services/v5/projects',
15
+ PivotalTracker.projects.uri
16
+ )
17
+ end
18
+
19
+ it 'should make a good project uri' do
20
+ assert_equal(
21
+ 'www.pivotaltracker.com/services/v5/projects/99',
22
+ PivotalTracker.projects[99].uri
23
+ )
24
+ end
25
+
26
+ it 'should make a good iterations uri' do
27
+ assert_equal(
28
+ 'www.pivotaltracker.com/services/v5/projects/99/iterations',
29
+ PivotalTracker.projects[99].iterations.uri
30
+ )
31
+ end
32
+
33
+ it 'should return a pivotal project' do
34
+ stub_pivotal_request(
35
+ :get,
36
+ 'projects/99',
37
+ body: fixture_file('projects.json')
38
+ )
39
+
40
+ PivotalTracker.api_token = 'test_token'
41
+ assert_equal(
42
+ 'GradesFirst',
43
+ PivotalTracker.projects[99].get.first['name']
44
+ )
45
+ end
46
+ end
@@ -0,0 +1,52 @@
1
+ require 'test_helper'
2
+ require 'gradesfirst/task_command'
3
+
4
+ describe GradesFirst::TaskCommand do
5
+ before do
6
+ stub_pivotal_request(
7
+ :get,
8
+ 'stories/29384799',
9
+ status: 404
10
+ )
11
+
12
+ stub_pivotal_request(
13
+ :get,
14
+ 'stories/29384793',
15
+ body: fixture_file('story_bar.json')
16
+ )
17
+
18
+ stub_pivotal_request(
19
+ :get,
20
+ 'projects/10687/stories/29384793/tasks',
21
+ body: fixture_file('tasks.json')
22
+ )
23
+
24
+ PivotalTracker.api_token = 'test_token'
25
+ end
26
+
27
+ describe '#response with a valid story' do
28
+ before do
29
+ @command = GradesFirst::TaskCommand.new
30
+ @command.stub :current_branch, '29384793' do
31
+ @command.execute
32
+ end
33
+ end
34
+
35
+ it 'should respond with a task list' do
36
+ assert_equal fixture_file('tasks.txt'), @command.response
37
+ end
38
+ end
39
+
40
+ describe '#response with an invalid story' do
41
+ before do
42
+ @command = GradesFirst::TaskCommand.new
43
+ @command.stub :current_branch, '29384799' do
44
+ @command.execute
45
+ end
46
+ end
47
+
48
+ it 'should respond with error message' do
49
+ assert_equal @command.send(:error_message), @command.response
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
2
+
3
+ require 'rubygems'
4
+ gem 'minitest' if RUBY_VERSION > '1.9'
5
+ require 'minitest/autorun'
6
+ require 'webmock/minitest'
7
+ require 'mocha/setup'
8
+ require 'pry'
9
+
10
+ # Retreive the contents of fixture files to be used in tests. It is easier
11
+ # to view, modify and visualize text used in tests when it is stored in files.
12
+ # Also, it is much easier to reuse that text.
13
+ def fixture_file(file_name)
14
+ File.open(File.dirname(__FILE__) + '/fixtures/' + file_name, 'rb').read
15
+ end
16
+
17
+ # Simplify and DRY up stubbing out of PivotalTracker requests.
18
+ def stub_pivotal_request(method, urn, options)
19
+ default_options = {
20
+ headers: { 'Content-Type' => 'application/json' },
21
+ body: '',
22
+ status: 200
23
+ }
24
+
25
+ uri = "https://www.pivotaltracker.com:443/services/v5/#{urn}"
26
+ stub_request(method, uri).
27
+ with(headers: { 'X-TrackerToken' => 'test_token' }).
28
+ to_return(default_options.merge(options))
29
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gradesfirst
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - GradesFirst
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-12-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: thor
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.14.6
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.14.6
30
+ - !ruby/object:Gem::Dependency
31
+ name: minitest
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 4.7.0
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 4.7.0
46
+ description: This utility will help manage the various tasks developers need to do
47
+ on their workstation such as database updates.
48
+ email: tech@gradesfirst.com
49
+ executables:
50
+ - gf
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - Gemfile
56
+ - Gemfile.lock
57
+ - MIT-LICENSE
58
+ - README.rdoc
59
+ - Rakefile
60
+ - bin/gf
61
+ - gradesfirst.gemspec
62
+ - lib/gradesfirst.rb
63
+ - lib/gradesfirst/branch_command.rb
64
+ - lib/gradesfirst/cli.rb
65
+ - lib/gradesfirst/command.rb
66
+ - lib/gradesfirst/commit_message_command.rb
67
+ - lib/gradesfirst/task_command.rb
68
+ - lib/http_magic.rb
69
+ - lib/pivotal_tracker.rb
70
+ - test/branch_command_test.rb
71
+ - test/cli_test.rb
72
+ - test/command_test.rb
73
+ - test/commit_message_command_test.rb
74
+ - test/fixtures/commit_message.txt
75
+ - test/fixtures/gf_branch.txt
76
+ - test/fixtures/git_branch.txt
77
+ - test/fixtures/projects.json
78
+ - test/fixtures/story_bar.json
79
+ - test/fixtures/story_foo.json
80
+ - test/fixtures/tasks.json
81
+ - test/fixtures/tasks.txt
82
+ - test/http_magic/get_test.rb
83
+ - test/http_magic/uri_test.rb
84
+ - test/pivotal_tracker_test.rb
85
+ - test/task_command_test.rb
86
+ - test/test_helper.rb
87
+ homepage: http://www.gradesfirst.com
88
+ licenses:
89
+ - MIT
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ! '>='
98
+ - !ruby/object:Gem::Version
99
+ version: 1.8.7
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: 1.3.6
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 1.8.24
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: GradesFirst command line utility for developers.
112
+ test_files: []