v2gpti 0.2.0.b1

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 (31) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +202 -0
  3. data/NOTICE +2 -0
  4. data/README.md +184 -0
  5. data/bin/git-finish +19 -0
  6. data/bin/git-release +19 -0
  7. data/bin/git-start +19 -0
  8. data/lib/git-pivotal-tracker-integration/command/base.rb +48 -0
  9. data/lib/git-pivotal-tracker-integration/command/command.rb +20 -0
  10. data/lib/git-pivotal-tracker-integration/command/configuration.rb +104 -0
  11. data/lib/git-pivotal-tracker-integration/command/finish.rb +38 -0
  12. data/lib/git-pivotal-tracker-integration/command/prepare-commit-msg.sh +26 -0
  13. data/lib/git-pivotal-tracker-integration/command/release.rb +56 -0
  14. data/lib/git-pivotal-tracker-integration/command/start.rb +96 -0
  15. data/lib/git-pivotal-tracker-integration/util/git.rb +244 -0
  16. data/lib/git-pivotal-tracker-integration/util/shell.rb +36 -0
  17. data/lib/git-pivotal-tracker-integration/util/story.rb +134 -0
  18. data/lib/git-pivotal-tracker-integration/util/util.rb +20 -0
  19. data/lib/git-pivotal-tracker-integration/version-update/gradle.rb +64 -0
  20. data/lib/git-pivotal-tracker-integration/version-update/version_update.rb +20 -0
  21. data/lib/git_pivotal_tracker_integration.rb +18 -0
  22. data/spec/git-pivotal-tracker-integration/command/base_spec.rb +38 -0
  23. data/spec/git-pivotal-tracker-integration/command/configuration_spec.rb +91 -0
  24. data/spec/git-pivotal-tracker-integration/command/finish_spec.rb +45 -0
  25. data/spec/git-pivotal-tracker-integration/command/release_spec.rb +57 -0
  26. data/spec/git-pivotal-tracker-integration/command/start_spec.rb +55 -0
  27. data/spec/git-pivotal-tracker-integration/util/git_spec.rb +235 -0
  28. data/spec/git-pivotal-tracker-integration/util/shell_spec.rb +52 -0
  29. data/spec/git-pivotal-tracker-integration/util/story_spec.rb +143 -0
  30. data/spec/git-pivotal-tracker-integration/version-update/gradle_spec.rb +74 -0
  31. metadata +213 -0
@@ -0,0 +1,48 @@
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/command/configuration'
18
+ require 'git-pivotal-tracker-integration/util/git'
19
+ require 'pivotal-tracker'
20
+ require 'parseconfig'
21
+
22
+ # An abstract base class for all commands
23
+ # @abstract Subclass and override {#run} to implement command functionality
24
+ class GitPivotalTrackerIntegration::Command::Base
25
+
26
+ # Common initialization functionality for all command classes. This
27
+ # enforces that:
28
+ # * the command is being run within a valid Git repository
29
+ # * the user has specified their Pivotal Tracker API token
30
+ # * all communication with Pivotal Tracker will be protected with SSL
31
+ # * the user has configured the project id for this repository
32
+ def initialize
33
+ @repository_root = GitPivotalTrackerIntegration::Util::Git.repository_root
34
+ @configuration = GitPivotalTrackerIntegration::Command::Configuration.new
35
+
36
+ PivotalTracker::Client.token = @configuration.api_token
37
+ PivotalTracker::Client.use_ssl = true
38
+
39
+ @project = PivotalTracker::Project.find @configuration.project_id
40
+ end
41
+
42
+ # The main entry point to the command's execution
43
+ # @abstract Override this method to implement command functionality
44
+ def run
45
+ raise NotImplementedError
46
+ end
47
+
48
+ 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 the commands for the project
19
+ module GitPivotalTrackerIntegration::Command
20
+ end
@@ -0,0 +1,104 @@
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
+
21
+ # A class that exposes configuration that commands can use
22
+ class GitPivotalTrackerIntegration::Command::Configuration
23
+
24
+ # Returns the user's Pivotal Tracker API token. If this token has not been
25
+ # configured, prompts the user for the value. The value is checked for in
26
+ # the _inherited_ Git configuration, but is stored in the _global_ Git
27
+ # configuration so that it can be used across multiple repositories.
28
+ #
29
+ # @return [String] The user's Pivotal Tracker API token
30
+ def api_token
31
+ api_token = GitPivotalTrackerIntegration::Util::Git.get_config KEY_API_TOKEN, :inherited
32
+
33
+ if api_token.empty?
34
+ self.check_config
35
+ if api_token.empty?
36
+ api_token = ask('Pivotal API Token (found at https://www.pivotaltracker.com/profile): ').strip
37
+ GitPivotalTrackerIntegration::Util::Git.set_config KEY_API_TOKEN, api_token, :global
38
+ puts
39
+ end
40
+ end
41
+
42
+ api_token
43
+ end
44
+
45
+ def check_config
46
+ repo_root = GitPivotalTrackerIntegration::Util::Git.repository_root
47
+ config_filename = "#{repo_root}/.v2gpti/config"
48
+ if File.file?(config_filename)
49
+ pconfig = ParseConfig.new(config_filename)
50
+ GitPivotalTrackerIntegration::Util::Git.set_config("pivotal.project-id", pconfig["pivotal-tracker"]["project-id"])
51
+ end
52
+ end
53
+
54
+ # Returns the Pivotal Tracker project id for this repository. If this id
55
+ # has not been configuration, prompts the user for the value. The value is
56
+ # checked for in the _inherited_ Git configuration, but is stored in the
57
+ # _local_ Git configuration so that it is specific to this repository.
58
+ #
59
+ # @return [String] The repository's Pivotal Tracker project id
60
+ def project_id
61
+ project_id = GitPivotalTrackerIntegration::Util::Git.get_config KEY_PROJECT_ID, :inherited
62
+
63
+ if project_id.empty?
64
+ project_id = choose do |menu|
65
+ menu.prompt = 'Choose project associated with this repository: '
66
+
67
+ PivotalTracker::Project.all.sort_by { |project| project.name }.each do |project|
68
+ menu.choice(project.name) { project.id }
69
+ end
70
+ end
71
+
72
+ GitPivotalTrackerIntegration::Util::Git.set_config KEY_PROJECT_ID, project_id, :local
73
+ puts
74
+ end
75
+
76
+ project_id
77
+ end
78
+
79
+ # Returns the story associated with the current development branch
80
+ #
81
+ # @param [PivotalTracker::Project] project the project the story belongs to
82
+ # @return [PivotalTracker::Story] the story associated with the current development branch
83
+ def story(project)
84
+ story_id = GitPivotalTrackerIntegration::Util::Git.get_config KEY_STORY_ID, :branch
85
+ project.stories.find story_id.to_i
86
+ end
87
+
88
+ # Stores the story associated with the current development branch
89
+ #
90
+ # @param [PivotalTracker::Story] story the story associated with the current development branch
91
+ # @return [void]
92
+ def story=(story)
93
+ GitPivotalTrackerIntegration::Util::Git.set_config KEY_STORY_ID, story.id, :branch
94
+ end
95
+
96
+ private
97
+
98
+ KEY_API_TOKEN = 'pivotal.api-token'.freeze
99
+
100
+ KEY_PROJECT_ID = 'pivotal.project-id'.freeze
101
+
102
+ KEY_STORY_ID = 'pivotal-story-id'.freeze
103
+
104
+ end
@@ -0,0 +1,38 @@
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
+ no_complete = argument =~ /--no-complete/
32
+
33
+ GitPivotalTrackerIntegration::Util::Git.trivial_merge?
34
+ GitPivotalTrackerIntegration::Util::Git.merge(@configuration.story(@project), no_complete)
35
+ GitPivotalTrackerIntegration::Util::Git.push GitPivotalTrackerIntegration::Util::Git.branch_name
36
+ end
37
+
38
+ 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,96 @@
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
+ self.check_branch
38
+ story = GitPivotalTrackerIntegration::Util::Story.select_story @project, filter
39
+
40
+ GitPivotalTrackerIntegration::Util::Story.pretty_print story
41
+
42
+ development_branch_name = development_branch_name story
43
+ GitPivotalTrackerIntegration::Util::Git.create_branch development_branch_name
44
+ @configuration.story = story
45
+
46
+ GitPivotalTrackerIntegration::Util::Git.add_hook 'prepare-commit-msg', File.join(File.dirname(__FILE__), 'prepare-commit-msg.sh')
47
+
48
+ start_on_tracker story
49
+ end
50
+
51
+ def check_branch
52
+
53
+ current_branch = GitPivotalTrackerIntegration::Util::Git.branch_name
54
+
55
+ suggested_branch = (GitPivotalTrackerIntegration::Util::Shell.exec "git config --get git-pivotal-tracker-integration.feature-root 2>/dev/null", false).chomp
56
+
57
+ if !suggested_branch.nil? && suggested_branch.length !=0 && current_branch != suggested_branch
58
+ should_chage_branch = ask("Your currently checked out branch is '#{current_branch}'. Do you want to checkout '#{suggested_branch}' before starting?(Y/n)")
59
+ if should_chage_branch != "n"
60
+ print "Checking out branch '#{suggested_branch}'...\n\n"
61
+ GitPivotalTrackerIntegration::Util::Shell.exec "git checkout --quiet #{suggested_branch}"
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+
68
+ private
69
+
70
+ def development_branch_name(story)
71
+ prefix = "#{story.id}-"
72
+ story_name = "#{story.name.gsub(/[^0-9a-z\\s]/i, '_')}"
73
+ if(story_name.length > 30)
74
+ suggested_suffix = story_name[0..27]
75
+ suggested_suffix << "__"
76
+ else
77
+ suggested_suffix = story_name
78
+ end
79
+ branch_name = "#{prefix}" + ask("Enter branch name (#{story.id}-<#{suggested_suffix}>): ")
80
+ puts
81
+ if branch_name == "#{prefix}"
82
+ branch_name << suggested_suffix
83
+ end
84
+ branch_name.gsub(/[^0-9a-z\\s\-]/i, '_')
85
+ end
86
+
87
+ def start_on_tracker(story)
88
+ print 'Starting story on Pivotal Tracker... '
89
+ story.update(
90
+ :current_state => 'started',
91
+ :owned_by => GitPivotalTrackerIntegration::Util::Git.get_config('user.name')
92
+ )
93
+ puts 'OK'
94
+ end
95
+
96
+ end
@@ -0,0 +1,244 @@
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'
68
+ end
69
+
70
+ if print_messages; print "Creating and checking out #{name}... " end
71
+ GitPivotalTrackerIntegration::Util::Shell.exec "git checkout --quiet -b #{name}"
72
+ set_config KEY_ROOT_BRANCH, root_branch, :branch
73
+ set_config KEY_ROOT_REMOTE, root_remote, :branch
74
+ if print_messages; puts 'OK'
75
+ end
76
+ end
77
+
78
+ # Creates a commit with a given message. The commit includes all change
79
+ # files.
80
+ #
81
+ # @param [String] message The commit message, which will be appended with
82
+ # +[#<story-id]+
83
+ # @param [PivotalTracker::Story] story the story associated with the current
84
+ # commit
85
+ # @return [void]
86
+ def self.create_commit(message, story)
87
+ GitPivotalTrackerIntegration::Util::Shell.exec "git commit --quiet --all --allow-empty --message \"#{message}\n\n[##{story.id}]\""
88
+ end
89
+
90
+ # Creates a tag with the given name. Before creating the tag, commits all
91
+ # outstanding changes with a commit message that reflects that these changes
92
+ # are for a release.
93
+ #
94
+ # @param [String] name the name of the tag to create
95
+ # @param [PivotalTracker::Story] story the story associated with the current
96
+ # tag
97
+ # @return [void]
98
+ def self.create_release_tag(name, story)
99
+ root_branch = branch_name
100
+
101
+ print "Creating tag v#{name}... "
102
+
103
+ create_branch RELEASE_BRANCH_NAME, false
104
+ create_commit "#{name} Release", story
105
+ GitPivotalTrackerIntegration::Util::Shell.exec "git tag v#{name}"
106
+ GitPivotalTrackerIntegration::Util::Shell.exec "git checkout --quiet #{root_branch}"
107
+ GitPivotalTrackerIntegration::Util::Shell.exec "git branch --quiet -D #{RELEASE_BRANCH_NAME}"
108
+
109
+ puts 'OK'
110
+ end
111
+
112
+ # Returns a Git configuration value. This value is read using the +git
113
+ # config+ command. The scope of the value to read can be controlled with the
114
+ # +scope+ parameter.
115
+ #
116
+ # @param [String] key the key of the configuration to retrieve
117
+ # @param [:branch, :inherited] scope the scope to read the configuration from
118
+ # * +:branch+: equivalent to calling +git config branch.branch-name.key+
119
+ # * +:inherited+: equivalent to calling +git config key+
120
+ # @return [String] the value of the configuration
121
+ # @raise if the specified scope is not +:branch+ or +:inherited+
122
+ def self.get_config(key, scope = :inherited)
123
+ if :branch == scope
124
+ GitPivotalTrackerIntegration::Util::Shell.exec("git config branch.#{branch_name}.#{key}", false).strip
125
+ elsif :inherited == scope
126
+ GitPivotalTrackerIntegration::Util::Shell.exec("git config #{key}", false).strip
127
+ else
128
+ raise "Unable to get Git configuration for scope '#{scope}'"
129
+ end
130
+ end
131
+
132
+ # Merges the current branch to its root branch and deletes the current branch
133
+ #
134
+ # @param [PivotalTracker::Story] story the story associated with the current branch
135
+ # @param [Boolean] no_complete whether to suppress the +Completes+ statement in the commit message
136
+ # @return [void]
137
+ def self.merge(story, no_complete)
138
+ development_branch = branch_name
139
+ root_branch = get_config KEY_ROOT_BRANCH, :branch
140
+
141
+ print "Merging #{development_branch} to #{root_branch}... "
142
+ GitPivotalTrackerIntegration::Util::Shell.exec "git checkout --quiet #{root_branch}"
143
+ 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}"
144
+ puts 'OK'
145
+
146
+ print "Deleting #{development_branch}... "
147
+ GitPivotalTrackerIntegration::Util::Shell.exec "git branch --quiet -D #{development_branch}"
148
+ puts 'OK'
149
+ end
150
+
151
+ # Push changes to the remote of the current branch
152
+ #
153
+ # @param [String] refs the explicit references to push
154
+ # @return [void]
155
+ def self.push(*refs)
156
+ remote = get_config KEY_REMOTE, :branch
157
+
158
+ print "Pushing to #{remote}... "
159
+ GitPivotalTrackerIntegration::Util::Shell.exec "git push --quiet #{remote} " + refs.join(' ')
160
+ puts 'OK'
161
+ end
162
+
163
+ # Returns the root path of the current Git repository. The root is
164
+ # determined by ascending the path hierarchy, starting with the current
165
+ # working directory (+Dir#pwd+), until a directory is found that contains a
166
+ # +.git/+ sub directory.
167
+ #
168
+ # @return [String] the root path of the Git repository
169
+ # @raise if the current working directory is not in a Git repository
170
+ def self.repository_root
171
+ repository_root = Dir.pwd
172
+
173
+ until Dir.entries(repository_root).any? { |child| File.directory?(child) && (child =~ /^.git$/) }
174
+ next_repository_root = File.expand_path('..', repository_root)
175
+ abort('Current working directory is not in a Git repository') unless repository_root != next_repository_root
176
+ repository_root = next_repository_root
177
+ end
178
+
179
+ repository_root
180
+ end
181
+
182
+ # Sets a Git configuration value. This value is set using the +git config+
183
+ # command. The scope of the set value can be controlled with the +scope+
184
+ # parameter.
185
+ #
186
+ # @param [String] key the key of configuration to store
187
+ # @param [String] value the value of the configuration to store
188
+ # @param [:branch, :global, :local] scope the scope to store the configuration value in.
189
+ # * +:branch+: equivalent to calling +git config --local branch.branch-name.key value+
190
+ # * +:global+: equivalent to calling +git config --global key value+
191
+ # * +:local+: equivalent to calling +git config --local key value+
192
+ # @return [void]
193
+ # @raise if the specified scope is not +:branch+, +:global+, or +:local+
194
+ def self.set_config(key, value, scope = :local)
195
+ if :branch == scope
196
+ GitPivotalTrackerIntegration::Util::Shell.exec "git config --local branch.#{branch_name}.#{key} #{value}"
197
+ elsif :global == scope
198
+ GitPivotalTrackerIntegration::Util::Shell.exec "git config --global #{key} #{value}"
199
+ elsif :local == scope
200
+ GitPivotalTrackerIntegration::Util::Shell.exec "git config --local #{key} #{value}"
201
+ else
202
+ raise "Unable to set Git configuration for scope '#{scope}'"
203
+ end
204
+ end
205
+
206
+ # Checks whether merging the current branch back to its root branch would be
207
+ # a trivial merge. A trivial merge is defined as one where the net change
208
+ # of the merge would be the same as the net change of the branch being
209
+ # merged. The easiest way to ensure that a merge is trivial is to rebase a
210
+ # development branch onto the tip of its root branch.
211
+ #
212
+ # @return [void]
213
+ def self.trivial_merge?
214
+ development_branch = branch_name
215
+ root_branch = get_config KEY_ROOT_BRANCH, :branch
216
+
217
+ print "Checking for trivial merge from #{development_branch} to #{root_branch}... "
218
+
219
+ GitPivotalTrackerIntegration::Util::Shell.exec "git checkout --quiet #{root_branch}"
220
+ GitPivotalTrackerIntegration::Util::Shell.exec 'git pull --quiet --ff-only'
221
+ GitPivotalTrackerIntegration::Util::Shell.exec "git checkout --quiet #{development_branch}"
222
+
223
+ root_tip = GitPivotalTrackerIntegration::Util::Shell.exec "git rev-parse #{root_branch}"
224
+ common_ancestor = GitPivotalTrackerIntegration::Util::Shell.exec "git merge-base #{root_branch} #{development_branch}"
225
+
226
+ if root_tip != common_ancestor
227
+ abort 'FAIL'
228
+ end
229
+
230
+ puts 'OK'
231
+ end
232
+
233
+ private
234
+
235
+ KEY_REMOTE = 'remote'.freeze
236
+
237
+ KEY_ROOT_BRANCH = 'root-branch'.freeze
238
+
239
+ KEY_ROOT_REMOTE = 'root-remote'.freeze
240
+
241
+ RELEASE_BRANCH_NAME = 'pivotal-tracker-release'.freeze
242
+
243
+ end
244
+