v2gpti 0.2.0.b1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/NOTICE +2 -0
- data/README.md +184 -0
- data/bin/git-finish +19 -0
- data/bin/git-release +19 -0
- data/bin/git-start +19 -0
- data/lib/git-pivotal-tracker-integration/command/base.rb +48 -0
- data/lib/git-pivotal-tracker-integration/command/command.rb +20 -0
- data/lib/git-pivotal-tracker-integration/command/configuration.rb +104 -0
- data/lib/git-pivotal-tracker-integration/command/finish.rb +38 -0
- data/lib/git-pivotal-tracker-integration/command/prepare-commit-msg.sh +26 -0
- data/lib/git-pivotal-tracker-integration/command/release.rb +56 -0
- data/lib/git-pivotal-tracker-integration/command/start.rb +96 -0
- data/lib/git-pivotal-tracker-integration/util/git.rb +244 -0
- data/lib/git-pivotal-tracker-integration/util/shell.rb +36 -0
- data/lib/git-pivotal-tracker-integration/util/story.rb +134 -0
- data/lib/git-pivotal-tracker-integration/util/util.rb +20 -0
- data/lib/git-pivotal-tracker-integration/version-update/gradle.rb +64 -0
- data/lib/git-pivotal-tracker-integration/version-update/version_update.rb +20 -0
- data/lib/git_pivotal_tracker_integration.rb +18 -0
- data/spec/git-pivotal-tracker-integration/command/base_spec.rb +38 -0
- data/spec/git-pivotal-tracker-integration/command/configuration_spec.rb +91 -0
- data/spec/git-pivotal-tracker-integration/command/finish_spec.rb +45 -0
- data/spec/git-pivotal-tracker-integration/command/release_spec.rb +57 -0
- data/spec/git-pivotal-tracker-integration/command/start_spec.rb +55 -0
- data/spec/git-pivotal-tracker-integration/util/git_spec.rb +235 -0
- data/spec/git-pivotal-tracker-integration/util/shell_spec.rb +52 -0
- data/spec/git-pivotal-tracker-integration/util/story_spec.rb +143 -0
- data/spec/git-pivotal-tracker-integration/version-update/gradle_spec.rb +74 -0
- 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
|
+
|