github-pivotal-flow 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +14 -0
- data/README.md +183 -0
- data/bin/git-finish +6 -0
- data/bin/git-start +6 -0
- data/lib/core_ext/object/blank.rb +105 -0
- data/lib/github_pivotal_flow.rb +31 -0
- data/lib/github_pivotal_flow/command.rb +57 -0
- data/lib/github_pivotal_flow/configuration.rb +251 -0
- data/lib/github_pivotal_flow/finish.rb +33 -0
- data/lib/github_pivotal_flow/git.rb +150 -0
- data/lib/github_pivotal_flow/github_api.rb +241 -0
- data/lib/github_pivotal_flow/prepare-commit-msg.sh +11 -0
- data/lib/github_pivotal_flow/project.rb +23 -0
- data/lib/github_pivotal_flow/shell.rb +21 -0
- data/lib/github_pivotal_flow/start.rb +40 -0
- data/lib/github_pivotal_flow/story.rb +278 -0
- data/spec/github_pivotal_flow/configuration_spec.rb +70 -0
- data/spec/github_pivotal_flow/finish_spec.rb +37 -0
- data/spec/github_pivotal_flow/git_spec.rb +167 -0
- data/spec/github_pivotal_flow/shell_spec.rb +36 -0
- data/spec/github_pivotal_flow/start_spec.rb +41 -0
- data/spec/github_pivotal_flow/story_spec.rb +125 -0
- metadata +186 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a4e63d5c7d9eda8c218b536bf973127a9ed3fd48
|
4
|
+
data.tar.gz: fcc22c50ee4958a67ffdad288bd13bdb86adaa90
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 85fb6cea0fbc88c4abd12b1622557378db19758037bb495b3132471fd01403a66fbfd7a724918a6316655d9a19bd455adbf2037ab34970be48a9828b45011df1
|
7
|
+
data.tar.gz: bec91a2d14f3cc3e597a0de10e03917ff8eef9efb8d65a3378d1c249a4a084a4c387775952eafa6bf0d0b3b3950e132f5d731ff051a36d7298ed11a8ab15b65f
|
data/LICENSE
ADDED
@@ -0,0 +1,14 @@
|
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
# Github Pivotal Flow
|
2
|
+
|
3
|
+
`github-pivotal-flow` provides a set of additional Git commands to help developers when working with [Pivotal Tracker][pivotal-tracker], git-flow and Github pull requests.
|
4
|
+
It follows the branch structure recommended by [Git flow][git-flow].
|
5
|
+
|
6
|
+
This is the tool we use internally to speed up our development process.
|
7
|
+
|
8
|
+
[pivotal-tracker]: http://www.pivotaltracker.com
|
9
|
+
[git-flow]: https://github.com/nvie/gitflow
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
`github-pivotal-flow` requires at least **Ruby 1.8.7**, **Git 1.8.2.1** in order to run. It is tested against Rubies _1.8.7_, _1.9.3_, and _2.0.0_. In order to install it, do the following:
|
13
|
+
|
14
|
+
```plain
|
15
|
+
$ gem install github-pivotal-flow
|
16
|
+
```
|
17
|
+
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
`github-pivotal-flow` is intended to vastly speed up your development workflow.
|
21
|
+
The typical workflow looks something like the following:
|
22
|
+
|
23
|
+
```plain
|
24
|
+
$ git start # Creates branch, opens a pull request on Github and starts story
|
25
|
+
$ git commit ...
|
26
|
+
$ git commit ... # Your existing development process
|
27
|
+
$ git commit ...
|
28
|
+
$ git finish # Merges back into the main branch. Pushes to origin, destroys branch and finishes story.
|
29
|
+
```
|
30
|
+
|
31
|
+
|
32
|
+
## Configuration
|
33
|
+
|
34
|
+
### Git Client
|
35
|
+
In order to use `github-pivotal-flow`, a few Git client configuration properties must be set. If these properties have not been set, you will be prompted for them and your Git configuration will be updated.
|
36
|
+
|
37
|
+
| Name | Description
|
38
|
+
| ---- | -----------
|
39
|
+
| `pivotal.api-token` | Your Pivotal Tracker API Token. This can be found in [your profile][profile] and should be set globally.
|
40
|
+
| `pivotal.project-id` | The Pivotal Tracker project id for the repository your are working in. This can be found in the project's URL and should be set.
|
41
|
+
| `gitflow.branch.master` | The Git-flow master branch name. If you've used Git-flow before this will already be set up. Otherwise this is the branch considered 'production'.
|
42
|
+
| `gitflow.branch.development` | The Git-flow development branch name. The branch that is commonly used for your development.
|
43
|
+
| `gitflow.prefix.feature` | Git-flow feature branch name prefix.
|
44
|
+
| `gitflow.prefix.hotfix` | Git-flow hotfix branch name prefix.
|
45
|
+
| `gitflow.prefix.feature` | Git-flow feature branch name prefix.
|
46
|
+
| `gitflow.prefix.release` | Git-flow release branch name prefix.
|
47
|
+
|
48
|
+
[profile]: https://www.pivotaltracker.com/profile
|
49
|
+
|
50
|
+
|
51
|
+
### Git Server
|
52
|
+
In order to take advantage of automatic issue completion, the [Pivotal Tracker Source Code Integration][integration] must be enabled. If you are using GitHub, this integration is easy to enable by navgating to your project's 'Service Hooks' settings and configuring it with the proper credentials.
|
53
|
+
|
54
|
+
[integration]: https://www.pivotaltracker.com/help/integrations?version=v3#scm
|
55
|
+
|
56
|
+
|
57
|
+
## Commands
|
58
|
+
|
59
|
+
### `git start [ type | story-id ]`
|
60
|
+
This command starts a story by creating a Git branch and changing the story's state to `started`. This command can be run in three ways. First it can be run specifying the id of the story that you want to start.
|
61
|
+
|
62
|
+
```plain
|
63
|
+
$ git start 12345678
|
64
|
+
```
|
65
|
+
|
66
|
+
The second way to run the command is by specyifying the type of story that you would like to start. In this case it will then offer you the first five stories (based on the backlog's order) of that type to choose from.
|
67
|
+
|
68
|
+
```plain
|
69
|
+
$ git start feature
|
70
|
+
|
71
|
+
1. Lorem ipsum dolor sit amet, consectetur adipiscing elit
|
72
|
+
2. Pellentesque sit amet ante eu tortor rutrum pharetra
|
73
|
+
3. Ut at purus dolor, vel ultricies metus
|
74
|
+
4. Duis egestas elit et leo ultrices non fringilla ante facilisis
|
75
|
+
5. Ut ut nunc neque, quis auctor mauris
|
76
|
+
Choose story to start:
|
77
|
+
```
|
78
|
+
|
79
|
+
Finally the command can be run without specifying anything. In this case, it will then offer the first five stories (based on the backlog's order) of any type to choose from.
|
80
|
+
|
81
|
+
```plain
|
82
|
+
$ git start
|
83
|
+
|
84
|
+
1. FEATURE Donec convallis leo mi, dictum ornare sem
|
85
|
+
2. CHORE Sed et magna lectus, sed auctor purus
|
86
|
+
3. FEATURE In a nunc et enim tincidunt interdum vitae et risus
|
87
|
+
4. FEATURE Fusce facilisis varius lorem, at tristique sem faucibus in
|
88
|
+
5. BUG Donec iaculis ante neque, ut tempus augue
|
89
|
+
Choose story to start:
|
90
|
+
```
|
91
|
+
|
92
|
+
Once a story has been selected by one of the three methods, the command then prompts for the name of the branch to create.
|
93
|
+
|
94
|
+
```plain
|
95
|
+
$ git start 12345678
|
96
|
+
Title: Lorem ipsum dolor sit amet, consectetur adipiscing elitattributes
|
97
|
+
Description: Ut consequat sapien ut erat volutpat egestas. Integer venenatis lacinia facilisis.
|
98
|
+
|
99
|
+
Enter branch name (12345678-<branch-name>):
|
100
|
+
```
|
101
|
+
|
102
|
+
The value entered here will be prepended with the story id such that the branch name is `<story-id>-<branch-name>`. This branch is then created and checked out.
|
103
|
+
|
104
|
+
If it doesn't exist already, a `prepare-commit-msg` commit hook is added to your repository. This commit hook augments the existing commit messsage pattern by appending the story id to the message automatically.
|
105
|
+
|
106
|
+
```plain
|
107
|
+
|
108
|
+
[#12345678]
|
109
|
+
# Please enter the commit message for your changes. Lines starting
|
110
|
+
# with '#' will be ignored, and an empty message aborts the commit.
|
111
|
+
# On branch 12345678-lorem-ipsum
|
112
|
+
# Changes to be committed:
|
113
|
+
# (use "git reset HEAD <file>..." to unstage)
|
114
|
+
#
|
115
|
+
# new file: dolor.txt
|
116
|
+
#
|
117
|
+
```
|
118
|
+
|
119
|
+
### `git finish [--no-complete]`
|
120
|
+
This command finishes a story by merging and cleaning up its branch and then pushing the changes to a remote server. This command can be run in two ways. First it can be run without the `--no-complete` option.
|
121
|
+
|
122
|
+
```plain
|
123
|
+
$ git finish
|
124
|
+
Checking for trivial merge from 12345678-lorem-ipsum to master... OK
|
125
|
+
Merging 12345678-lorem-ipsum to master... OK
|
126
|
+
Deleting 12345678-lorem-ipsum... OK
|
127
|
+
Pushing to origin... OK
|
128
|
+
```
|
129
|
+
|
130
|
+
The command checks that it will be able to do a trivial merge from the development branch to the target branch before it does anything. The check has the following constraints
|
131
|
+
|
132
|
+
1. The local repository must be up to date with the remote repository (e.g. `origin`)
|
133
|
+
2. The local merge target branch (e.g. `master`) must be up to date with the remote merge target branch (e.g. `origin/master`)
|
134
|
+
3. The common ancestor (i.e. the branch point) of the development branch (e.g. `12345678-lorem-ipsum`) must be tip of the local merge target branch (e.g. `master`)
|
135
|
+
|
136
|
+
If all of these conditions are met, the development branch will be merged into the target branch with a message of:
|
137
|
+
|
138
|
+
```plain
|
139
|
+
Merge 12345678-lorem-ipsum to master
|
140
|
+
|
141
|
+
[Completes #12345678]
|
142
|
+
```
|
143
|
+
|
144
|
+
The second way is with the `--no-complete` option specified. In this case `finish` performs the same actions except the `Completes`... statement in the commit message will be supressed.
|
145
|
+
|
146
|
+
```plain
|
147
|
+
Merge 12345678-lorem-ipsum to master
|
148
|
+
|
149
|
+
[#12345678]
|
150
|
+
```
|
151
|
+
|
152
|
+
After merging, the development branch is deleted and the changes are pushed to the remote repository.
|
153
|
+
|
154
|
+
### `git release [story-id]`
|
155
|
+
This command creates a release for a story. It does this by updating the version string in the project and creating a tag. This command can be run in two ways. First it can be run specifying the release that you want to create.
|
156
|
+
|
157
|
+
```plain
|
158
|
+
$ git release 12345678
|
159
|
+
```
|
160
|
+
The other way the command can be run without specifying anything. In this case, it will select the first release story (based on the backlog's order).
|
161
|
+
|
162
|
+
```plain
|
163
|
+
$ git release
|
164
|
+
Title: Lorem ipsum dolor sit amet, consectetur adipiscing elitattributes
|
165
|
+
```
|
166
|
+
|
167
|
+
Once a story has been selected by one of the two methods, the command then prompts for the release version and next development version.
|
168
|
+
|
169
|
+
```plain
|
170
|
+
$ git release
|
171
|
+
Title: Lorem ipsum dolor sit amet, consectetur adipiscing elitattributes
|
172
|
+
|
173
|
+
Enter release version (current: 1.0.0.BUILD-SNAPSHOT): 1.0.0.M1
|
174
|
+
Enter next development version (current: 1.0.0.BUILD-SNAPSHOT): 1.1.0.BUILD-SNAPSHOT
|
175
|
+
Creating tag v1.0.0.M1... OK
|
176
|
+
Pushing to origin... OK
|
177
|
+
```
|
178
|
+
|
179
|
+
Once these have been entered, the version string for the current project is updated to the release version and a tag is created. Then the version string for the current project is updated to the next development version and a new commit along the original branch is created. Finally the tag and changes are pushed to the remote sever.
|
180
|
+
|
181
|
+
Version update is currently supported for the following kinds of projects. If you do not see a project type that you would like supported, please open an issue or submit a pull request.
|
182
|
+
|
183
|
+
* Gradle
|
data/bin/git-finish
ADDED
data/bin/git-start
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Object
|
4
|
+
# An object is blank if it's false, empty, or a whitespace string.
|
5
|
+
# For example, '', ' ', +nil+, [], and {} are all blank.
|
6
|
+
#
|
7
|
+
# This simplifies:
|
8
|
+
#
|
9
|
+
# if address.nil? || address.empty?
|
10
|
+
#
|
11
|
+
# ...to:
|
12
|
+
#
|
13
|
+
# if address.blank?
|
14
|
+
def blank?
|
15
|
+
respond_to?(:empty?) ? empty? : !self
|
16
|
+
end
|
17
|
+
|
18
|
+
# An object is present if it's not <tt>blank?</tt>.
|
19
|
+
def present?
|
20
|
+
!blank?
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns object if it's <tt>present?</tt> otherwise returns +nil+.
|
24
|
+
# <tt>object.presence</tt> is equivalent to <tt>object.present? ? object : nil</tt>.
|
25
|
+
#
|
26
|
+
# This is handy for any representation of objects where blank is the same
|
27
|
+
# as not present at all. For example, this simplifies a common check for
|
28
|
+
# HTTP POST/query parameters:
|
29
|
+
#
|
30
|
+
# state = params[:state] if params[:state].present?
|
31
|
+
# country = params[:country] if params[:country].present?
|
32
|
+
# region = state || country || 'US'
|
33
|
+
#
|
34
|
+
# ...becomes:
|
35
|
+
#
|
36
|
+
# region = params[:state].presence || params[:country].presence || 'US'
|
37
|
+
def presence
|
38
|
+
self if present?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class NilClass
|
43
|
+
# +nil+ is blank:
|
44
|
+
#
|
45
|
+
# nil.blank? # => true
|
46
|
+
def blank?
|
47
|
+
true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class FalseClass
|
52
|
+
# +false+ is blank:
|
53
|
+
#
|
54
|
+
# false.blank? # => true
|
55
|
+
def blank?
|
56
|
+
true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class TrueClass
|
61
|
+
# +true+ is not blank:
|
62
|
+
#
|
63
|
+
# true.blank? # => false
|
64
|
+
def blank?
|
65
|
+
false
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Array
|
70
|
+
# An array is blank if it's empty:
|
71
|
+
#
|
72
|
+
# [].blank? # => true
|
73
|
+
# [1,2,3].blank? # => false
|
74
|
+
alias_method :blank?, :empty?
|
75
|
+
end
|
76
|
+
|
77
|
+
class Hash
|
78
|
+
# A hash is blank if it's empty:
|
79
|
+
#
|
80
|
+
# {}.blank? # => true
|
81
|
+
# { key: 'value' }.blank? # => false
|
82
|
+
alias_method :blank?, :empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
class String
|
86
|
+
# A string is blank if it's empty or contains whitespaces only:
|
87
|
+
#
|
88
|
+
# ''.blank? # => true
|
89
|
+
# ' '.blank? # => true
|
90
|
+
# ' '.blank? # => true
|
91
|
+
# ' something here '.blank? # => false
|
92
|
+
def blank?
|
93
|
+
self !~ /[^[:space:]]/
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class Numeric #:nodoc:
|
98
|
+
# No number is blank:
|
99
|
+
#
|
100
|
+
# 1.blank? # => false
|
101
|
+
# 0.blank? # => false
|
102
|
+
def blank?
|
103
|
+
false
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,31 @@
|
|
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
|
+
$:.unshift(File.dirname(__FILE__))
|
17
|
+
|
18
|
+
require 'highline/import'
|
19
|
+
require 'pivotal-tracker'
|
20
|
+
|
21
|
+
require File.join('core_ext', 'object', 'blank')
|
22
|
+
|
23
|
+
require File.join('github_pivotal_flow', 'shell')
|
24
|
+
require File.join('github_pivotal_flow', 'git')
|
25
|
+
require File.join('github_pivotal_flow', 'project')
|
26
|
+
require File.join('github_pivotal_flow', 'configuration')
|
27
|
+
require File.join('github_pivotal_flow', 'github_api')
|
28
|
+
require File.join('github_pivotal_flow', 'story')
|
29
|
+
require File.join('github_pivotal_flow', 'command')
|
30
|
+
require File.join('github_pivotal_flow', 'start')
|
31
|
+
require File.join('github_pivotal_flow', 'finish')
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# An abstract base class for all commands
|
2
|
+
# @abstract Subclass and override {#run} to implement command functionality
|
3
|
+
module GithubPivotalFlow
|
4
|
+
class Command
|
5
|
+
|
6
|
+
attr_reader :options, :configuration
|
7
|
+
|
8
|
+
# Common initialization functionality for all command classes. This
|
9
|
+
# enforces that:
|
10
|
+
# * the command is being run within a valid Git repository
|
11
|
+
# * the user has specified their Pivotal Tracker API token
|
12
|
+
# * all communication with Pivotal Tracker will be protected with SSL
|
13
|
+
# * the user has configured the project id for this repository
|
14
|
+
def initialize(*args)
|
15
|
+
@options = {}
|
16
|
+
args = parse_argv(*args)
|
17
|
+
@options[:args] = args
|
18
|
+
|
19
|
+
@repository_root = Git.repository_root
|
20
|
+
@configuration = Configuration.new
|
21
|
+
|
22
|
+
PivotalTracker::Client.token = @configuration.api_token
|
23
|
+
PivotalTracker::Client.use_ssl = true
|
24
|
+
|
25
|
+
@project = PivotalTracker::Project.find @configuration.project_id
|
26
|
+
|
27
|
+
# Make sure that all the git flow config options are set up
|
28
|
+
@configuration.development_branch
|
29
|
+
@configuration.master_branch
|
30
|
+
@configuration.feature_prefix
|
31
|
+
@configuration.hotfix_prefix
|
32
|
+
end
|
33
|
+
|
34
|
+
# The main entry point to the command's execution
|
35
|
+
# @abstract Override this method to implement command functionality
|
36
|
+
def run!
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def parse_argv(*args)
|
43
|
+
OptionParser.new do |opts|
|
44
|
+
opts.banner = "Usage: git start <feature|chore|bug|story_id> | git finish"
|
45
|
+
opts.on("-t", "--api-token=", "Pivotal Tracker API key") { |k| options[:api_token] = k }
|
46
|
+
opts.on("-p", "--project-id=", "Pivotal Tracker project id") { |p| options[:project_id] = p }
|
47
|
+
opts.on("-n", "--full-name=", "Your Pivotal Tracker full name") { |n| options[:full_name] = n }
|
48
|
+
opts.on_tail("-h", "--help", "This usage guide") { put opts.to_s; exit 0 }
|
49
|
+
end.parse!(args)
|
50
|
+
end
|
51
|
+
|
52
|
+
def current_branch_name
|
53
|
+
Git.branch_name
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,251 @@
|
|
1
|
+
require 'highline/import'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module GithubPivotalFlow
|
5
|
+
# A class that exposes configuration that commands can use
|
6
|
+
class Configuration
|
7
|
+
def initialize
|
8
|
+
@github_password_cache = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns the user's Pivotal Tracker API token. If this token has not been
|
12
|
+
# configured, prompts the user for the value. The value is checked for in
|
13
|
+
# the _inherited_ Git configuration, but is stored in the _global_ Git
|
14
|
+
# configuration so that it can be used across multiple repositories.
|
15
|
+
#
|
16
|
+
# @return [String] The user's Pivotal Tracker API token
|
17
|
+
def api_token
|
18
|
+
api_token = Git.get_config KEY_API_TOKEN, :inherited
|
19
|
+
|
20
|
+
if api_token.empty?
|
21
|
+
api_token = ask('Pivotal API Token (found at https://www.pivotaltracker.com/profile): ').strip
|
22
|
+
Git.set_config KEY_API_TOKEN, api_token, :global
|
23
|
+
puts
|
24
|
+
end
|
25
|
+
|
26
|
+
api_token
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the Pivotal Tracker project id for this repository. If this id
|
30
|
+
# has not been configuration, prompts the user for the value. The value is
|
31
|
+
# checked for in the _inherited_ Git configuration, but is stored in the
|
32
|
+
# _local_ Git configuration so that it is specific to this repository.
|
33
|
+
#
|
34
|
+
# @return [String] The repository's Pivotal Tracker project id
|
35
|
+
def project_id
|
36
|
+
project_id = Git.get_config KEY_PROJECT_ID, :inherited
|
37
|
+
|
38
|
+
if project_id.empty?
|
39
|
+
project_id = choose do |menu|
|
40
|
+
menu.prompt = 'Choose project associated with this repository: '
|
41
|
+
|
42
|
+
PivotalTracker::Project.all.sort_by { |project| project.name }.each do |project|
|
43
|
+
menu.choice(project.name) { project.id }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
Git.set_config KEY_PROJECT_ID, project_id, :local
|
48
|
+
puts
|
49
|
+
end
|
50
|
+
|
51
|
+
project_id
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the story associated with the current development branch
|
55
|
+
#
|
56
|
+
# @param [PivotalTracker::Project] project the project the story belongs to
|
57
|
+
# @return [PivotalTracker::Story] the story associated with the current development branch
|
58
|
+
def story(project)
|
59
|
+
story_id = Git.get_config(KEY_STORY_ID, :branch)
|
60
|
+
Story.new(project.stories.find(story_id.to_i), :branch_name => Git.current_branch)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Stores the story associated with the current development branch
|
64
|
+
#
|
65
|
+
# @param [PivotalTracker::Story] story the story associated with the current development branch
|
66
|
+
# @return [void]
|
67
|
+
def story=(story)
|
68
|
+
Git.set_config KEY_STORY_ID, story.id, :branch
|
69
|
+
end
|
70
|
+
|
71
|
+
def feature_prefix
|
72
|
+
feature_prefix = Git.get_config KEY_FEATURE_PREFIX, :inherited
|
73
|
+
|
74
|
+
if feature_prefix.empty?
|
75
|
+
feature_prefix = ask('Please enter your git-flow feature branch prefix: [feature]').strip
|
76
|
+
feature_prefix = 'feature' if feature_prefix.blank?
|
77
|
+
Git.set_config KEY_FEATURE_PREFIX, feature_prefix, :local
|
78
|
+
puts
|
79
|
+
end
|
80
|
+
|
81
|
+
feature_prefix
|
82
|
+
end
|
83
|
+
|
84
|
+
def hotfix_prefix
|
85
|
+
hotfix_prefix = Git.get_config KEY_HOTFIX_PREFIX, :inherited
|
86
|
+
|
87
|
+
if hotfix_prefix.empty?
|
88
|
+
hotfix_prefix = ask('Please enter your git-flow hotfix branch prefix: [hotfix]').strip
|
89
|
+
hotfix_prefix = 'hotfix' if hotfix_prefix.blank?
|
90
|
+
Git.set_config KEY_HOTFIX_PREFIX, hotfix_prefix, :local
|
91
|
+
puts
|
92
|
+
end
|
93
|
+
|
94
|
+
hotfix_prefix
|
95
|
+
end
|
96
|
+
|
97
|
+
def release_prefix
|
98
|
+
release_prefix = Git.get_config KEY_RELEASE_PREFIX, :inherited
|
99
|
+
|
100
|
+
if release_prefix.empty?
|
101
|
+
release_prefix = ask('Please enter your git-flow release branch prefix: [release]').strip
|
102
|
+
release_prefix = 'release' if release_prefix.blank?
|
103
|
+
Git.set_config KEY_RELEASE_PREFIX, release_prefix, :local
|
104
|
+
puts
|
105
|
+
end
|
106
|
+
|
107
|
+
release_prefix
|
108
|
+
end
|
109
|
+
|
110
|
+
def development_branch
|
111
|
+
development_branch = Git.get_config KEY_DEVELOPMENT_BRANCH, :inherited
|
112
|
+
|
113
|
+
if development_branch.empty?
|
114
|
+
development_branch = ask('Please enter your git-flow development branch name: [development]').strip
|
115
|
+
development_branch = 'development' if development_branch.nil? || development_branch.empty?
|
116
|
+
Git.set_config KEY_DEVELOPMENT_BRANCH, development_branch, :local
|
117
|
+
puts
|
118
|
+
end
|
119
|
+
Git.ensure_branch_exists(development_branch)
|
120
|
+
|
121
|
+
development_branch
|
122
|
+
end
|
123
|
+
|
124
|
+
def master_branch
|
125
|
+
master_branch = Git.get_config KEY_MASTER_BRANCH, :inherited
|
126
|
+
|
127
|
+
if master_branch.blank?
|
128
|
+
master_branch = ask('Please enter your git-flow production branch name: [master]').strip
|
129
|
+
master_branch = 'master' if master_branch.nil? || master_branch.empty?
|
130
|
+
Git.set_config KEY_MASTER_BRANCH, master_branch, :local
|
131
|
+
puts
|
132
|
+
end
|
133
|
+
Git.ensure_branch_exists(master_branch)
|
134
|
+
|
135
|
+
master_branch
|
136
|
+
end
|
137
|
+
|
138
|
+
def github_project
|
139
|
+
@github_project ||= Project.from_url(URI(Git.get_config("remote.#{Git.get_remote}.url")))
|
140
|
+
end
|
141
|
+
|
142
|
+
def github_username(host)
|
143
|
+
return ENV['GITHUB_USER'] unless ENV['GITHUB_USER'].to_s.blank?
|
144
|
+
github_username = Git.get_config KEY_GITHUB_USERNAME, :inherited
|
145
|
+
if github_username.blank?
|
146
|
+
github_username = ask('Github username: ').strip
|
147
|
+
Git.set_config KEY_GITHUB_USERNAME, github_username, :local
|
148
|
+
end
|
149
|
+
github_username
|
150
|
+
end
|
151
|
+
|
152
|
+
def github_username=(github_username)
|
153
|
+
Git.set_config KEY_GITHUB_USERNAME, github_username, :local unless github_username.blank?
|
154
|
+
end
|
155
|
+
|
156
|
+
def github_password(host, user)
|
157
|
+
return ENV['GITHUB_PASSWORD'] unless ENV['GITHUB_PASSWORD'].to_s.empty?
|
158
|
+
@github_password_cache["#{user}"] ||= ask_password(user)
|
159
|
+
end
|
160
|
+
|
161
|
+
def github_api_token(host, user)
|
162
|
+
github_token = Git.get_config KEY_GITHUB_API_TOKEN, :inherited
|
163
|
+
if github_token.blank?
|
164
|
+
github_token = yield
|
165
|
+
Git.set_config KEY_GITHUB_API_TOKEN, github_token, :local
|
166
|
+
end
|
167
|
+
github_token
|
168
|
+
end
|
169
|
+
|
170
|
+
# special prompt that has hidden input
|
171
|
+
def ask_password(user)
|
172
|
+
print "Github password for #{user} (never stored): "
|
173
|
+
if $stdin.tty?
|
174
|
+
password = askpass
|
175
|
+
puts ''
|
176
|
+
password
|
177
|
+
else
|
178
|
+
# in testing
|
179
|
+
$stdin.gets.chomp
|
180
|
+
end
|
181
|
+
rescue Interrupt
|
182
|
+
abort
|
183
|
+
end
|
184
|
+
|
185
|
+
def ask_auth_code
|
186
|
+
print "two-factor authentication code: "
|
187
|
+
$stdin.gets.chomp
|
188
|
+
rescue Interrupt
|
189
|
+
abort
|
190
|
+
end
|
191
|
+
|
192
|
+
def askpass
|
193
|
+
noecho $stdin do |input|
|
194
|
+
input.gets.chomp
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def noecho io
|
199
|
+
require 'io/console'
|
200
|
+
io.noecho { yield io }
|
201
|
+
rescue LoadError
|
202
|
+
fallback_noecho io
|
203
|
+
end
|
204
|
+
|
205
|
+
def fallback_noecho io
|
206
|
+
tty_state = `stty -g 2>#{NULL}`
|
207
|
+
system 'stty raw -echo -icanon isig' if $?.success?
|
208
|
+
pass = ''
|
209
|
+
while char = getbyte(io) and !(char == 13 or char == 10)
|
210
|
+
if char == 127 or char == 8
|
211
|
+
pass[-1,1] = '' unless pass.empty?
|
212
|
+
else
|
213
|
+
pass << char.chr
|
214
|
+
end
|
215
|
+
end
|
216
|
+
pass
|
217
|
+
ensure
|
218
|
+
system "stty #{tty_state}" unless tty_state.empty?
|
219
|
+
end
|
220
|
+
|
221
|
+
def getbyte(io)
|
222
|
+
if io.respond_to?(:getbyte)
|
223
|
+
io.getbyte
|
224
|
+
else
|
225
|
+
# In Ruby <= 1.8.6, getc behaved the same
|
226
|
+
io.getc
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def proxy_uri(with_ssl)
|
231
|
+
env_name = "HTTP#{with_ssl ? 'S' : ''}_PROXY"
|
232
|
+
if proxy = ENV[env_name] || ENV[env_name.downcase] and !proxy.empty?
|
233
|
+
proxy = "http://#{proxy}" unless proxy.include? '://'
|
234
|
+
URI.parse proxy
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
KEY_API_TOKEN = 'pivotal.api-token'.freeze
|
241
|
+
KEY_PROJECT_ID = 'pivotal.project-id'.freeze
|
242
|
+
KEY_STORY_ID = 'pivotal-story-id'.freeze
|
243
|
+
KEY_FEATURE_PREFIX = 'gitflow.prefix.feature'.freeze
|
244
|
+
KEY_HOTFIX_PREFIX = 'gitflow.prefix.hotfix'.freeze
|
245
|
+
KEY_RELEASE_PREFIX = 'gitflow.prefix.release'.freeze
|
246
|
+
KEY_DEVELOPMENT_BRANCH = 'gitflow.branch.develop'.freeze
|
247
|
+
KEY_MASTER_BRANCH = 'gitflow.branch.master'.freeze
|
248
|
+
KEY_GITHUB_USERNAME = 'github.username'.freeze
|
249
|
+
KEY_GITHUB_API_TOKEN = 'github.api-token'.freeze
|
250
|
+
end
|
251
|
+
end
|