spire-git-pivotal-tracker 1.0.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 (30) hide show
  1. data/LICENSE +202 -0
  2. data/NOTICE +2 -0
  3. data/README.md +176 -0
  4. data/bin/git-finish +19 -0
  5. data/bin/git-release +19 -0
  6. data/bin/git-start +19 -0
  7. data/lib/git-pivotal-tracker-integration/command/base.rb +47 -0
  8. data/lib/git-pivotal-tracker-integration/command/command.rb +20 -0
  9. data/lib/git-pivotal-tracker-integration/command/configuration.rb +109 -0
  10. data/lib/git-pivotal-tracker-integration/command/finish.rb +57 -0
  11. data/lib/git-pivotal-tracker-integration/command/prepare-commit-msg.sh +26 -0
  12. data/lib/git-pivotal-tracker-integration/command/release.rb +56 -0
  13. data/lib/git-pivotal-tracker-integration/command/start.rb +67 -0
  14. data/lib/git-pivotal-tracker-integration/util/git.rb +263 -0
  15. data/lib/git-pivotal-tracker-integration/util/shell.rb +36 -0
  16. data/lib/git-pivotal-tracker-integration/util/story.rb +124 -0
  17. data/lib/git-pivotal-tracker-integration/util/util.rb +20 -0
  18. data/lib/git-pivotal-tracker-integration/version-update/gradle.rb +64 -0
  19. data/lib/git-pivotal-tracker-integration/version-update/version_update.rb +20 -0
  20. data/lib/git_pivotal_tracker_integration.rb +18 -0
  21. data/spec/git-pivotal-tracker-integration/command/base_spec.rb +38 -0
  22. data/spec/git-pivotal-tracker-integration/command/configuration_spec.rb +91 -0
  23. data/spec/git-pivotal-tracker-integration/command/finish_spec.rb +45 -0
  24. data/spec/git-pivotal-tracker-integration/command/release_spec.rb +57 -0
  25. data/spec/git-pivotal-tracker-integration/command/start_spec.rb +54 -0
  26. data/spec/git-pivotal-tracker-integration/util/git_spec.rb +235 -0
  27. data/spec/git-pivotal-tracker-integration/util/shell_spec.rb +52 -0
  28. data/spec/git-pivotal-tracker-integration/util/story_spec.rb +137 -0
  29. data/spec/git-pivotal-tracker-integration/version-update/gradle_spec.rb +74 -0
  30. metadata +215 -0
@@ -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 the commands for the project
19
+ module GitPivotalTrackerIntegration::Command
20
+ end
@@ -0,0 +1,109 @@
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/command/command'
17
+ require 'git-pivotal-tracker-integration/util/git'
18
+ require 'highline/import'
19
+ require 'pivotal-tracker'
20
+ require 'github_api'
21
+
22
+ # A class that exposes configuration that commands can use
23
+ class GitPivotalTrackerIntegration::Command::Configuration
24
+
25
+ # Returns the user's Pivotal Tracker API token. If this token has not been
26
+ # configured, prompts the user for the value. The value is checked for in
27
+ # the _inherited_ Git configuration, but is stored in the _global_ Git
28
+ # configuration so that it can be used across multiple repositories.
29
+ #
30
+ # @return [String] The user's Pivotal Tracker API token
31
+ def api_token
32
+ api_token = GitPivotalTrackerIntegration::Util::Git.get_config KEY_API_TOKEN, :inherited
33
+
34
+ if api_token.empty?
35
+ api_token = ask('Pivotal API Token (found at https://www.pivotaltracker.com/profile): ').strip
36
+ GitPivotalTrackerIntegration::Util::Git.set_config KEY_API_TOKEN, api_token, :global
37
+ puts
38
+ end
39
+
40
+ api_token
41
+ end
42
+
43
+ # Returns the Pivotal Tracker project id for this repository. If this id
44
+ # has not been configuration, prompts the user for the value. The value is
45
+ # checked for in the _inherited_ Git configuration, but is stored in the
46
+ # _local_ Git configuration so that it is specific to this repository.
47
+ #
48
+ # @return [String] The repository's Pivotal Tracker project id
49
+ def project_id
50
+ project_id = GitPivotalTrackerIntegration::Util::Git.get_config KEY_PROJECT_ID, :inherited
51
+
52
+ if project_id.empty?
53
+ project_id = choose do |menu|
54
+ menu.prompt = 'Choose project associated with this repository: '
55
+
56
+ PivotalTracker::Project.all.sort_by { |project| project.name }.each do |project|
57
+ menu.choice(project.name) { project.id }
58
+ end
59
+ end
60
+
61
+ GitPivotalTrackerIntegration::Util::Git.set_config KEY_PROJECT_ID, project_id, :local
62
+ puts
63
+ end
64
+
65
+ project_id
66
+ end
67
+
68
+ # Returns the story associated with the current development branch
69
+ #
70
+ # @param [PivotalTracker::Project] project the project the story belongs to
71
+ # @return [PivotalTracker::Story] the story associated with the current development branch
72
+ def story(project)
73
+ story_id = GitPivotalTrackerIntegration::Util::Git.get_config KEY_STORY_ID, :branch
74
+ project.stories.find story_id.to_i
75
+ end
76
+
77
+ # Stores the story associated with the current development branch
78
+ #
79
+ # @param [PivotalTracker::Story] story the story associated with the current development branch
80
+ # @return [void]
81
+ def story=(story)
82
+ GitPivotalTrackerIntegration::Util::Git.set_config KEY_STORY_ID, story.id, :branch
83
+ end
84
+
85
+ def github
86
+ token = GitPivotalTrackerIntegration::Util::Git.get_config GITHUB_API_OAUTH_TOKEN
87
+
88
+ if (token.empty?)
89
+ token = ask("Github OAuth Token (help.github.com/articles/creating-an-access-token-for-command-line-use): ").strip
90
+ GitPivotalTrackerIntegration::Util::Git.set_config(GITHUB_API_OAUTH_TOKEN, token, :local)
91
+ end
92
+
93
+ repo = GitPivotalTrackerIntegration::Util::Git.repo_name
94
+
95
+ ::Github.new(:oauth_token => token, :org => "spire-inc", :repo => repo, :user => "spire-inc")
96
+ end
97
+
98
+ private
99
+
100
+ KEY_API_TOKEN = 'pivotal.api-token'.freeze
101
+
102
+ KEY_PROJECT_ID = 'pivotal.project-id'.freeze
103
+
104
+ KEY_STORY_ID = 'pivotal-story-id'.freeze
105
+
106
+ GITHUB_API_OAUTH_TOKEN = 'workflow.github.oauth'.freeze
107
+ GITHUB_API_REPO_TOKEN = 'workflow.github.repo'.freeze
108
+
109
+ end
@@ -0,0 +1,57 @@
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/command/base'
17
+ require 'git-pivotal-tracker-integration/command/command'
18
+ require 'git-pivotal-tracker-integration/util/git'
19
+
20
+ # The class that encapsulates finishing a Pivotal Tracker Story
21
+ class GitPivotalTrackerIntegration::Command::Finish < GitPivotalTrackerIntegration::Command::Base
22
+
23
+ # Finishes a Pivotal Tracker story by doing the following steps:
24
+ # * Check that the pending merge will be trivial
25
+ # * Merge the development branch into the root branch
26
+ # * Delete the development branch
27
+ # * Push changes to remote
28
+ #
29
+ # @return [void]
30
+ def run(argument)
31
+ GitPivotalTrackerIntegration::Util::Git.verify_uncommitted_changes!
32
+
33
+ github = @configuration.github
34
+
35
+ story = @configuration.story(@project)
36
+
37
+ branch_name = GitPivotalTrackerIntegration::Util::Git.branch_name
38
+
39
+ print 'Creating PR on Github... '
40
+ pr = github.pull_requests.create(
41
+ user: github.user,
42
+ repo: github.repo,
43
+ base: GitPivotalTrackerIntegration::Util::Git.root_branch,
44
+ head: branch_name,
45
+ title: "Fixing #{branch_name}",
46
+ body: "#{story.name}\n#{story.description}\nPivotal Task: #{story.url}"
47
+ )
48
+ puts 'OK'
49
+ print 'Finishing story on Pivotal Tracker... '
50
+ story.update(
51
+ :current_state => 'finished',
52
+ :owned_by => GitPivotalTrackerIntegration::Util::Git.get_config('user.name')
53
+ )
54
+ puts 'OK'
55
+ end
56
+
57
+ end
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ # Git Pivotal Tracker Integration
3
+ # Copyright (c) 2013 the original author or authors.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ CURRENT_BRANCH=$(git branch | grep "*" | sed "s/* //")
18
+ STORY_ID=$(git config branch.$CURRENT_BRANCH.pivotal-story-id)
19
+
20
+ if [[ $2 != "commit" && -n $STORY_ID ]]; then
21
+ ORIG_MSG_FILE="$1"
22
+ TEMP=$(mktemp /tmp/git-XXXXX)
23
+
24
+ (printf "\n\n[#$STORY_ID]" ; cat "$1") > "$TEMP"
25
+ cat "$TEMP" > "$ORIG_MSG_FILE"
26
+ fi
@@ -0,0 +1,56 @@
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/command/base'
17
+ require 'git-pivotal-tracker-integration/command/command'
18
+ require 'git-pivotal-tracker-integration/util/git'
19
+ require 'git-pivotal-tracker-integration/util/story'
20
+ require 'git-pivotal-tracker-integration/version-update/gradle'
21
+
22
+ # The class that encapsulates releasing a Pivotal Tracker Story
23
+ class GitPivotalTrackerIntegration::Command::Release < GitPivotalTrackerIntegration::Command::Base
24
+
25
+ # Releases a Pivotal Tracker story by doing the following steps:
26
+ # * Update the version to the release version
27
+ # * Create a tag for the release version
28
+ # * Update the version to the new development version
29
+ # * Push tag and changes to remote
30
+ #
31
+ # @param [String, nil] filter a filter for selecting the release to start. This
32
+ # filter can be either:
33
+ # * a story id
34
+ # * +nil+
35
+ # @return [void]
36
+ def run(filter)
37
+ story = GitPivotalTrackerIntegration::Util::Story.select_story(@project, filter.nil? ? 'release' : filter, 1)
38
+ GitPivotalTrackerIntegration::Util::Story.pretty_print story
39
+
40
+ updater = [
41
+ GitPivotalTrackerIntegration::VersionUpdate::Gradle.new(@repository_root)
42
+ ].find { |candidate| candidate.supports? }
43
+
44
+ current_version = updater.current_version
45
+ release_version = ask("Enter release version (current: #{current_version}): ")
46
+ next_version = ask("Enter next development version (current: #{current_version}): ")
47
+
48
+ updater.update_version release_version
49
+ GitPivotalTrackerIntegration::Util::Git.create_release_tag release_version, story
50
+ updater.update_version next_version
51
+ GitPivotalTrackerIntegration::Util::Git.create_commit "#{next_version} Development", story
52
+
53
+ GitPivotalTrackerIntegration::Util::Git.push GitPivotalTrackerIntegration::Util::Git.branch_name, "v#{release_version}"
54
+ end
55
+
56
+ end
@@ -0,0 +1,67 @@
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/command/base'
17
+ require 'git-pivotal-tracker-integration/command/command'
18
+ require 'git-pivotal-tracker-integration/util/git'
19
+ require 'git-pivotal-tracker-integration/util/story'
20
+ require 'pivotal-tracker'
21
+
22
+ # The class that encapsulates starting a Pivotal Tracker Story
23
+ class GitPivotalTrackerIntegration::Command::Start < GitPivotalTrackerIntegration::Command::Base
24
+
25
+ # Starts a Pivotal Tracker story by doing the following steps:
26
+ # * Create a branch
27
+ # * Add default commit hook
28
+ # * Start the story on Pivotal Tracker
29
+ #
30
+ # @param [String, nil] filter a filter for selecting the story to start. This
31
+ # filter can be either:
32
+ # * a story id
33
+ # * a story type (feature, bug, chore)
34
+ # * +nil+
35
+ # @return [void]
36
+ def run(filter)
37
+ story = GitPivotalTrackerIntegration::Util::Story.select_story @project, filter
38
+
39
+ GitPivotalTrackerIntegration::Util::Story.pretty_print story
40
+
41
+ development_branch_name = development_branch_name story
42
+ GitPivotalTrackerIntegration::Util::Git.create_branch development_branch_name
43
+ @configuration.story = story
44
+
45
+ GitPivotalTrackerIntegration::Util::Git.add_hook 'prepare-commit-msg', File.join(File.dirname(__FILE__), 'prepare-commit-msg.sh')
46
+
47
+ start_on_tracker story
48
+ end
49
+
50
+ private
51
+
52
+ def development_branch_name(story)
53
+ branch_name = "#{story.id}-" + ask("Enter branch name (#{story.id}-<branch-name>): ")
54
+ puts
55
+ branch_name
56
+ end
57
+
58
+ def start_on_tracker(story)
59
+ print 'Starting story on Pivotal Tracker... '
60
+ story.update(
61
+ :current_state => 'started',
62
+ :owned_by => GitPivotalTrackerIntegration::Util::Git.get_config('user.name')
63
+ )
64
+ puts 'OK'
65
+ end
66
+
67
+ end
@@ -0,0 +1,263 @@
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
+ def self.root_branch
55
+ get_config KEY_ROOT_BRANCH, :branch
56
+ end
57
+
58
+ def self.repo_name
59
+ GitPivotalTrackerIntegration::Util::Shell.exec('git config -l').scan(/spire\-inc\/(.*)\.git/)[0][0]
60
+ end
61
+
62
+ # Creates a branch with a given +name+. First pulls the current branch to
63
+ # ensure that it is up to date and then creates and checks out the new
64
+ # branch. If specified, sets branch-specific properties that are passed in.
65
+ #
66
+ # @param [String] name the name of the branch to create
67
+ # @param [Boolean] print_messages whether to print messages
68
+ # @return [void]
69
+ def self.create_branch(name, print_messages = true)
70
+ root_branch = branch_name
71
+ root_remote = get_config KEY_REMOTE, :branch
72
+
73
+ if print_messages; print "Pulling #{root_branch}... " end
74
+ GitPivotalTrackerIntegration::Util::Shell.exec 'git pull --quiet --ff-only'
75
+ if print_messages; puts 'OK'
76
+ end
77
+
78
+ if print_messages; print "Creating and checking out #{name}... " end
79
+ GitPivotalTrackerIntegration::Util::Shell.exec "git checkout --quiet -b #{name}"
80
+ set_config KEY_ROOT_BRANCH, root_branch, :branch
81
+ set_config KEY_ROOT_REMOTE, root_remote, :branch
82
+ if print_messages; puts 'OK'
83
+ end
84
+ end
85
+
86
+ # Creates a commit with a given message. The commit includes all change
87
+ # files.
88
+ #
89
+ # @param [String] message The commit message, which will be appended with
90
+ # +[#<story-id]+
91
+ # @param [PivotalTracker::Story] story the story associated with the current
92
+ # commit
93
+ # @return [void]
94
+ def self.create_commit(message, story)
95
+ GitPivotalTrackerIntegration::Util::Shell.exec "git commit --quiet --all --allow-empty --message \"#{message}\n\n[##{story.id}]\""
96
+ end
97
+
98
+ # Creates a tag with the given name. Before creating the tag, commits all
99
+ # outstanding changes with a commit message that reflects that these changes
100
+ # are for a release.
101
+ #
102
+ # @param [String] name the name of the tag to create
103
+ # @param [PivotalTracker::Story] story the story associated with the current
104
+ # tag
105
+ # @return [void]
106
+ def self.create_release_tag(name, story)
107
+ root_branch = branch_name
108
+
109
+ print "Creating tag v#{name}... "
110
+
111
+ create_branch RELEASE_BRANCH_NAME, false
112
+ create_commit "#{name} Release", story
113
+ GitPivotalTrackerIntegration::Util::Shell.exec "git tag v#{name}"
114
+ GitPivotalTrackerIntegration::Util::Shell.exec "git checkout --quiet #{root_branch}"
115
+ GitPivotalTrackerIntegration::Util::Shell.exec "git branch --quiet -D #{RELEASE_BRANCH_NAME}"
116
+
117
+ puts 'OK'
118
+ end
119
+
120
+ def self.verify_uncommitted_changes!
121
+ result = `git diff --exit-code`
122
+ if $?.exitstatus != 0
123
+ abort "You have uncommitted changes!"
124
+ end
125
+ result = `git diff --staged --exit-code`
126
+
127
+ if $?.exitstatus != 0
128
+ abort "You have uncommitted staged changes!"
129
+ end
130
+ end
131
+
132
+ # Returns a Git configuration value. This value is read using the +git
133
+ # config+ command. The scope of the value to read can be controlled with the
134
+ # +scope+ parameter.
135
+ #
136
+ # @param [String] key the key of the configuration to retrieve
137
+ # @param [:branch, :inherited] scope the scope to read the configuration from
138
+ # * +:branch+: equivalent to calling +git config branch.branch-name.key+
139
+ # * +:inherited+: equivalent to calling +git config key+
140
+ # @return [String] the value of the configuration
141
+ # @raise if the specified scope is not +:branch+ or +:inherited+
142
+ def self.get_config(key, scope = :inherited)
143
+ if :branch == scope
144
+ GitPivotalTrackerIntegration::Util::Shell.exec("git config branch.#{branch_name}.#{key}", false).strip
145
+ elsif :inherited == scope
146
+ GitPivotalTrackerIntegration::Util::Shell.exec("git config #{key}", false).strip
147
+ else
148
+ raise "Unable to get Git configuration for scope '#{scope}'"
149
+ end
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
+ # @return [void]
157
+ def self.merge(story, no_complete)
158
+ development_branch = branch_name
159
+ root_branch = get_config KEY_ROOT_BRANCH, :branch
160
+
161
+ print "Merging #{development_branch} to #{root_branch}... "
162
+ GitPivotalTrackerIntegration::Util::Shell.exec "git checkout --quiet #{root_branch}"
163
+ GitPivotalTrackerIntegration::Util::Shell.exec "git merge --quiet --no-ff -m \"Merge #{development_branch} to #{root_branch}\n\n[#{no_complete ? '' : 'Completes '}##{story.id}]\" #{development_branch}"
164
+ puts 'OK'
165
+
166
+ print "Deleting #{development_branch}... "
167
+ GitPivotalTrackerIntegration::Util::Shell.exec "git branch --quiet -D #{development_branch}"
168
+ puts 'OK'
169
+ end
170
+
171
+ # Push changes to the remote of the current branch
172
+ #
173
+ # @param [String] refs the explicit references to push
174
+ # @return [void]
175
+ def self.push(*refs)
176
+ remote = get_config KEY_REMOTE, :branch
177
+
178
+ print "Pushing to #{remote}... "
179
+ GitPivotalTrackerIntegration::Util::Shell.exec "git push --quiet #{remote} " + refs.join(' ')
180
+ puts 'OK'
181
+ end
182
+
183
+ # Returns the root path of the current Git repository. The root is
184
+ # determined by ascending the path hierarchy, starting with the current
185
+ # working directory (+Dir#pwd+), until a directory is found that contains a
186
+ # +.git/+ sub directory.
187
+ #
188
+ # @return [String] the root path of the Git repository
189
+ # @raise if the current working directory is not in a Git repository
190
+ def self.repository_root
191
+ repository_root = Dir.pwd
192
+
193
+ until Dir.entries(repository_root).any? { |child| File.directory?(child) && (child =~ /^.git$/) }
194
+ next_repository_root = File.expand_path('..', repository_root)
195
+ abort('Current working directory is not in a Git repository') unless repository_root != next_repository_root
196
+ repository_root = next_repository_root
197
+ end
198
+
199
+ repository_root
200
+ end
201
+
202
+ # Sets a Git configuration value. This value is set using the +git config+
203
+ # command. The scope of the set value can be controlled with the +scope+
204
+ # parameter.
205
+ #
206
+ # @param [String] key the key of configuration to store
207
+ # @param [String] value the value of the configuration to store
208
+ # @param [:branch, :global, :local] scope the scope to store the configuration value in.
209
+ # * +:branch+: equivalent to calling +git config --local branch.branch-name.key value+
210
+ # * +:global+: equivalent to calling +git config --global key value+
211
+ # * +:local+: equivalent to calling +git config --local key value+
212
+ # @return [void]
213
+ # @raise if the specified scope is not +:branch+, +:global+, or +:local+
214
+ def self.set_config(key, value, scope = :local)
215
+ if :branch == scope
216
+ GitPivotalTrackerIntegration::Util::Shell.exec "git config --local branch.#{branch_name}.#{key} #{value}"
217
+ elsif :global == scope
218
+ GitPivotalTrackerIntegration::Util::Shell.exec "git config --global #{key} #{value}"
219
+ elsif :local == scope
220
+ GitPivotalTrackerIntegration::Util::Shell.exec "git config --local #{key} #{value}"
221
+ else
222
+ raise "Unable to set Git configuration for scope '#{scope}'"
223
+ end
224
+ end
225
+
226
+ # Checks whether merging the current branch back to its root branch would be
227
+ # a trivial merge. A trivial merge is defined as one where the net change
228
+ # of the merge would be the same as the net change of the branch being
229
+ # merged. The easiest way to ensure that a merge is trivial is to rebase a
230
+ # development branch onto the tip of its root branch.
231
+ #
232
+ # @return [void]
233
+ def self.trivial_merge?
234
+ development_branch = branch_name
235
+
236
+ print "Checking for trivial merge from #{development_branch} to #{root_branch}... "
237
+
238
+ GitPivotalTrackerIntegration::Util::Shell.exec "git checkout --quiet #{root_branch}"
239
+ GitPivotalTrackerIntegration::Util::Shell.exec 'git pull --quiet --ff-only'
240
+ GitPivotalTrackerIntegration::Util::Shell.exec "git checkout --quiet #{development_branch}"
241
+
242
+ root_tip = GitPivotalTrackerIntegration::Util::Shell.exec "git rev-parse #{root_branch}"
243
+ common_ancestor = GitPivotalTrackerIntegration::Util::Shell.exec "git merge-base #{root_branch} #{development_branch}"
244
+
245
+ if root_tip != common_ancestor
246
+ abort 'FAIL'
247
+ end
248
+
249
+ puts 'OK'
250
+ end
251
+
252
+ private
253
+
254
+ KEY_REMOTE = 'remote'.freeze
255
+
256
+ KEY_ROOT_BRANCH = 'root-branch'.freeze
257
+
258
+ KEY_ROOT_REMOTE = 'root-remote'.freeze
259
+
260
+ RELEASE_BRANCH_NAME = 'pivotal-tracker-release'.freeze
261
+
262
+ end
263
+