gradesfirst 0.2.0

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