git_reflow 0.2

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.
@@ -0,0 +1,138 @@
1
+ Given /^I have a git repository with a branch named "([^"]+)" checked out$/ do |branch_name|
2
+ steps %{
3
+ Given a directory named "master_repo"
4
+ And I cd to "master_repo"
5
+ And I write to "README" with:
6
+ | Initialized |
7
+ And I successfully run `git init`
8
+ And I successfully run `git add README`
9
+ And I successfully run `git commit -m "Initial commit"`
10
+ }
11
+
12
+ unless branch_name == "master"
13
+ steps %{
14
+ And I successfully run `git checkout -b #{branch_name}`
15
+ }
16
+ end
17
+
18
+ steps %{
19
+ And I cd to ".."
20
+ }
21
+ end
22
+
23
+ Given /^I have a remote git repository named "([^"]+)"$/ do |remote_name|
24
+ steps %{
25
+ Given a directory named "#{remote_name}_repo"
26
+ When I cd to "#{remote_name}_repo"
27
+ And I successfully run `git init`
28
+ And I write to "README" with:
29
+ | Initialized |
30
+ And I successfully run `git add .`
31
+ And I successfully run `git commit -am "Initial commit"`
32
+ And I cd to ".."
33
+ And I cd to "master_repo"
34
+ And I successfully run `git remote add #{remote_name} ../#{remote_name}_repo`
35
+ And I cd to ".."
36
+ }
37
+ end
38
+
39
+ Given /^the remote repository named "([^"]+)" has changes on the "([^"]+)" branch$/ do |remote_name, branch_name|
40
+ steps %{
41
+ Given a directory named "#{remote_name}_repo"
42
+ When I cd to "#{remote_name}_repo"
43
+ And I successfully run `git checkout #{branch_name}`
44
+ And I append to "README" with:
45
+ | changed |
46
+ And I successfully run `git add .`
47
+ And I successfully run `git commit -am "Changed readme"`
48
+ And I cd to ".."
49
+ }
50
+ end
51
+
52
+ Given /^the repository has been initialized$/ do
53
+ steps %{
54
+ Given I successfully run `git branch`
55
+ Then the output should contain "master"
56
+ }
57
+ end
58
+
59
+ Given /^I have a new branch named "([^"]+)" checked out$/ do |branch_name|
60
+ steps %{
61
+ When I cd to "master_repo"
62
+ And I successfully run `git checkout -b #{branch_name}`
63
+ }
64
+ end
65
+
66
+ Given /^I have a reviewed feature branch named "([^"]+)" checked out$/ do |branch_name|
67
+ pull = {
68
+ "title" => "Amazing new feature",
69
+ "body" => "Please pull this in!",
70
+ "head" => "reenhanced:#{branch_name}",
71
+ "base" => "master",
72
+ "state" => "open"
73
+ }
74
+ stub_github_with(
75
+ :user => 'reenhanced',
76
+ :repo => 'repo',
77
+ :branch => branch_name,
78
+ :pull => pull
79
+ )
80
+
81
+ review_options = {
82
+ 'base' => pull['base'],
83
+ 'title' => pull['title'],
84
+ 'body' => pull['body']
85
+ }
86
+
87
+ GitReflow.review review_options
88
+
89
+ # ensure we do not stay inside the remote repo
90
+ steps %{
91
+ Given I cd to ".."
92
+ }
93
+ end
94
+
95
+ When /^I deliver my "([^"]+)" branch$/ do |branch_name|
96
+ pull = {
97
+ "title" => "Amazing new feature",
98
+ "body" => "Please pull this in!",
99
+ "head" => "reenhanced:#{branch_name}",
100
+ "base" => "master",
101
+ "state" => "open"
102
+ }
103
+ stub_github_with(
104
+ :user => 'reenhanced',
105
+ :repo => 'repo',
106
+ :branch => branch_name,
107
+ :pull => pull
108
+ )
109
+ GitReflow.deliver
110
+ GitReflow.stub(:current_branch).and_return("master")
111
+ end
112
+
113
+ Then /^a branch named "([^"]+)" should have been created from "([^"]+)"$/ do |new_branch, base_branch|
114
+ steps %{
115
+ Then the output should match /\\* \\[new branch\\]\\s* #{Regexp.escape(base_branch)}\\s* \\-\\> #{Regexp.escape(new_branch)}/
116
+ }
117
+ end
118
+
119
+ Then /^the base branch named "([^"]+)" should have fetched changes from the remote git repository "([^"]+)"$/ do |base_branch, remote_name|
120
+ steps %{
121
+ Then the output should match /\\* \\[new branch\\]\\s* #{Regexp.escape(base_branch)}\\s* \\-\\> #{remote_name}.#{Regexp.escape(base_branch)}/
122
+ }
123
+ end
124
+
125
+ Then /^the subcommand "([^"]+)" should run$/ do |subcommand|
126
+ has_subcommand?(subcommand).should be_true
127
+ end
128
+
129
+ Then /^the branch "([^"]+)" should be checked out$/ do |branch_name|
130
+ GitReflow.current_branch.should == branch_name
131
+ end
132
+
133
+ Then /^the branch "([^"]+)" should be up to date with the remote repository$/ do |branch_name|
134
+ steps %{
135
+ When I successfully run `git pull origin #{branch_name}`
136
+ Then the output should contain "Already up-to-date"
137
+ }
138
+ end
@@ -0,0 +1,27 @@
1
+ require 'aruba/cucumber'
2
+ require 'ruby-debug'
3
+ require 'webmock/cucumber'
4
+ require 'cucumber/rspec/doubles'
5
+
6
+ Before('@gem') do
7
+ CukeGem.setup('./git_reflow.gemspec')
8
+ end
9
+
10
+ After('@gem') do
11
+ CukeGem.teardown
12
+ end
13
+
14
+ Before do
15
+ FileUtils.rm_rf Dir.glob("#{Dir.tmpdir}/aruba")
16
+ end
17
+
18
+ WebMock.disable_net_connect!
19
+
20
+ def has_subcommand?(command)
21
+ # In order to see if a subcommand is run
22
+ # we have to look it up in Aruba's process list
23
+ # Aruba has a get_process helper, but it errors if none is found
24
+ # See: https://github.com/cucumber/aruba/blob/master/lib/aruba/api.rb#L239
25
+ found = processes.reverse.find{ |name, _| name == command }
26
+ found[-1] if found
27
+ end
@@ -0,0 +1,85 @@
1
+ # Thanks to:
2
+ # Copyright 2011 Solano Labs All Rights Reserved
3
+ # https://gist.github.com/1132465
4
+
5
+ require 'aruba'
6
+ require 'aruba/api'
7
+
8
+ class CukeGem
9
+ @setup_done = false
10
+
11
+ class << self
12
+ include Aruba::Api
13
+
14
+ attr_reader :setup_done
15
+
16
+ def setup(gemspec, once=true)
17
+ gem_home = setup_env
18
+ if !@setup_done || !once then
19
+ @setup_done = true
20
+ mkgemdir(gem_home)
21
+ gem_install(gemspec)
22
+ end
23
+ end
24
+
25
+ def teardown
26
+ restore_env
27
+ end
28
+
29
+ def setup_env
30
+ tid = ENV['TDDIUM_TID'] || ''
31
+ gem_home = File.join(ENV['HOME'], 'tmp', 'aruba-gem')
32
+ gem_home = File.expand_path(gem_home)
33
+
34
+ set_env('GEM_HOME', gem_home)
35
+ set_env('GEM_PATH', gem_home)
36
+ set_env('BUNDLE_PATH', gem_home)
37
+ unset_bundler_env_vars
38
+
39
+ paths = (ENV['PATH'] || "").split(File::PATH_SEPARATOR)
40
+ paths.unshift(File.join(gem_home, 'bin'))
41
+ set_env('PATH', paths.uniq.join(File::PATH_SEPARATOR))
42
+
43
+ return gem_home
44
+ end
45
+
46
+ def mkgemdir(gem_home)
47
+ FileUtils::rm_rf(gem_home)
48
+ FileUtils::mkdir_p(gem_home)
49
+
50
+ output = `gem install bundler`
51
+ if $?.exitstatus != 0 then
52
+ raise "unable to install bundler into #{gem_home}: #{output}"
53
+ end
54
+ end
55
+
56
+ def gem_install(gemspec)
57
+ gem_file = nil
58
+ begin
59
+ pwd = Dir.pwd
60
+ gemspec_dir = File.dirname(gemspec)
61
+ Dir.chdir(gemspec_dir)
62
+ output = `gem build #{File.basename(gemspec)}`
63
+ Dir.chdir(pwd)
64
+
65
+ if $?.exitstatus != 0 then
66
+ raise "unable to build gem: #{output}"
67
+ end
68
+
69
+ if output =~ /File:\s+([A-Za-z0-9_.-]+[.]gem)/ then
70
+ gem_file = $1
71
+ output = `gem install #{File.join(gemspec_dir, gem_file)}`
72
+ if $?.exitstatus != 0 then
73
+ raise "unable to install gem: #{output}"
74
+ end
75
+ else
76
+ raise "garbled gem build output: #{output}"
77
+ end
78
+ ensure
79
+ if gem_file then
80
+ FileUtils.rm_f(File.join(gemspec_dir, gem_file))
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,3 @@
1
+ require File.expand_path('../../../spec/support/github_helpers', __FILE__)
2
+
3
+ World(GithubHelpers)
@@ -0,0 +1,22 @@
1
+ @gem
2
+ Feature: User delivers a flow
3
+ As a User
4
+ I can deliver a flow
5
+ So I can merge in my topic branch
6
+
7
+ Background:
8
+ Given I have a git repository with a branch named "master" checked out
9
+ And I have a remote git repository named "origin"
10
+ And the remote repository named "origin" has changes on the "master" branch
11
+ And I cd to "master_repo"
12
+ When I run `git-reflow start new-branch`
13
+ And I append to "README" with:
14
+ | changed |
15
+ And I successfully run `git add .`
16
+ And I successfully run `git commit -am "Changed readme"`
17
+ Given I have a reviewed feature branch named "new-feature" checked out
18
+
19
+ Scenario: User runs git-reflow deliver without any parameters
20
+ When I deliver my "new-feature" branch
21
+ Then the branch "master" should be checked out
22
+ And the branch "master" should be up to date with the remote repository
@@ -0,0 +1,18 @@
1
+ Feature: User installs gem
2
+ As a user
3
+ When I install a gem
4
+ It should initialize the gem configuration
5
+
6
+ Scenario: User installs gem
7
+ When I build and install the gem
8
+ Then the output should contain "You need to setup your GitHub OAuth token\nPlease run 'git-reflow setup'"
9
+ When I successfully run `git-reflow`
10
+ Then the output should contain "usage: git-reflow [global options] command [command options]"
11
+
12
+ Scenario: User sets up GitHub
13
+ When I run `git-reflow setup` interactively
14
+ And I type "user"
15
+ And I type "password"
16
+ Then the output should contain "Please enter your GitHub username: "
17
+ And the output should contain "Please enter your GitHub password (we do NOT store this): "
18
+ And the output should contain "Your GitHub account was successfully setup!"
@@ -0,0 +1,19 @@
1
+ @gem
2
+ Feature: User starts a new flow
3
+ As a User
4
+ When I start a new flow
5
+ I should be on a new working feature branch
6
+
7
+ Scenario: User runs git-reflow start without any parameters
8
+ When I run `git-reflow start`
9
+ Then the output should contain "usage: git-reflow start [new-branch-name]"
10
+
11
+ Scenario: User runs git-reflow start with new branch name
12
+ Given I have a git repository with a branch named "master" checked out
13
+ And I have a remote git repository named "origin"
14
+ And the remote repository named "origin" has changes on the "master" branch
15
+ And I cd to "master_repo"
16
+ When I run `git-reflow start new-branch`
17
+ Then a branch named "new-branch" should have been created from "master"
18
+ And the base branch named "master" should have fetched changes from the remote git repository "origin"
19
+ And the output should contain "Switched to a new branch 'new-branch'"
@@ -0,0 +1,32 @@
1
+ # Ensure we require the local version and not one we might have installed already
2
+ require File.join([File.dirname(__FILE__),'lib','git_reflow/version.rb'])
3
+ spec = Gem::Specification.new do |s|
4
+ s.name = 'git_reflow'
5
+ s.version = GitReflow::VERSION
6
+ s.authors = ["Valentino Stoll", "Robert Stern", "Nicholas Hance"]
7
+ s.email = ["dev@reenhanced.com"]
8
+ s.homepage = "http://github.com/reenhanced/gitreflow"
9
+ s.summary = "A better git process"
10
+ s.description = "Git Reflow manages your git workflow."
11
+ s.platform = Gem::Platform::RUBY
12
+ s.files = `git ls-files`.split("\n")
13
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
15
+ s.has_rdoc = true
16
+ s.extra_rdoc_files = ['README.rdoc']
17
+ s.bindir = 'bin'
18
+ s.require_paths << 'lib'
19
+ s.rdoc_options << '--title' << 'git_reflow' << '--main' << 'README.rdoc' << '-ri'
20
+ s.add_development_dependency('rake')
21
+ s.add_development_dependency('rdoc')
22
+ s.add_development_dependency('rspec')
23
+ s.add_development_dependency('aruba', '~> 0.4.6')
24
+ s.add_development_dependency('jeweler')
25
+ s.add_development_dependency('webmock')
26
+ s.add_dependency('gli', '2.0.0')
27
+ s.add_dependency('json_pure', '1.7.5')
28
+ s.add_dependency('highline')
29
+ s.add_dependency('httpclient')
30
+ s.add_dependency('github_api', '0.6.5')
31
+ s.post_install_message = "You need to setup your GitHub OAuth token\nPlease run 'git-reflow setup'"
32
+ end
File without changes
@@ -0,0 +1,3 @@
1
+ module GitReflow
2
+ VERSION = "0.2"
3
+ end
data/lib/git_reflow.rb ADDED
@@ -0,0 +1,233 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'json/pure'
4
+ require 'open-uri'
5
+ require "highline/import"
6
+ require 'httpclient'
7
+ require 'github_api'
8
+
9
+ module GitReflow
10
+ extend self
11
+
12
+ LGTM = /lgtm|looks good to me|:\+1:|:thumbsup:/i
13
+
14
+ def setup
15
+ gh_user = ask "Please enter your GitHub username: "
16
+ gh_password = ask "Please enter your GitHub password (we do NOT store this): "
17
+ puts "\nYour GitHub account was successfully setup!"
18
+ github = Github.new :basic_auth => "#{gh_user}:#{gh_password}"
19
+ authorization = github.oauth.create 'scopes' => ['repo']
20
+ oauth_token = authorization[:token]
21
+ set_oauth_token(oauth_token)
22
+ end
23
+
24
+ def review(options = {})
25
+ options['base'] ||= 'master'
26
+ fetch_destination options['base']
27
+
28
+ begin
29
+ puts push_current_branch
30
+ pull_request = github.pull_requests.create(remote_user, remote_repo_name,
31
+ 'title' => options['title'],
32
+ 'body' => options['body'],
33
+ 'head' => "#{remote_user}:#{current_branch}",
34
+ 'base' => options['base'])
35
+
36
+ puts "Successfully created pull request ##{pull_request.number}: #{pull_request.title}\nPull Request URL: #{pull_request.html_url}\n"
37
+ ask_to_open_in_browser(pull_request.html_url)
38
+ rescue Github::Error::UnprocessableEntity => e
39
+ error_message = e.to_s
40
+ if error_message =~ /request already exists/i
41
+ existing_pull_request = find_pull_request( :from => current_branch, :to => options['base'] )
42
+ puts "Existing pull request at: #{existing_pull_request[:html_url]}"
43
+ ask_to_open_in_browser(existing_pull_request.html_url)
44
+ else
45
+ puts error_message
46
+ end
47
+ end
48
+ end
49
+
50
+ def deliver(options = {})
51
+ feature_branch = current_branch
52
+ options['base'] ||= 'master'
53
+ fetch_destination options['base']
54
+
55
+ begin
56
+ existing_pull_request = find_pull_request( :from => current_branch, :to => options['base'] )
57
+
58
+ if existing_pull_request.nil?
59
+ puts "Error: No pull request exists for #{remote_user}:#{current_branch}\nPlease submit your branch for review first with \`git reflow review\`"
60
+ else
61
+
62
+ open_comment_authors = find_authors_of_open_pull_request_comments(existing_pull_request)
63
+
64
+ # if there any comment_authors left, then they haven't given a lgtm after the last commit
65
+ if open_comment_authors.empty?
66
+ lgtm_authors = comment_authors_for_pull_request(existing_pull_request, :with => LGTM)
67
+ commit_message = get_first_commit_message
68
+ puts "Merging pull request ##{existing_pull_request[:number]}: '#{existing_pull_request[:title]}', from '#{existing_pull_request[:head][:label]}' into '#{existing_pull_request[:base][:label]}'"
69
+
70
+ update_destination(options['base'])
71
+ merge_feature_branch(:feature_branch => feature_branch,
72
+ :destination_branch => options['base'],
73
+ :pull_request_number => existing_pull_request[:number],
74
+ :message => "\nCloses ##{existing_pull_request[:number]}\n\nLGTM given by: @#{lgtm_authors.join(', @')}\n")
75
+ append_to_squashed_commit_message(commit_message)
76
+ committed = system('git commit')
77
+
78
+ if committed
79
+ puts "Merge complete!"
80
+ deploy_and_cleanup = ask "Would you like to push this branch to your remote repo and cleanup your feature branch? "
81
+ if deploy_and_cleanup =~ /^y/i
82
+ puts `git push origin #{options['base']}`
83
+ puts `git push origin :#{feature_branch}`
84
+ puts `git br -D #{feature_branch}`
85
+ puts "Nice job buddy."
86
+ end
87
+ else
88
+ puts "There were problems commiting your feature... please check the errors above and try again."
89
+ end
90
+ else
91
+ puts "[deliver halted] You still need a LGTM from: #{open_comment_authors.join(', ')}"
92
+ end
93
+ end
94
+
95
+ rescue Github::Error::UnprocessableEntity => e
96
+ errors = JSON.parse(e.response_message[:body])
97
+ error_messages = errors["errors"].collect {|error| "GitHub Error: #{error["message"].gsub(/^base\s/, '')}" unless error["message"].nil?}.compact.join("\n")
98
+ puts error_messages
99
+ end
100
+ end
101
+
102
+ def github
103
+ @github ||= Github.new :oauth_token => get_oauth_token
104
+ end
105
+
106
+ def get_oauth_token
107
+ `git config --get github.oauth-token`.strip
108
+ end
109
+
110
+ def current_branch
111
+ `git branch --no-color | grep '^\* ' | grep -v 'no branch' | sed 's/^* //g'`.strip
112
+ end
113
+
114
+ def github_user
115
+ `git config --get github.user`.strip
116
+ end
117
+
118
+ def remote_user
119
+ gh_remote_user = `git config --get remote.origin.url`.strip
120
+ gh_remote_user.slice!(/github\.com[\/:](\w|-|\.)+/i)[11..-1]
121
+ end
122
+
123
+ def remote_repo_name
124
+ gh_repo = `git config --get remote.origin.url`.strip
125
+ gh_repo.slice(/\/(\w|-|\.)+$/i)[1..-5]
126
+ end
127
+
128
+ def get_first_commit_message
129
+ `git log --pretty=format:"%s" --no-merges -n 1`.strip
130
+ end
131
+
132
+ private
133
+
134
+ def set_oauth_token(oauth_token)
135
+ `git config --global --replace-all github.oauth-token #{oauth_token}`
136
+ end
137
+
138
+ def push_current_branch
139
+ `git push origin #{current_branch}`
140
+ end
141
+
142
+ def fetch_destination(destination_branch)
143
+ `git fetch origin #{destination_branch}`
144
+ end
145
+
146
+ def update_destination(destination_branch)
147
+ origin_branch = current_branch
148
+ `git checkout #{destination_branch}`
149
+ puts `git pull origin #{destination_branch}`
150
+ `git checkout #{origin_branch}`
151
+ end
152
+
153
+ def merge_feature_branch(options = {})
154
+ options[:destination_branch] ||= 'master'
155
+ message = options[:message] || "\nCloses ##{options[:pull_request_number]}\n"
156
+
157
+ `git checkout #{options[:destination_branch]}`
158
+ puts `git merge --squash #{options[:feature_branch]}`
159
+ # append pull request number to commit message
160
+ append_to_squashed_commit_message(message)
161
+ end
162
+
163
+ def append_to_squashed_commit_message(message = '')
164
+ `echo "#{message}" | cat - .git/SQUASH_MSG > ./tmp_squash_msg`
165
+ `mv ./tmp_squash_msg .git/SQUASH_MSG`
166
+ end
167
+
168
+ def find_pull_request(options)
169
+ existing_pull_request = nil
170
+ github.pull_requests.all(remote_user, remote_repo_name, :state => 'open') do |pull_request|
171
+ if pull_request[:base][:label] == "#{remote_user}:#{options[:to]}" and
172
+ pull_request[:head][:label] == "#{remote_user}:#{options[:from]}"
173
+ existing_pull_request = pull_request
174
+ break
175
+ end
176
+ end
177
+ existing_pull_request
178
+ end
179
+
180
+ def find_authors_of_open_pull_request_comments(pull_request)
181
+ # first we'll gather all the authors that have commented on the pull request
182
+ comments = github.issues.comments.all remote_user, remote_repo_name, pull_request[:number]
183
+ review_comments = github.pull_requests.comments.all remote_user, remote_repo_name, pull_request[:number]
184
+ all_comments = comments + review_comments
185
+ comment_authors = comment_authors_for_pull_request(pull_request)
186
+
187
+ # now we need to check that all the commented authors have given a lgtm after the last commit
188
+ all_comments.each do |comment|
189
+ next unless comment_authors.include?(comment.user.login)
190
+ pull_last_committed_at = Time.parse pull_request.head.repo.updated_at
191
+ comment_created_at = Time.parse(comment.created_at)
192
+ if comment_created_at > pull_last_committed_at
193
+ if comment.body =~ LGTM
194
+ comment_authors -= [comment.user.login]
195
+ else
196
+ comment_authors << comment.user.login unless comment_authors.include?(comment.user.login)
197
+ end
198
+ end
199
+ end
200
+
201
+ comment_authors || []
202
+ end
203
+
204
+ def comment_authors_for_pull_request(pull_request, options = {})
205
+ comments = github.issues.comments.all remote_user, remote_repo_name, pull_request[:number]
206
+ review_comments = github.pull_requests.comments.all remote_user, remote_repo_name, pull_request[:number]
207
+ all_comments = comments + review_comments
208
+ comment_authors = []
209
+
210
+ all_comments.each do |comment|
211
+ comment_authors << comment.user.login if !comment_authors.include?(comment.user.login) and (options[:with].nil? or comment.body =~ options[:with])
212
+ end
213
+
214
+ # remove the current user from the list to check
215
+ comment_authors -= [github_user]
216
+ end
217
+
218
+ # WARNING: this currently only supports OS X and UBUNTU
219
+ def ask_to_open_in_browser(url)
220
+ if RUBY_PLATFORM =~ /darwin|linux/i
221
+ open_in_browser = ask "Would you like to open it in your browser? "
222
+ if open_in_browser =~ /^y/i
223
+ if RUBY_PLATFORM =~ /darwin/i
224
+ # OS X
225
+ `open #{url}`
226
+ else
227
+ # Ubuntu
228
+ `xdg-open #{url}`
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,7 @@
1
+ [user]
2
+ name = Reenhanced
3
+ email = dev@reenhanced.com
4
+ [github]
5
+ user = reenhanced
6
+ token = 123456
7
+ oauth-token = 123456