v2gpti 0.2.0.b1

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