pivotal-integration 1.6.0.1
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.
- checksums.yaml +15 -0
- data/LICENSE +202 -0
- data/NOTICE +2 -0
- data/README.md +331 -0
- data/bin/pivotal +81 -0
- data/lib/pivotal-integration/command/assign.rb +50 -0
- data/lib/pivotal-integration/command/base.rb +63 -0
- data/lib/pivotal-integration/command/command.rb +20 -0
- data/lib/pivotal-integration/command/comment.rb +31 -0
- data/lib/pivotal-integration/command/configuration.rb +129 -0
- data/lib/pivotal-integration/command/estimate.rb +53 -0
- data/lib/pivotal-integration/command/finish.rb +50 -0
- data/lib/pivotal-integration/command/info.rb +30 -0
- data/lib/pivotal-integration/command/label.rb +33 -0
- data/lib/pivotal-integration/command/mark.rb +44 -0
- data/lib/pivotal-integration/command/new.rb +52 -0
- data/lib/pivotal-integration/command/open.rb +31 -0
- data/lib/pivotal-integration/command/prepare-commit-msg.sh +26 -0
- data/lib/pivotal-integration/command/release.rb +54 -0
- data/lib/pivotal-integration/command/start.rb +82 -0
- data/lib/pivotal-integration/command/switch.rb +44 -0
- data/lib/pivotal-integration/util/git.rb +280 -0
- data/lib/pivotal-integration/util/label.rb +72 -0
- data/lib/pivotal-integration/util/shell.rb +36 -0
- data/lib/pivotal-integration/util/story.rb +170 -0
- data/lib/pivotal-integration/util/util.rb +20 -0
- data/lib/pivotal-integration/version-update/gradle.rb +64 -0
- data/lib/pivotal-integration/version-update/version_update.rb +20 -0
- data/lib/pivotal_integration.rb +18 -0
- data/spec/git-pivotal-tracker-integration/command/assign_spec.rb +55 -0
- data/spec/git-pivotal-tracker-integration/command/base_spec.rb +38 -0
- data/spec/git-pivotal-tracker-integration/command/configuration_spec.rb +119 -0
- data/spec/git-pivotal-tracker-integration/command/finish_spec.rb +45 -0
- data/spec/git-pivotal-tracker-integration/command/label_spec.rb +44 -0
- data/spec/git-pivotal-tracker-integration/command/mark_spec.rb +49 -0
- data/spec/git-pivotal-tracker-integration/command/release_spec.rb +57 -0
- data/spec/git-pivotal-tracker-integration/command/start_spec.rb +55 -0
- data/spec/git-pivotal-tracker-integration/util/git_spec.rb +235 -0
- data/spec/git-pivotal-tracker-integration/util/label_spec.rb +193 -0
- data/spec/git-pivotal-tracker-integration/util/shell_spec.rb +52 -0
- data/spec/git-pivotal-tracker-integration/util/story_spec.rb +158 -0
- data/spec/git-pivotal-tracker-integration/version-update/gradle_spec.rb +74 -0
- metadata +241 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
# Git Pivotal Tracker Integration
|
2
|
+
# Copyright (c) 2013 the original author or authors.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
require_relative 'base'
|
17
|
+
require 'pivotal-tracker'
|
18
|
+
|
19
|
+
# The class that encapsulates starting a Pivotal Tracker Story
|
20
|
+
class PivotalIntegration::Command::Start < PivotalIntegration::Command::Base
|
21
|
+
desc "Start working on a story"
|
22
|
+
|
23
|
+
# Starts a Pivotal Tracker story by doing the following steps:
|
24
|
+
# * Create a branch
|
25
|
+
# * Add default commit hook
|
26
|
+
# * Start the story on Pivotal Tracker
|
27
|
+
#
|
28
|
+
# @param [String, nil] filter a filter for selecting the story to start. This
|
29
|
+
# filter can be either:
|
30
|
+
# * a story id
|
31
|
+
# * a story type (feature, bug, chore)
|
32
|
+
# * +nil+
|
33
|
+
# @return [void]
|
34
|
+
def run(*arguments)
|
35
|
+
filter = arguments.first
|
36
|
+
use_current_branch = arguments.delete('--use-current')
|
37
|
+
|
38
|
+
if filter == 'new'
|
39
|
+
arguments.shift
|
40
|
+
story = PivotalIntegration::Util::Story.new(@project, *PivotalIntegration::Command::New.collect_type_and_name(arguments))
|
41
|
+
else
|
42
|
+
story = PivotalIntegration::Util::Story.select_story @project, filter
|
43
|
+
end
|
44
|
+
|
45
|
+
if story.estimate.nil? or story.estimate == -1
|
46
|
+
PivotalIntegration::Util::Story.estimate(story, PivotalIntegration::Command::Estimate.collect_estimation(@project))
|
47
|
+
end
|
48
|
+
|
49
|
+
PivotalIntegration::Util::Story.pretty_print story
|
50
|
+
|
51
|
+
development_branch_name = development_branch_name story
|
52
|
+
PivotalIntegration::Util::Git.switch_branch 'master' unless use_current_branch
|
53
|
+
PivotalIntegration::Util::Git.create_branch development_branch_name
|
54
|
+
@configuration.story = story
|
55
|
+
|
56
|
+
PivotalIntegration::Util::Git.add_hook 'prepare-commit-msg', File.join(File.dirname(__FILE__), 'prepare-commit-msg.sh')
|
57
|
+
|
58
|
+
start_on_tracker story
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def development_branch_name(story)
|
64
|
+
branch_name = branch_prefix(story) + ask("Enter branch name (#{branch_prefix(story)}<branch-name>): ")
|
65
|
+
puts
|
66
|
+
branch_name
|
67
|
+
end
|
68
|
+
|
69
|
+
def branch_prefix(story)
|
70
|
+
"#{story.id}-"
|
71
|
+
end
|
72
|
+
|
73
|
+
def start_on_tracker(story)
|
74
|
+
print 'Starting story on Pivotal Tracker... '
|
75
|
+
story.update(
|
76
|
+
:current_state => 'started',
|
77
|
+
:owned_by => PivotalIntegration::Util::Git.get_config('user.name')
|
78
|
+
)
|
79
|
+
puts 'OK'
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Git Pivotal Tracker Integration
|
2
|
+
# Copyright (c) 2013 the original author or authors.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
require_relative 'base'
|
17
|
+
require 'pivotal-tracker'
|
18
|
+
|
19
|
+
# The class that encapsulates assigning current Pivotal Tracker Story to a user
|
20
|
+
class PivotalIntegration::Command::Switch < PivotalIntegration::Command::Base
|
21
|
+
desc "Switch the current story to another story"
|
22
|
+
|
23
|
+
def run(*arguments)
|
24
|
+
id = arguments.first
|
25
|
+
|
26
|
+
if id == '-'
|
27
|
+
previous_story = PivotalIntegration::Util::Git.get_config('pivotal.previous-story')
|
28
|
+
abort "No previous story ID was set." unless previous_story.present?
|
29
|
+
story = @configuration.project.stories.find(previous_story)
|
30
|
+
else
|
31
|
+
story = @configuration.project.stories.find(id)
|
32
|
+
abort "A valid story ID must be provided." unless story
|
33
|
+
end
|
34
|
+
|
35
|
+
PivotalIntegration::Util::Git.set_config('pivotal.previous-story', @configuration.story.id)
|
36
|
+
|
37
|
+
@configuration.story = story
|
38
|
+
PivotalIntegration::Util::Story.pretty_print(story)
|
39
|
+
|
40
|
+
# TODO: When switching stories, switch to the correct branch as well
|
41
|
+
# TODO: Add post-checkout hook to switch to the correct story when changing branches
|
42
|
+
# http://schacon.github.io/git/githooks.html#_post_checkout
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,280 @@
|
|
1
|
+
# Git Pivotal Tracker Integration
|
2
|
+
# Copyright (c) 2013 the original author or authors.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
require_relative 'shell'
|
17
|
+
require_relative 'util'
|
18
|
+
require 'cgi'
|
19
|
+
require 'launchy'
|
20
|
+
|
21
|
+
# Utilities for dealing with Git
|
22
|
+
class PivotalIntegration::Util::Git
|
23
|
+
KEY_REMOTE = 'remote'.freeze
|
24
|
+
KEY_ROOT_BRANCH = 'root-branch'.freeze
|
25
|
+
KEY_ROOT_REMOTE = 'root-remote'.freeze
|
26
|
+
KEY_FINISH_MODE = 'finish-mode'.freeze
|
27
|
+
RELEASE_BRANCH_NAME = 'pivotal-tracker-release'.freeze
|
28
|
+
|
29
|
+
# Adds a Git hook to the current repository
|
30
|
+
#
|
31
|
+
# @param [String] name the name of the hook to add
|
32
|
+
# @param [String] source the file to use as the source for the created hook
|
33
|
+
# @param [Boolean] overwrite whether to overwrite the hook if it already exists
|
34
|
+
# @return [void]
|
35
|
+
def self.add_hook(name, source, overwrite = false)
|
36
|
+
hooks_directory = File.join repository_root, '.git', 'hooks'
|
37
|
+
hook = File.join hooks_directory, name
|
38
|
+
|
39
|
+
if overwrite || !File.exist?(hook)
|
40
|
+
print "Creating Git hook #{name}... "
|
41
|
+
|
42
|
+
FileUtils.mkdir_p hooks_directory
|
43
|
+
File.open(source, 'r') do |input|
|
44
|
+
File.open(hook, 'w') do |output|
|
45
|
+
output.write(input.read)
|
46
|
+
output.chmod(0755)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
puts 'OK'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the name of the currently checked out branch
|
55
|
+
#
|
56
|
+
# @return [String] the name of the currently checked out branch
|
57
|
+
def self.branch_name
|
58
|
+
PivotalIntegration::Util::Shell.exec('git branch').scan(/\* (.*)/)[0][0]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Creates a branch with a given +name+. First pulls the current branch to
|
62
|
+
# ensure that it is up to date and then creates and checks out the new
|
63
|
+
# branch. If specified, sets branch-specific properties that are passed in.
|
64
|
+
#
|
65
|
+
# @param [String] name the name of the branch to create
|
66
|
+
# @param [Boolean] print_messages whether to print messages
|
67
|
+
# @return [void]
|
68
|
+
def self.create_branch(name, print_messages = true)
|
69
|
+
root_branch = branch_name
|
70
|
+
root_remote = get_config KEY_REMOTE, :branch
|
71
|
+
|
72
|
+
if print_messages; print "Pulling #{root_branch}... " end
|
73
|
+
PivotalIntegration::Util::Shell.exec 'git pull --quiet --ff-only'
|
74
|
+
if print_messages; puts 'OK'
|
75
|
+
end
|
76
|
+
|
77
|
+
if print_messages; print "Creating and checking out #{name}... " end
|
78
|
+
PivotalIntegration::Util::Shell.exec "git checkout --quiet -B #{name}"
|
79
|
+
set_config KEY_ROOT_BRANCH, root_branch, :branch
|
80
|
+
set_config KEY_ROOT_REMOTE, root_remote, :branch
|
81
|
+
if print_messages; puts 'OK'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.switch_branch(name)
|
86
|
+
PivotalIntegration::Util::Shell.exec "git checkout --quiet #{name}"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Creates a commit with a given message. The commit includes all change
|
90
|
+
# files.
|
91
|
+
#
|
92
|
+
# @param [String] message The commit message, which will be appended with
|
93
|
+
# +[#<story-id]+
|
94
|
+
# @param [PivotalTracker::Story] story the story associated with the current
|
95
|
+
# commit
|
96
|
+
# @return [void]
|
97
|
+
def self.create_commit(message, story)
|
98
|
+
PivotalIntegration::Util::Shell.exec "git commit --quiet --all --allow-empty --message \"#{message}\n\n[##{story.id}]\""
|
99
|
+
end
|
100
|
+
|
101
|
+
# Creates a tag with the given name. Before creating the tag, commits all
|
102
|
+
# outstanding changes with a commit message that reflects that these changes
|
103
|
+
# are for a release.
|
104
|
+
#
|
105
|
+
# @param [String] name the name of the tag to create
|
106
|
+
# @param [PivotalTracker::Story] story the story associated with the current
|
107
|
+
# tag
|
108
|
+
# @return [void]
|
109
|
+
def self.create_release_tag(name, story)
|
110
|
+
root_branch = branch_name
|
111
|
+
|
112
|
+
print "Creating tag v#{name}... "
|
113
|
+
|
114
|
+
create_branch RELEASE_BRANCH_NAME, false
|
115
|
+
create_commit "#{name} Release", story
|
116
|
+
PivotalIntegration::Util::Shell.exec "git tag v#{name}"
|
117
|
+
PivotalIntegration::Util::Shell.exec "git checkout --quiet #{root_branch}"
|
118
|
+
PivotalIntegration::Util::Shell.exec "git branch --quiet -D #{RELEASE_BRANCH_NAME}"
|
119
|
+
|
120
|
+
puts 'OK'
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns a Git configuration value. This value is read using the +git
|
124
|
+
# config+ command. The scope of the value to read can be controlled with the
|
125
|
+
# +scope+ parameter.
|
126
|
+
#
|
127
|
+
# @param [String] key the key of the configuration to retrieve
|
128
|
+
# @param [:branch, :inherited] scope the scope to read the configuration from
|
129
|
+
# * +:branch+: equivalent to calling +git config branch.branch-name.key+
|
130
|
+
# * +:inherited+: equivalent to calling +git config key+
|
131
|
+
# @return [String] the value of the configuration
|
132
|
+
# @raise if the specified scope is not +:branch+ or +:inherited+
|
133
|
+
def self.get_config(key, scope = :inherited)
|
134
|
+
if :branch == scope
|
135
|
+
PivotalIntegration::Util::Shell.exec("git config branch.#{branch_name}.#{key}", false).strip
|
136
|
+
elsif :inherited == scope
|
137
|
+
PivotalIntegration::Util::Shell.exec("git config #{key}", false).strip
|
138
|
+
else
|
139
|
+
raise "Unable to get Git configuration for scope '#{scope}'"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.get_config_with_default(key, default = nil, scope = :inherited)
|
144
|
+
value = get_config(key, scope)
|
145
|
+
value.blank? ? default : value
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.finish_mode
|
149
|
+
get_config_with_default('pivotal.finish-mode', :merge).to_sym
|
150
|
+
end
|
151
|
+
|
152
|
+
# Merges the current branch to its root branch and deletes the current branch
|
153
|
+
#
|
154
|
+
# @param [PivotalTracker::Story] story the story associated with the current branch
|
155
|
+
# @param [Boolean] no_complete whether to suppress the +Completes+ statement in the commit message
|
156
|
+
# @param [Boolean] no_delete whether to delete development branch
|
157
|
+
# @return [void]
|
158
|
+
def self.merge(story, no_complete, no_delete)
|
159
|
+
development_branch = branch_name
|
160
|
+
root_branch = get_config KEY_ROOT_BRANCH, :branch
|
161
|
+
|
162
|
+
print "Merging #{development_branch} to #{root_branch}... "
|
163
|
+
PivotalIntegration::Util::Shell.exec "git checkout --quiet #{root_branch}"
|
164
|
+
PivotalIntegration::Util::Shell.exec "git merge --quiet --no-ff -m \"Merge #{development_branch} to #{root_branch}\n\n[#{no_complete ? '' : 'Completes '}##{story.id}]\" #{development_branch}"
|
165
|
+
puts 'OK'
|
166
|
+
|
167
|
+
unless no_delete
|
168
|
+
print "Deleting #{development_branch}... "
|
169
|
+
PivotalIntegration::Util::Shell.exec "git branch --quiet -D #{development_branch}"
|
170
|
+
puts 'OK'
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def self.create_pull_request(story)
|
175
|
+
case get_config_with_default('pivotal.pull-request-editor', :web).to_sym
|
176
|
+
when :web
|
177
|
+
# Open the PR editor in a web browser
|
178
|
+
repo = get_config('remote.origin.url')[/(?<=git@github.com:)[a-z0-9_-]+\/[a-z0-9_-]+/]
|
179
|
+
title = CGI::escape("#{story.name} [##{story.id}]")
|
180
|
+
Launchy.open "https://github.com/#{repo}/compare/#{branch_name}?expand=1&pull_request[title]=#{title}"
|
181
|
+
|
182
|
+
else
|
183
|
+
print 'Checking for hub installation... '
|
184
|
+
if PivotalIntegration::Util::Shell.exec('which hub', false).empty?
|
185
|
+
puts "FAIL"
|
186
|
+
puts "Hub required to use this feature (brew install hub / https://github.com/github/hub)."
|
187
|
+
abort
|
188
|
+
else
|
189
|
+
puts "OK"
|
190
|
+
end
|
191
|
+
|
192
|
+
puts "Creating a pull request for #{branch_name}... "
|
193
|
+
system "hub pull-request"
|
194
|
+
puts 'OK'
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Push changes to the remote of the current branch
|
199
|
+
#
|
200
|
+
# @param [String] refs the explicit references to push
|
201
|
+
# @return [void]
|
202
|
+
def self.push(*refs)
|
203
|
+
remote = get_config KEY_REMOTE, :branch
|
204
|
+
|
205
|
+
print "Pushing to #{remote}... "
|
206
|
+
PivotalIntegration::Util::Shell.exec "git push --quiet origin #{remote} " + refs.join(' ')
|
207
|
+
puts 'OK'
|
208
|
+
end
|
209
|
+
|
210
|
+
# Returns the root path of the current Git repository. The root is
|
211
|
+
# determined by ascending the path hierarchy, starting with the current
|
212
|
+
# working directory (+Dir#pwd+), until a directory is found that contains a
|
213
|
+
# +.git/+ sub directory.
|
214
|
+
#
|
215
|
+
# @return [String] the root path of the Git repository
|
216
|
+
# @raise if the current working directory is not in a Git repository
|
217
|
+
def self.repository_root
|
218
|
+
repository_root = Dir.pwd
|
219
|
+
|
220
|
+
until Dir.entries(repository_root).any? { |child| File.directory?(child) && (child =~ /^.git$/) }
|
221
|
+
next_repository_root = File.expand_path('..', repository_root)
|
222
|
+
abort('Current working directory is not in a Git repository') unless repository_root != next_repository_root
|
223
|
+
repository_root = next_repository_root
|
224
|
+
end
|
225
|
+
|
226
|
+
repository_root
|
227
|
+
end
|
228
|
+
|
229
|
+
# Sets a Git configuration value. This value is set using the +git config+
|
230
|
+
# command. The scope of the set value can be controlled with the +scope+
|
231
|
+
# parameter.
|
232
|
+
#
|
233
|
+
# @param [String] key the key of configuration to store
|
234
|
+
# @param [String] value the value of the configuration to store
|
235
|
+
# @param [:branch, :global, :local] scope the scope to store the configuration value in.
|
236
|
+
# * +:branch+: equivalent to calling +git config --local branch.branch-name.key value+
|
237
|
+
# * +:global+: equivalent to calling +git config --global key value+
|
238
|
+
# * +:local+: equivalent to calling +git config --local key value+
|
239
|
+
# @return [void]
|
240
|
+
# @raise if the specified scope is not +:branch+, +:global+, or +:local+
|
241
|
+
def self.set_config(key, value, scope = :local)
|
242
|
+
if :branch == scope
|
243
|
+
PivotalIntegration::Util::Shell.exec "git config --local branch.#{branch_name}.#{key} #{value}"
|
244
|
+
elsif :global == scope
|
245
|
+
PivotalIntegration::Util::Shell.exec "git config --global #{key} #{value}"
|
246
|
+
elsif :local == scope
|
247
|
+
PivotalIntegration::Util::Shell.exec "git config --local #{key} #{value}"
|
248
|
+
else
|
249
|
+
raise "Unable to set Git configuration for scope '#{scope}'"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Checks whether merging the current branch back to its root branch would be
|
254
|
+
# a trivial merge. A trivial merge is defined as one where the net change
|
255
|
+
# of the merge would be the same as the net change of the branch being
|
256
|
+
# merged. The easiest way to ensure that a merge is trivial is to rebase a
|
257
|
+
# development branch onto the tip of its root branch.
|
258
|
+
#
|
259
|
+
# @return [void]
|
260
|
+
def self.trivial_merge?
|
261
|
+
development_branch = branch_name
|
262
|
+
root_branch = get_config KEY_ROOT_BRANCH, :branch
|
263
|
+
|
264
|
+
print "Checking for trivial merge from #{development_branch} to #{root_branch}... "
|
265
|
+
|
266
|
+
PivotalIntegration::Util::Shell.exec "git checkout --quiet #{root_branch}"
|
267
|
+
PivotalIntegration::Util::Shell.exec 'git pull --quiet --ff-only'
|
268
|
+
PivotalIntegration::Util::Shell.exec "git checkout --quiet #{development_branch}"
|
269
|
+
|
270
|
+
root_tip = PivotalIntegration::Util::Shell.exec "git rev-parse #{root_branch}"
|
271
|
+
common_ancestor = PivotalIntegration::Util::Shell.exec "git merge-base #{root_branch} #{development_branch}"
|
272
|
+
|
273
|
+
if root_tip != common_ancestor
|
274
|
+
abort 'FAIL'
|
275
|
+
end
|
276
|
+
|
277
|
+
puts 'OK'
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# Git Pivotal Tracker Integration
|
2
|
+
# Copyright (c) 2013 the original author or authors.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
require_relative 'util'
|
17
|
+
|
18
|
+
# Utilities for dealing with the shell
|
19
|
+
class PivotalIntegration::Util::Label
|
20
|
+
|
21
|
+
# Add labels to story if they are not already appended to story.
|
22
|
+
#
|
23
|
+
# @param [PivotalTracker::Story, String] labels as Strings, one label per parameter.
|
24
|
+
# @return [boolean] Boolean defining whether story was updated or not.
|
25
|
+
def self.add(story, *labels)
|
26
|
+
current_labels = story.labels.split(',') rescue []
|
27
|
+
new_labels = current_labels | labels
|
28
|
+
if story.update(:labels => new_labels)
|
29
|
+
puts "Updated labels on #{story.name}:"
|
30
|
+
puts "#{current_labels} => #{new_labels}"
|
31
|
+
else
|
32
|
+
abort("Failed to update labels on Pivotal Tracker")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Add labels from story and remove those labels from every other story in a project.
|
37
|
+
#
|
38
|
+
# @param [PivotalTracker::Story, String] labels as Strings, one label per parameter.
|
39
|
+
# @return [boolean] Boolean defining whether story was updated or not.
|
40
|
+
def self.once(story, *labels)
|
41
|
+
PivotalTracker::Project.find(story.project_id).stories.all.each do |other_story|
|
42
|
+
self.remove(other_story, *labels) if story.name != other_story.name and
|
43
|
+
other_story.labels and
|
44
|
+
(other_story.labels.split(',') & labels).any?
|
45
|
+
end
|
46
|
+
self.add(story, *labels)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Remove labels from story.
|
50
|
+
#
|
51
|
+
# @param [PivotalTracker::Story, String] labels as Strings, one label per parameter.
|
52
|
+
# @return [boolean] Boolean defining whether story was updated or not.
|
53
|
+
def self.remove(story, *labels)
|
54
|
+
current_labels = story.labels.split(',') rescue []
|
55
|
+
new_labels = current_labels - labels
|
56
|
+
if story.update(:labels => new_labels)
|
57
|
+
puts "Updated labels on #{story.name}:"
|
58
|
+
puts "#{current_labels} => #{new_labels}"
|
59
|
+
else
|
60
|
+
abort("Failed to update labels on Pivotal Tracker")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Print labels from story.
|
65
|
+
#
|
66
|
+
# @param [PivotalTracker::Story, String] labels as Strings, one label per parameter.
|
67
|
+
# @return [boolean] Boolean defining whether story was updated or not.
|
68
|
+
def self.list(story)
|
69
|
+
puts "Story labels:"
|
70
|
+
puts story.labels.split(',') rescue []
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Git Pivotal Tracker Integration
|
2
|
+
# Copyright (c) 2013 the original author or authors.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
require_relative 'util'
|
17
|
+
|
18
|
+
# Utilities for dealing with the shell
|
19
|
+
class PivotalIntegration::Util::Shell
|
20
|
+
|
21
|
+
# Executes a command
|
22
|
+
#
|
23
|
+
# @param [String] command the command to execute
|
24
|
+
# @param [Boolean] abort_on_failure whether to +Kernel#abort+ with +FAIL+ as
|
25
|
+
# the message when the command's +Status#existstatus+ is not +0+
|
26
|
+
# @return [String] the result of the command
|
27
|
+
def self.exec(command, abort_on_failure = true)
|
28
|
+
result = `#{command}`
|
29
|
+
if $?.exitstatus != 0 && abort_on_failure
|
30
|
+
abort 'FAIL'
|
31
|
+
end
|
32
|
+
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# Git Pivotal Tracker Integration
|
2
|
+
# Copyright (c) 2013 the original author or authors.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
require_relative 'util'
|
17
|
+
require 'highline/import'
|
18
|
+
require 'pivotal-tracker'
|
19
|
+
|
20
|
+
# Utilities for dealing with +PivotalTracker::Story+s
|
21
|
+
class PivotalIntegration::Util::Story
|
22
|
+
|
23
|
+
def self.new(project, name, type)
|
24
|
+
project.stories.create(name: name, story_type: type)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Print a human readable version of a story. This pretty prints the title,
|
28
|
+
# description, and notes for the story.
|
29
|
+
#
|
30
|
+
# @param [PivotalTracker::Story] story the story to pretty print
|
31
|
+
# @return [void]
|
32
|
+
def self.pretty_print(story)
|
33
|
+
print_label 'ID'
|
34
|
+
print_value story.id
|
35
|
+
|
36
|
+
print_label 'Project'
|
37
|
+
print_value PivotalTracker::Project.find(story.project_id).account
|
38
|
+
|
39
|
+
print_label LABEL_TITLE
|
40
|
+
print_value story.name
|
41
|
+
|
42
|
+
description = story.description
|
43
|
+
if !description.nil? && !description.empty?
|
44
|
+
print_label 'Description'
|
45
|
+
print_value description
|
46
|
+
end
|
47
|
+
|
48
|
+
print_label 'Type'
|
49
|
+
print_value story.story_type.titlecase
|
50
|
+
|
51
|
+
print_label 'State'
|
52
|
+
print_value story.current_state.titlecase
|
53
|
+
|
54
|
+
print_label 'Estimate'
|
55
|
+
print_value story.estimate == -1 ? 'Unestimated' : story.estimate
|
56
|
+
|
57
|
+
PivotalTracker::Note.all(story).sort_by { |note| note.noted_at }.each_with_index do |note, index|
|
58
|
+
print_label "Note #{index + 1}"
|
59
|
+
print_value note.text
|
60
|
+
end
|
61
|
+
|
62
|
+
puts
|
63
|
+
end
|
64
|
+
|
65
|
+
# Assign story to pivotal tracker member.
|
66
|
+
#
|
67
|
+
# @param [PivotalTracker::Story] story to be assigned
|
68
|
+
# @param [PivotalTracker::Member] assigned user
|
69
|
+
# @return [void]
|
70
|
+
def self.assign(story, username)
|
71
|
+
puts "Story assigned to #{username}" if story.update(owned_by: username)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Marks Pivotal Tracker story with given state
|
75
|
+
#
|
76
|
+
# @param [PivotalTracker::Story] story to be assigned
|
77
|
+
# @param [PivotalTracker::Member] assigned user
|
78
|
+
# @return [void]
|
79
|
+
def self.mark(story, state)
|
80
|
+
puts "Changed state to #{state}" if story.update(current_state: state)
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.estimate(story, points)
|
84
|
+
story.update(estimate: points)
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.add_comment(story, comment)
|
88
|
+
story.notes.create(text: comment)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Selects a Pivotal Tracker story by doing the following steps:
|
92
|
+
#
|
93
|
+
# @param [PivotalTracker::Project] project the project to select stories from
|
94
|
+
# @param [String, nil] filter a filter for selecting the story to start. This
|
95
|
+
# filter can be either:
|
96
|
+
# * a story id: selects the story represented by the id
|
97
|
+
# * a story type (feature, bug, chore): offers the user a selection of stories of the given type
|
98
|
+
# * +nil+: offers the user a selection of stories of all types
|
99
|
+
# @param [Fixnum] limit The number maximum number of stories the user can choose from
|
100
|
+
# @return [PivotalTracker::Story] The Pivotal Tracker story selected by the user
|
101
|
+
def self.select_story(project, filter = nil, limit = 5)
|
102
|
+
if filter =~ /[[:digit:]]/
|
103
|
+
story = project.stories.find filter.to_i
|
104
|
+
else
|
105
|
+
story = find_story project, filter, limit
|
106
|
+
end
|
107
|
+
|
108
|
+
story
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
CANDIDATE_STATES = %w(rejected unstarted unscheduled).freeze
|
114
|
+
|
115
|
+
LABEL_DESCRIPTION = 'Description'.freeze
|
116
|
+
|
117
|
+
LABEL_TITLE = 'Title'.freeze
|
118
|
+
|
119
|
+
LABEL_WIDTH = (LABEL_DESCRIPTION.length + 2).freeze
|
120
|
+
|
121
|
+
CONTENT_WIDTH = (HighLine.new.output_cols - LABEL_WIDTH).freeze
|
122
|
+
|
123
|
+
def self.print_label(label)
|
124
|
+
print "%#{LABEL_WIDTH}s" % ["#{label}: "]
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.print_value(value)
|
128
|
+
value = value.to_s
|
129
|
+
|
130
|
+
if value.blank?
|
131
|
+
puts ''
|
132
|
+
else
|
133
|
+
value.scan(/\S.{0,#{CONTENT_WIDTH - 2}}\S(?=\s|$)|\S+/).each_with_index do |line, index|
|
134
|
+
if index == 0
|
135
|
+
puts line
|
136
|
+
else
|
137
|
+
puts "%#{LABEL_WIDTH}s%s" % ['', line]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.find_story(project, type, limit)
|
144
|
+
criteria = {
|
145
|
+
:current_state => CANDIDATE_STATES
|
146
|
+
}
|
147
|
+
if type
|
148
|
+
criteria[:story_type] = type
|
149
|
+
end
|
150
|
+
|
151
|
+
candidates = project.stories.all(criteria).sort_by{ |s| s.owned_by == @user ? 1 : 0 }.slice(0..limit)
|
152
|
+
if candidates.length == 1
|
153
|
+
story = candidates[0]
|
154
|
+
else
|
155
|
+
story = choose do |menu|
|
156
|
+
menu.prompt = 'Choose story to start: '
|
157
|
+
|
158
|
+
candidates.each do |story|
|
159
|
+
name = story.owned_by ? '[%s] ' % story.owned_by : ''
|
160
|
+
name += type ? story.name : '%-7s %s' % [story.story_type.upcase, story.name]
|
161
|
+
menu.choice(name) { story }
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
puts
|
166
|
+
end
|
167
|
+
|
168
|
+
story
|
169
|
+
end
|
170
|
+
end
|