git-pivotal-tracker-integration 1.1.0 → 1.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -1
  3. data/bin/git-finish +16 -2
  4. data/bin/git-release +19 -0
  5. data/bin/git-start +16 -2
  6. data/lib/git-pivotal-tracker-integration/command/base.rb +47 -0
  7. data/lib/git-pivotal-tracker-integration/{base.rb → command/command.rb} +3 -14
  8. data/lib/git-pivotal-tracker-integration/command/configuration.rb +92 -0
  9. data/lib/git-pivotal-tracker-integration/command/finish.rb +36 -0
  10. data/lib/git-pivotal-tracker-integration/command/prepare-commit-msg.sh +26 -0
  11. data/lib/git-pivotal-tracker-integration/command/release.rb +56 -0
  12. data/lib/git-pivotal-tracker-integration/command/start.rb +64 -0
  13. data/lib/git-pivotal-tracker-integration/util/git.rb +242 -0
  14. data/lib/git-pivotal-tracker-integration/util/shell.rb +36 -0
  15. data/lib/git-pivotal-tracker-integration/util/story.rb +128 -0
  16. data/lib/git-pivotal-tracker-integration/util/util.rb +20 -0
  17. data/lib/git-pivotal-tracker-integration/version-update/gradle.rb +64 -0
  18. data/lib/git-pivotal-tracker-integration/version-update/version_update.rb +20 -0
  19. data/lib/git_pivotal_tracker_integration.rb +18 -0
  20. data/spec/git-pivotal-tracker-integration/command/base_spec.rb +38 -0
  21. data/spec/git-pivotal-tracker-integration/command/configuration_spec.rb +91 -0
  22. data/spec/git-pivotal-tracker-integration/command/finish_spec.rb +45 -0
  23. data/spec/git-pivotal-tracker-integration/command/release_spec.rb +57 -0
  24. data/spec/git-pivotal-tracker-integration/command/start_spec.rb +50 -0
  25. data/spec/git-pivotal-tracker-integration/util/git_spec.rb +239 -0
  26. data/spec/git-pivotal-tracker-integration/util/shell_spec.rb +52 -0
  27. data/spec/git-pivotal-tracker-integration/util/story_spec.rb +137 -0
  28. data/spec/git-pivotal-tracker-integration/version-update/gradle_spec.rb +74 -0
  29. metadata +104 -21
  30. data/lib/git-pivotal-tracker-integration/finish.rb +0 -106
  31. data/lib/git-pivotal-tracker-integration/pivotal_configuration.rb +0 -90
  32. data/lib/git-pivotal-tracker-integration/prepare-commit-msg.sh +0 -12
  33. data/lib/git-pivotal-tracker-integration/start.rb +0 -167
@@ -0,0 +1,242 @@
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 "git-pivotal-tracker-integration/util/shell"
17
+ require "git-pivotal-tracker-integration/util/util"
18
+
19
+ # Utilities for dealing with Git
20
+ class GitPivotalTrackerIntegration::Util::Git
21
+
22
+ # Adds a Git hook to the current repository
23
+ #
24
+ # @param [String] name the name of the hook to add
25
+ # @param [String] source the file to use as the source for the created hook
26
+ # @param [Boolean] overwrite whether to overwrite the hook if it already exists
27
+ # @return [void]
28
+ def self.add_hook(name, source, overwrite = false)
29
+ hooks_directory = File.join repository_root, ".git", "hooks"
30
+ hook = File.join hooks_directory, name
31
+
32
+ if overwrite || !File.exist?(hook)
33
+ print "Creating Git hook #{name}... "
34
+
35
+ FileUtils.mkdir_p hooks_directory
36
+ File.open(source, "r") do |input|
37
+ File.open(hook, "w") do |output|
38
+ output.write(input.read)
39
+ output.chmod(0755)
40
+ end
41
+ end
42
+
43
+ puts "OK"
44
+ end
45
+ end
46
+
47
+ # Returns the name of the currently checked out branch
48
+ #
49
+ # @return [String] the name of the currently checked out branch
50
+ def self.branch_name
51
+ GitPivotalTrackerIntegration::Util::Shell.exec("git branch").scan(/\* (.*)/)[0][0]
52
+ end
53
+
54
+ # Creates a branch with a given +name+. First pulls the current branch to
55
+ # ensure that it is up to date and then creates and checks out the new
56
+ # branch. If specified, sets branch-specific properties that are passed in.
57
+ #
58
+ # @param [String] name the name of the branch to create
59
+ # @param [Boolean] print_messages whether to print messages
60
+ # @return [void]
61
+ def self.create_branch(name, print_messages = true)
62
+ root_branch = branch_name
63
+ root_remote = get_config @@KEY_REMOTE, :branch
64
+
65
+ if print_messages; print "Pulling #{root_branch}... " end
66
+ GitPivotalTrackerIntegration::Util::Shell.exec "git pull --quiet --ff-only"
67
+ if print_messages; puts "OK" end
68
+
69
+ if print_messages; print "Creating and checking out #{name}... " end
70
+ GitPivotalTrackerIntegration::Util::Shell.exec "git checkout --quiet -b #{name}"
71
+ set_config @@KEY_ROOT_BRANCH, root_branch, :branch
72
+ set_config @@KEY_ROOT_REMOTE, root_remote, :branch
73
+ if print_messages; puts "OK" end
74
+ end
75
+
76
+ # Creates a commit with a given message. The commit includes all change
77
+ # files.
78
+ #
79
+ # @param [String] message The commit message, which will be appended with
80
+ # +[#<story-id]+
81
+ # @param [PivotalTracker::Story] story the story associated with the current
82
+ # commit
83
+ # @return [void]
84
+ def self.create_commit(message, story)
85
+ GitPivotalTrackerIntegration::Util::Shell.exec "git commit --quiet --all --allow-empty --message \"#{message}\n\n[##{story.id}]\""
86
+ end
87
+
88
+ # Creates a tag with the given name. Before creating the tag, commits all
89
+ # outstanding changes with a commit message that reflects that these changes
90
+ # are for a release.
91
+ #
92
+ # @param [String] name the name of the tag to create
93
+ # @param [PivotalTracker::Story] story the story associated with the current
94
+ # tag
95
+ # @return [void]
96
+ def self.create_release_tag(name, story)
97
+ root_branch = branch_name
98
+
99
+ print "Creating tag v#{name}... "
100
+
101
+ create_branch @@RELEASE_BRANCH_NAME, false
102
+ create_commit "#{name} Release", story
103
+ GitPivotalTrackerIntegration::Util::Shell.exec "git tag v#{name}"
104
+ GitPivotalTrackerIntegration::Util::Shell.exec "git checkout --quiet #{root_branch}"
105
+ GitPivotalTrackerIntegration::Util::Shell.exec "git branch --quiet -D #{@@RELEASE_BRANCH_NAME}"
106
+
107
+ puts "OK"
108
+ end
109
+
110
+ # Returns a Git configuration value. This value is read using the +git
111
+ # config+ command. The scope of the value to read can be controlled with the
112
+ # +scope+ parameter.
113
+ #
114
+ # @param [String] key the key of the configuration to retrieve
115
+ # @param [:branch, :inherited] scope the scope to read the configuration from
116
+ # * +:branch+: equivalent to calling +git config branch.branch-name.key+
117
+ # * +:inherited+: equivalent to calling +git config key+
118
+ # @return [String] the value of the configuration
119
+ # @raise if the specified scope is not +:branch+ or +:inherited+
120
+ def self.get_config(key, scope = :inherited)
121
+ if :branch == scope
122
+ GitPivotalTrackerIntegration::Util::Shell.exec("git config branch.#{branch_name}.#{key}", false).strip
123
+ elsif :inherited == scope
124
+ GitPivotalTrackerIntegration::Util::Shell.exec("git config #{key}", false).strip
125
+ else
126
+ raise "Unable to get Git configuration for scope '#{scope}'"
127
+ end
128
+ end
129
+
130
+ # Merges the current branch to its root branch and deletes the current branch
131
+ #
132
+ # @param [PivotalTracker::Story] story the story associated with the current
133
+ # branch
134
+ # @return [void]
135
+ def self.merge(story)
136
+ development_branch = branch_name
137
+ root_branch = get_config @@KEY_ROOT_BRANCH, :branch
138
+
139
+ print "Merging #{development_branch} to #{root_branch}... "
140
+ GitPivotalTrackerIntegration::Util::Shell.exec "git checkout --quiet #{root_branch}"
141
+ GitPivotalTrackerIntegration::Util::Shell.exec "git merge --quiet --no-ff -m \"Merge #{development_branch} to #{root_branch}\n\n[Completes ##{story.id}]\" #{development_branch}"
142
+ puts "OK"
143
+
144
+ print "Deleting #{development_branch}... "
145
+ GitPivotalTrackerIntegration::Util::Shell.exec "git branch --quiet -D #{development_branch}"
146
+ puts "OK"
147
+ end
148
+
149
+ # Push changes to the remote of the current branch
150
+ #
151
+ # @param [String] refs the explicit references to push
152
+ # @return [void]
153
+ def self.push(*refs)
154
+ remote = get_config @@KEY_REMOTE, :branch
155
+
156
+ print "Pushing to #{remote}... "
157
+ GitPivotalTrackerIntegration::Util::Shell.exec "git push --quiet #{remote} " + refs.join(" ")
158
+ puts "OK"
159
+ end
160
+
161
+ # Returns the root path of the current Git repository. The root is
162
+ # determined by ascending the path hierarcy, starting with the current
163
+ # working directory (+Dir#pwd+), until a directory is found that contains a
164
+ # +.git/+ sub directory.
165
+ #
166
+ # @return [String] the root path of the Git repository
167
+ # @raise if the current working directory is not in a Git repository
168
+ def self.repository_root
169
+ repository_root = Dir.pwd
170
+
171
+ until Dir.entries(repository_root).any? { |child| File.directory?(child) && (child =~ /^.git$/) }
172
+ next_repository_root = File.expand_path("..", repository_root)
173
+ abort("Current working directory is not in a Git repository") unless repository_root != next_repository_root
174
+ repository_root = next_repository_root
175
+ end
176
+
177
+ repository_root
178
+ end
179
+
180
+ # Sets a Git configuration value. This value is set using the +git config+
181
+ # command. The scope of the set value can be controlled with the +scope+
182
+ # parameter.
183
+ #
184
+ # @param [String] key the key of configuration to store
185
+ # @param [String] value the value of the configuration to store
186
+ # @param [:branch, :global, :local] scope the scope to store the configuration value in.
187
+ # * +:branch+: equivalent to calling +git config --local branch.branch-name.key value+
188
+ # * +:global+: equivalent to calling +git config --global key value+
189
+ # * +:local+: equivalent to calling +git config --local key value+
190
+ # @return [void]
191
+ # @raise if the specified scope is not +:branch+, +:global+, or +:local+
192
+ def self.set_config(key, value, scope = :local)
193
+ if :branch == scope
194
+ GitPivotalTrackerIntegration::Util::Shell.exec "git config --local branch.#{branch_name}.#{key} #{value}"
195
+ elsif :global == scope
196
+ GitPivotalTrackerIntegration::Util::Shell.exec "git config --global #{key} #{value}"
197
+ elsif :local == scope
198
+ GitPivotalTrackerIntegration::Util::Shell.exec "git config --local #{key} #{value}"
199
+ else
200
+ raise "Unable to set Git configuration for scope '#{scope}'"
201
+ end
202
+ end
203
+
204
+ # Checks whether merging the current branch back to its root branch would be
205
+ # a trivial merge. A trivial merge is defined as one where the net change
206
+ # of the merge would be the same as the net change of the branch being
207
+ # merged. The easiest way to ensure that a merge is trivial is to rebase a
208
+ # development branch onto the tip of its root branch.
209
+ #
210
+ # @return [void]
211
+ def self.trivial_merge?
212
+ development_branch = branch_name
213
+ root_branch = get_config @@KEY_ROOT_BRANCH, :branch
214
+ root_remote = get_config @@KEY_ROOT_REMOTE, :branch
215
+
216
+ print "Checking for trivial merge from #{development_branch} to #{root_branch}... "
217
+
218
+ GitPivotalTrackerIntegration::Util::Shell.exec "git fetch #{root_remote}"
219
+
220
+ remote_tip = GitPivotalTrackerIntegration::Util::Shell.exec "git rev-parse #{root_remote}/#{root_branch}"
221
+ local_tip = GitPivotalTrackerIntegration::Util::Shell.exec "git rev-parse #{root_branch}"
222
+ common_ancestor = GitPivotalTrackerIntegration::Util::Shell.exec "git merge-base #{root_branch} #{development_branch}"
223
+
224
+ if remote_tip != local_tip || local_tip != common_ancestor
225
+ abort "FAIL"
226
+ end
227
+
228
+ puts "OK"
229
+ end
230
+
231
+ private
232
+
233
+ @@KEY_REMOTE = "remote"
234
+
235
+ @@KEY_ROOT_BRANCH = "root-branch"
236
+
237
+ @@KEY_ROOT_REMOTE = "root-remote"
238
+
239
+ @@RELEASE_BRANCH_NAME = "pivotal-tracker-release"
240
+
241
+ end
242
+
@@ -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 "git-pivotal-tracker-integration/util/util"
17
+
18
+ # Utilties for dealing with the shell
19
+ class GitPivotalTrackerIntegration::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,128 @@
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 "git-pivotal-tracker-integration/util/util"
17
+ require "highline/import"
18
+ require "pivotal-tracker"
19
+
20
+ # Utilties for dealing with +PivotalTracker::Story+s
21
+ class GitPivotalTrackerIntegration::Util::Story
22
+
23
+ # Print a human readable version of a story. This pretty prints the title,
24
+ # description, and notes for the story.
25
+ #
26
+ # @param [PivotalTracker::Story] story the story to pretty print
27
+ # @return [void]
28
+ def self.pretty_print(story)
29
+ print_label @@LABEL_TITLE
30
+ print_value story.name
31
+
32
+ description = story.description
33
+ if !description.nil? && !description.empty?
34
+ print_label "Description"
35
+ print_value description
36
+ end
37
+
38
+ PivotalTracker::Note.all(story).sort_by { |note| note.noted_at }.each_with_index do |note, index|
39
+ print_label "Note #{index + 1}"
40
+ print_value note.text
41
+ end
42
+
43
+ puts
44
+ end
45
+
46
+ # Selects a Pivotal Tracker story by doing the following steps:
47
+ #
48
+ # @param [PivotalTracker::Project] project the project to select stories from
49
+ # @param [String, nil] filter a filter for selecting the story to start. This
50
+ # filter can be either:
51
+ # * a story id: selects the story represented by the id
52
+ # * a story type (feature, bug, chore): offers the user a selection of stories of the given type
53
+ # * +nil+: offers the user a selection of stories of all types
54
+ # @param [Fixnum] limit The number maximum number of stories the user can choose from
55
+ # @return [PivotalTracker::Story] The Pivotal Tracker story selected by the user
56
+ def self.select_story(project, filter = nil, limit = 5)
57
+ story = nil
58
+
59
+ if filter =~ /[[:digit:]]/
60
+ story = project.stories.find filter.to_i
61
+ else
62
+ story = find_story project, filter, limit
63
+ end
64
+
65
+ story
66
+ end
67
+
68
+ private
69
+
70
+ @@CANDIDATE_STATES = ["rejected", "unstarted", "unscheduled"]
71
+
72
+ @@LABEL_DESCRIPTION = "Description"
73
+
74
+ @@LABEL_TITLE = "Title"
75
+
76
+ @@LABEL_WIDTH = @@LABEL_DESCRIPTION.length + 2
77
+
78
+ @@CONTENT_WIDTH = HighLine.new.output_cols - @@LABEL_WIDTH
79
+
80
+ def self.print_label(label)
81
+ print "%#{@@LABEL_WIDTH}s" % ["#{label}: "]
82
+ end
83
+
84
+ def self.print_value(value)
85
+ if value.nil? || value.empty?
86
+ puts ""
87
+ else
88
+ value.scan(/\S.{0,#{@@CONTENT_WIDTH - 2}}\S(?=\s|$)|\S+/).each_with_index do |line, index|
89
+ if index == 0
90
+ puts line
91
+ else
92
+ puts "%#{@@LABEL_WIDTH}s%s" % ["", line]
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ def self.find_story(project, type, limit)
99
+ story = nil
100
+
101
+ criteria = {
102
+ :current_state => @@CANDIDATE_STATES,
103
+ :limit => limit
104
+ }
105
+ if type
106
+ criteria[:story_type] = type
107
+ end
108
+
109
+ candidates = project.stories.all criteria
110
+ if candidates.length == 1
111
+ story = candidates[0]
112
+ else
113
+ story = choose do |menu|
114
+ menu.prompt = "Choose story to start: "
115
+
116
+ candidates.each do |story|
117
+ name = type ? story.name : "%-7s %s" % [story.story_type.upcase, story.name]
118
+ menu.choice(name) { story }
119
+ end
120
+ end
121
+
122
+ puts
123
+ end
124
+
125
+ story
126
+ end
127
+
128
+ end
@@ -0,0 +1,20 @@
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 "git_pivotal_tracker_integration"
17
+
18
+ # A module encapsulating utilities for the project
19
+ module GitPivotalTrackerIntegration::Util
20
+ end
@@ -0,0 +1,64 @@
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 "git-pivotal-tracker-integration/version-update/version_update"
17
+
18
+ # A version updater for dealing with _typical_ Gradle projects. This updater
19
+ # assumes that the version of the current project is stored within a
20
+ # +gradle.properties+ file in the root of the repository. This properties
21
+ # file should have an entry with a key of +version+ and version number as the key.
22
+ class GitPivotalTrackerIntegration::VersionUpdate::Gradle
23
+
24
+ # Creates an instance of this updater
25
+ #
26
+ # @param [String] root The root of the repository
27
+ def initialize(root)
28
+ @gradle_properties = File.expand_path "gradle.properties", root
29
+
30
+ if File.exist? @gradle_properties
31
+ groups = nil
32
+ File.open(@gradle_properties, "r") do |file|
33
+ groups = file.read().scan(/version[=:](.*)/)
34
+ end
35
+ @version = groups[0] ? groups[0][0]: nil
36
+ end
37
+ end
38
+
39
+ # Whether this updater supports updating this project
40
+ #
41
+ # @return [Boolean] +true+ if a valid version number was found on
42
+ # initiialization, +false+ otherwise
43
+ def supports?
44
+ !@version.nil?
45
+ end
46
+
47
+ # The current version of the project
48
+ #
49
+ # @return [String] the current version of the project
50
+ def current_version
51
+ @version
52
+ end
53
+
54
+ # Update the version of the project
55
+ #
56
+ # @param [String] new_version the version to update the project to
57
+ # @return [void]
58
+ def update_version(new_version)
59
+ contents = File.read(@gradle_properties)
60
+ contents = contents.gsub(/(version[=:])#{@version}/, "\\1#{new_version}")
61
+ File.open(@gradle_properties, "w") { |file| file.write(contents) }
62
+ end
63
+
64
+ end