git_reflow 0.3.5 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,117 @@
1
+ [
2
+ {
3
+ "url": "https://api.github.com/reenhanced/repo/pulls/1",
4
+ "html_url": "https://github.com/reenhanced/repo/pulls/1",
5
+ "diff_url": "https://github.com/reenhanced/repo/pulls/1.diff",
6
+ "patch_url": "https://github.com/reenhanced/repo/pulls/1.patch",
7
+ "issue_url": "https://github.com/reenhanced/repo/issue/1",
8
+ "number": 1,
9
+ "state": "open",
10
+ "title": "new-feature",
11
+ "body": "Please pull these awesome changes",
12
+ "created_at": "2011-01-26T19:01:12Z",
13
+ "updated_at": "2011-01-26T19:01:12Z",
14
+ "closed_at": "2011-01-26T19:01:12Z",
15
+ "merged_at": "2011-01-26T19:01:12Z",
16
+ "head": {
17
+ "label": "new-feature",
18
+ "ref": "new-feature",
19
+ "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
20
+ "user": {
21
+ "login": "reenhanced",
22
+ "id": 1,
23
+ "avatar_url": "https://github.com/images/error/reenhanced_happy.gif",
24
+ "gravatar_id": "somehexcode",
25
+ "url": "https://api.github.com/users/reenhanced"
26
+ },
27
+ "repo": {
28
+ "url": "https://api.github.com/repos/reenhanced/repo",
29
+ "html_url": "https://github.com/reenhanced/repo",
30
+ "clone_url": "https://github.com/reenhanced/repo.git",
31
+ "git_url": "git://github.com/reenhanced/repo.git",
32
+ "ssh_url": "git@github.com:reenhanced/repo.git",
33
+ "svn_url": "https://svn.github.com/reenhanced/repo",
34
+ "mirror_url": "git://git.example.com/reenhanced/repo",
35
+ "id": 1296269,
36
+ "owner": {
37
+ "login": "reenhanced",
38
+ "id": 1,
39
+ "avatar_url": "https://github.com/images/error/reenhanced_happy.gif",
40
+ "gravatar_id": "somehexcode",
41
+ "url": "https://api.github.com/users/reenhanced"
42
+ },
43
+ "name": "repo",
44
+ "description": "This your first repo!",
45
+ "homepage": "https://github.com",
46
+ "language": null,
47
+ "private": false,
48
+ "fork": false,
49
+ "forks": 9,
50
+ "watchers": 80,
51
+ "size": 108,
52
+ "master_branch": "master",
53
+ "open_issues": 0,
54
+ "pushed_at": "2011-01-26T19:06:43Z",
55
+ "created_at": "2011-01-26T19:01:12Z",
56
+ "updated_at": "2011-01-26T19:14:43Z"
57
+ }
58
+ },
59
+ "base": {
60
+ "label": "master",
61
+ "ref": "master",
62
+ "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
63
+ "user": {
64
+ "login": "reenhanced",
65
+ "id": 1,
66
+ "avatar_url": "https://github.com/images/error/reenhanced_happy.gif",
67
+ "gravatar_id": "somehexcode",
68
+ "url": "https://api.github.com/users/reenhanced"
69
+ },
70
+ "repo": {
71
+ "url": "https://api.github.com/repos/reenhanced/repo",
72
+ "html_url": "https://github.com/reenhanced/repo",
73
+ "clone_url": "https://github.com/reenhanced/repo.git",
74
+ "git_url": "git://github.com/reenhanced/repo.git",
75
+ "ssh_url": "git@github.com:reenhanced/repo.git",
76
+ "svn_url": "https://svn.github.com/reenhanced/repo",
77
+ "mirror_url": "git://git.example.com/reenhanced/repo",
78
+ "id": 1296269,
79
+ "owner": {
80
+ "login": "reenhanced",
81
+ "id": 1,
82
+ "avatar_url": "https://github.com/images/error/reenhanced_happy.gif",
83
+ "gravatar_id": "somehexcode",
84
+ "url": "https://api.github.com/users/reenhanced"
85
+ },
86
+ "name": "repo",
87
+ "description": "This your first repo!",
88
+ "homepage": "https://github.com",
89
+ "language": null,
90
+ "private": false,
91
+ "fork": false,
92
+ "forks": 9,
93
+ "watchers": 80,
94
+ "size": 108,
95
+ "master_branch": "master",
96
+ "open_issues": 0,
97
+ "pushed_at": "2011-01-26T19:06:43Z",
98
+ "created_at": "2011-01-26T19:01:12Z",
99
+ "updated_at": "2011-01-26T19:14:43Z"
100
+ }
101
+ },
102
+ "_links": {
103
+ "self": {
104
+ "href": "https://api.github.com/reenhanced/repo/pulls/1"
105
+ },
106
+ "html": {
107
+ "href": "https://github.com/reenhanced/repo/pull/1"
108
+ },
109
+ "comments": {
110
+ "href": "https://api.github.com/reenhanced/repo/issues/1/comments"
111
+ },
112
+ "review_comments": {
113
+ "href": "https://api.github.com/reenhanced/repo/pulls/1/comments"
114
+ }
115
+ }
116
+ }
117
+ ]
@@ -0,0 +1,31 @@
1
+ [
2
+ {
3
+ "created_at": "2012-07-20T01:19:13Z",
4
+ "updated_at": "2012-07-20T01:19:13Z",
5
+ "state": "success",
6
+ "target_url": "https://ci.example.com/1000/output",
7
+ "description": "Build has completed successfully",
8
+ "id": 1,
9
+ "url": "https://api.github.com/repos/reenhanced/example/statuses/1",
10
+ "context": "continuous-integration/jenkins",
11
+ "creator": {
12
+ "login": "reenhanced",
13
+ "id": 1,
14
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
15
+ "gravatar_id": "somehexcode",
16
+ "url": "https://api.github.com/users/reenhanced",
17
+ "html_url": "https://github.com/reenhanced",
18
+ "followers_url": "https://api.github.com/users/reenhanced/followers",
19
+ "following_url": "https://api.github.com/users/reenhanced/following{/other_user}",
20
+ "gists_url": "https://api.github.com/users/reenhanced/gists{/gist_id}",
21
+ "starred_url": "https://api.github.com/users/reenhanced/starred{/owner}{/repo}",
22
+ "subscriptions_url": "https://api.github.com/users/reenhanced/subscriptions",
23
+ "organizations_url": "https://api.github.com/users/reenhanced/orgs",
24
+ "repos_url": "https://api.github.com/users/reenhanced/repos",
25
+ "events_url": "https://api.github.com/users/reenhanced/events{/privacy}",
26
+ "received_events_url": "https://api.github.com/users/reenhanced/received_events",
27
+ "type": "User",
28
+ "site_admin": false
29
+ }
30
+ }
31
+ ]
@@ -0,0 +1,386 @@
1
+ require 'spec_helper'
2
+
3
+ describe GitReflow do
4
+ let(:git_server) { GitReflow::GitServer::GitHub.new {} }
5
+ let(:github) { Github.new basic_auth: "#{user}:#{password}" }
6
+ let(:user) { 'reenhanced' }
7
+ let(:password) { 'shazam' }
8
+ let(:oauth_token_hash) { Hashie::Mash.new({ token: 'a1b2c3d4e5f6g7h8i9j0', note: 'hostname.local git-reflow'}) }
9
+ let(:repo) { 'repo' }
10
+ let(:base_branch) { 'master' }
11
+ let(:feature_branch) { 'new-feature' }
12
+ let(:enterprise_site) { 'https://github.reenhanced.com' }
13
+ let(:enterprise_api) { 'https://github.reenhanced.com' }
14
+ let(:hostname) { 'hostname.local' }
15
+
16
+ let(:github_authorizations) { Github::Client::Authorizations.new }
17
+ let(:existing_pull_requests) { JSON.parse(fixture('pull_requests/pull_requests.json').read).collect { |pull| Hashie::Mash.new(pull)} }
18
+ let(:existing_pull_request) { existing_pull_requests.first }
19
+
20
+ before do
21
+ HighLine.any_instance.stub(:ask) do |terminal, question|
22
+ values = {
23
+ "Please enter your GitHub username: " => user,
24
+ "Please enter your GitHub password (we do NOT store this): " => password,
25
+ "Please enter your Enterprise site URL (e.g. https://github.company.com):" => enterprise_site,
26
+ "Please enter your Enterprise API endpoint (e.g. https://github.company.com/api/v3):" => enterprise_api,
27
+ "Would you like to push this branch to your remote repo and cleanup your feature branch? " => 'yes',
28
+ "Would you like to open it in your browser?" => 'n'
29
+ }
30
+ return_value = values[question] || values[terminal]
31
+ question = ""
32
+ return_value
33
+ end
34
+ end
35
+
36
+ context :status do
37
+ subject { GitReflow.status(base_branch) }
38
+
39
+ before do
40
+ GitReflow.stub(:current_branch).and_return(feature_branch)
41
+ GitReflow.stub(:destination_branch).and_return(base_branch)
42
+
43
+ Github.stub(:new).and_return(github)
44
+ GitReflow.stub(:git_server).and_return(git_server)
45
+ end
46
+
47
+ context 'with no existing pull request' do
48
+ before { git_server.stub(:find_pull_request).with({from: feature_branch, to: base_branch}).and_return(nil) }
49
+ it { expect{ subject }.to have_output "\n[notice] No pull request exists for #{feature_branch} -> #{base_branch}" }
50
+ it { expect{ subject }.to have_output "[notice] Run 'git reflow review #{base_branch}' to start the review process" }
51
+ end
52
+
53
+ context 'with an existing pull request' do
54
+ before { git_server.stub(:find_pull_request).with({from: feature_branch, to: base_branch}).and_return(existing_pull_request) }
55
+
56
+ it 'displays a summary of the pull request and asks to open it in the browser' do
57
+ GitReflow.should_receive(:display_pull_request_summary).with(existing_pull_request)
58
+ GitReflow.should_receive(:ask_to_open_in_browser).with(existing_pull_request.html_url)
59
+ subject
60
+ $output.should include "Here's the status of your review:"
61
+ end
62
+ end
63
+ end
64
+
65
+ # Github Response specs thanks to:
66
+ # https://github.com/peter-murach/github/blob/master/spec/github/pull_requests_spec.rb
67
+ context :review do
68
+ let(:branch) { 'new-feature' }
69
+ let(:inputs) {
70
+ {
71
+ "title" => "Amazing new feature",
72
+ "body" => "Please pull this in!",
73
+ "head" => "reenhanced:new-feature",
74
+ "base" => "master",
75
+ "state" => "open"
76
+ }
77
+ }
78
+
79
+ let(:github) do
80
+ stub_github_with({
81
+ :user => user,
82
+ :password => password,
83
+ :repo => repo,
84
+ :branch => branch,
85
+ :pull => inputs
86
+ })
87
+ end
88
+
89
+ subject { GitReflow.review inputs }
90
+
91
+ it "fetches the latest changes to the destination branch" do
92
+ GitReflow.should_receive(:fetch_destination).with(inputs['base'])
93
+ github.should_receive(:find_pull_request).and_return(nil)
94
+ github.stub(:create_pull_request).and_return(existing_pull_request)
95
+ subject
96
+ end
97
+
98
+ it "pushes the latest current branch to the origin repo" do
99
+ GitReflow.should_receive(:push_current_branch)
100
+ github.should_receive(:find_pull_request).and_return(nil)
101
+ github.stub(:create_pull_request).and_return(existing_pull_request)
102
+ subject
103
+ end
104
+
105
+ context "pull request doesn't exist" do
106
+ before { github.stub(:find_pull_request).and_return(nil) }
107
+
108
+ it "successfully creates a pull request if I do not provide one" do
109
+ existing_pull_request.stub(:title).and_return(inputs['title'])
110
+ github.should_receive(:create_pull_request).with(inputs.except('state').symbolize_keys).and_return(existing_pull_request)
111
+ expect { subject }.to have_output "Successfully created pull request #1: #{inputs['title']}\nPull Request URL: https://github.com/#{user}/#{repo}/pulls/1\n"
112
+ end
113
+ end
114
+
115
+ context "pull request exists" do
116
+ before do
117
+ GitReflow.stub(:push_current_branch)
118
+ github_error = Github::Error::UnprocessableEntity.new( eval(fixture('pull_requests/pull_request_exists_error.json').read) )
119
+ github.stub(:create_pull_request).with(inputs.except('state')).and_raise(github_error)
120
+ GitReflow.stub(:display_pull_request_summary).with(existing_pull_request)
121
+ end
122
+
123
+ subject { GitReflow.review inputs }
124
+
125
+ it "displays a pull request summary for the existing pull request" do
126
+ GitReflow.should_receive(:display_pull_request_summary).with(existing_pull_request)
127
+ subject
128
+ end
129
+
130
+ it "asks to open the pull request in the browser" do
131
+ GitReflow.should_receive(:ask_to_open_in_browser).with(existing_pull_request.html_url)
132
+ subject
133
+ end
134
+ end
135
+ end
136
+
137
+ context :deliver do
138
+ let(:branch) { 'new-feature' }
139
+ let(:inputs) { {} }
140
+ let!(:github) do
141
+ stub_github_with({
142
+ :user => user,
143
+ :password => password,
144
+ :repo => repo,
145
+ :branch => branch,
146
+ :pull => existing_pull_request
147
+ })
148
+ end
149
+
150
+
151
+ before do
152
+ module Kernel
153
+ def system(cmd)
154
+ "call #{cmd}"
155
+ end
156
+ end
157
+ end
158
+
159
+ subject { GitReflow.deliver inputs }
160
+
161
+ it "fetches the latest changes to the destination branch" do
162
+ GitReflow.should_receive(:fetch_destination).with('master')
163
+ subject
164
+ end
165
+
166
+ it "looks for a pull request matching the feature branch and destination branch" do
167
+ github.should_receive(:find_pull_request).with(from: branch, to: 'master')
168
+ subject
169
+ end
170
+
171
+ context "and pull request exists for the feature branch to the destination branch" do
172
+ before do
173
+ github.stub(:get_build_status).and_return(build_status)
174
+ github.stub(:has_pull_request_comments?).and_return(true)
175
+ github.stub(:find_authors_of_open_pull_request_comments).and_return([])
176
+ github.stub(:comment_authors_for_pull_request).and_return(['codenamev'])
177
+ end
178
+
179
+ context 'and build status is not "success"' do
180
+ let(:build_status) { Hashie::Mash.new({ state: 'failure', description: 'Build resulted in failed test(s)' }) }
181
+
182
+ before do
183
+ # just stubbing these in a locked state as the test is specific to this scenario
184
+ GitReflow.stub(:find_authors_of_open_pull_request_comments).and_return([])
185
+ GitReflow.stub(:has_pull_request_comments?).and_return(true)
186
+ end
187
+
188
+ it "halts delivery and notifies user of a failed build" do
189
+ expect { subject }.to have_output "[#{ 'deliver halted'.colorize(:red) }] #{build_status.description}: #{build_status.target_url}"
190
+ end
191
+ end
192
+
193
+ context 'and build status is nil' do
194
+ let(:build_status) { nil }
195
+ let(:inputs) {{ skip_lgtm: true }}
196
+
197
+ before do
198
+ # stubbing unrelated results so we can just test that it made it insdide the conditional block
199
+ GitReflow.stub(:find_authors_of_open_pull_request_comments).and_return([])
200
+ GitReflow.stub(:has_pull_request_comments?).and_return(true)
201
+ GitReflow.stub(:comment_authors_for_pull_request).and_return([])
202
+ GitReflow.stub(:update_destination).and_return(true)
203
+ GitReflow.stub(:merge_feature_branch).and_return(true)
204
+ GitReflow.stub(:append_to_squashed_commit_message).and_return(true)
205
+ end
206
+
207
+ it "ignores build status when not setup" do
208
+ expect { subject }.to have_output "Merge complete!"
209
+ end
210
+ end
211
+
212
+ context 'and build status is "success"' do
213
+ let(:build_status) { Hashie::Mash.new({ state: 'success' }) }
214
+
215
+ context 'and has comments' do
216
+ before do
217
+ GitReflow.stub(:has_pull_request_comments?).and_return(true)
218
+ GitReflow.stub(:find_authors_of_open_pull_request_comments).and_return([])
219
+ end
220
+
221
+ context 'but there is a LGTM' do
222
+ let(:lgtm_comment_authors) { ['nhance'] }
223
+ before { stub_with_fallback(github, :comment_authors_for_pull_request).with(existing_pull_request, with: GitReflow::LGTM).and_return(lgtm_comment_authors) }
224
+
225
+ it "includes the pull request body in the commit message" do
226
+ squash_message = "#{existing_pull_request.body}\nCloses ##{existing_pull_request.number}\n\nLGTM given by: @nhance\n"
227
+ GitReflow.should_receive(:append_to_squashed_commit_message).with(squash_message)
228
+ subject
229
+ end
230
+
231
+ context "and the pull request has no body" do
232
+ let(:first_commit_message) { "We'll do it live." }
233
+
234
+ before do
235
+ existing_pull_request[:body] = ''
236
+ github.stub(:find_pull_request).and_return(existing_pull_request)
237
+ GitReflow.stub(:get_first_commit_message).and_return(first_commit_message)
238
+ github.stub(:comment_authors_for_pull_request).and_return(lgtm_comment_authors)
239
+ end
240
+
241
+ it "includes the first commit message for the new branch in the commit message of the merge" do
242
+ squash_message = "#{first_commit_message}\nCloses ##{existing_pull_request.number}\n\nLGTM given by: @nhance\n"
243
+ GitReflow.should_receive(:append_to_squashed_commit_message).with(squash_message)
244
+ subject
245
+ end
246
+ end
247
+
248
+ it "notifies user of the merge and performs it" do
249
+ GitReflow.should_receive(:merge_feature_branch).with('new-feature', {
250
+ destination_branch: 'master',
251
+ pull_request_number: existing_pull_request.number,
252
+ lgtm_authors: ['nhance'],
253
+ message: existing_pull_request.body
254
+ })
255
+
256
+ expect { subject }.to have_output "Merging pull request ##{existing_pull_request.number}: '#{existing_pull_request.title}', from '#{existing_pull_request.head.label}' into '#{existing_pull_request.base.label}'"
257
+ end
258
+
259
+ it "updates the destination brnach" do
260
+ GitReflow.should_receive(:update_destination).with('master')
261
+ subject
262
+ end
263
+
264
+ it "commits the changes for the squash merge" do
265
+ subject
266
+ $output.should include 'Merge complete!'
267
+ end
268
+
269
+ context "and cleaning up feature branch" do
270
+ before do
271
+ HighLine.any_instance.stub(:ask) do |terminal, question|
272
+ values = {
273
+ "Please enter your GitHub username: " => user,
274
+ "Please enter your GitHub password (we do NOT store this): " => password,
275
+ "Please enter your Enterprise site URL (e.g. https://github.company.com):" => enterprise_site,
276
+ "Please enter your Enterprise API endpoint (e.g. https://github.company.com/api/v3):" => enterprise_api,
277
+ "Would you like to push this branch to your remote repo and cleanup your feature branch? " => 'yes',
278
+ "Would you like to open it in your browser?" => 'no'
279
+ }
280
+ return_value = values[question] || values[terminal]
281
+ question = ""
282
+ return_value
283
+ end
284
+ end
285
+
286
+ it "pushes local squash merged base branch to remote repo" do
287
+ expect { subject }.to have_run_command("git push origin master")
288
+ end
289
+
290
+ it "deletes the remote feature branch" do
291
+ expect { subject }.to have_run_command("git push origin :new-feature")
292
+ end
293
+
294
+ it "deletes the local feature branch" do
295
+ expect { subject }.to have_run_command("git branch -D new-feature")
296
+ end
297
+ end
298
+
299
+ context "and not cleaning up feature branch" do
300
+ before do
301
+ HighLine.any_instance.stub(:ask) do |terminal, question|
302
+ values = {
303
+ "Please enter your GitHub username: " => user,
304
+ "Please enter your GitHub password (we do NOT store this): " => password,
305
+ "Please enter your Enterprise site URL (e.g. https://github.company.com):" => enterprise_site,
306
+ "Please enter your Enterprise API endpoint (e.g. https://github.company.com/api/v3):" => enterprise_api,
307
+ "Would you like to push this branch to your remote repo and cleanup your feature branch? " => 'no',
308
+ "Would you like to open it in your browser?" => 'no'
309
+ }
310
+ return_value = values[question] || values[terminal]
311
+ question = ""
312
+ return_value
313
+ end
314
+ end
315
+
316
+ it "doesn't update the remote repo with the new squash merge" do
317
+ expect { subject }.to_not have_run_command('git push origin master')
318
+ end
319
+
320
+ it "doesn't delete the feature branch on the remote repo" do
321
+ expect { subject }.to_not have_run_command('git push origin :new-feature')
322
+ end
323
+
324
+ it "doesn't delete the local feature branch" do
325
+ expect { subject }.to_not have_run_command('git branch -D new-feature')
326
+ end
327
+
328
+ it "provides instructions to undo the steps taken" do
329
+ expect { subject }.to have_output("To reset and go back to your branch run \`git reset --hard origin/master && git checkout new-feature\`")
330
+ end
331
+ end
332
+
333
+ context "and there were issues commiting the squash merge to the base branch" do
334
+ before { stub_with_fallback(GitReflow, :run_command_with_label).with('git commit', {with_system: true}).and_return false }
335
+ it "notifies user of issues commiting the squash merge of the feature branch" do
336
+ expect { subject }.to have_output("There were problems commiting your feature... please check the errors above and try again.")
337
+ end
338
+ end
339
+
340
+ end
341
+
342
+ context 'but there are still unaddressed comments' do
343
+ let(:open_comment_authors) { ['nhance', 'codenamev'] }
344
+ before { github.stub(:find_authors_of_open_pull_request_comments).and_return(open_comment_authors) }
345
+ it "notifies the user to get their code reviewed" do
346
+ expect { subject }.to have_output "[deliver halted] You still need a LGTM from: #{open_comment_authors.join(', ')}"
347
+ end
348
+ end
349
+ end
350
+
351
+ context 'but has no comments' do
352
+ before do
353
+ github.stub(:has_pull_request_comments?).and_return(false)
354
+ github.stub(:find_authors_of_open_pull_request_comments).and_return([])
355
+ end
356
+
357
+ it "notifies the user to get their code reviewed" do
358
+ expect { subject }.to have_output "[deliver halted] Your code has not been reviewed yet."
359
+ end
360
+ end
361
+
362
+ it "successfully finds a pull request for the current feature branch" do
363
+ expect { subject }.to have_output "Merging pull request #1: 'new-feature', from 'new-feature' into 'master'"
364
+ end
365
+
366
+ it "checks out the destination branch and updates any remote changes" do
367
+ GitReflow.should_receive(:update_destination)
368
+ subject
369
+ end
370
+
371
+ it "merges and squashes the feature branch into the master branch" do
372
+ GitReflow.should_receive(:merge_feature_branch)
373
+ subject
374
+ end
375
+ end
376
+ end
377
+
378
+ context "and no pull request exists for the feature branch to the destination branch" do
379
+ before { github.stub(:find_pull_request).and_return(nil) }
380
+
381
+ it "notifies the user of a missing pull request" do
382
+ expect { subject }.to have_output "Error: No pull request exists for #{user}:#{branch}\nPlease submit your branch for review first with \`git reflow review\`"
383
+ end
384
+ end
385
+ end
386
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe GitReflow::Config do
4
+ describe ".get(key)" do
5
+ subject { GitReflow::Config.get('chucknorris.roundhouse') }
6
+ it { expect{ subject }.to have_run_command_silently 'git config --get chucknorris.roundhouse' }
7
+ end
8
+
9
+ describe ".set(key)" do
10
+ subject { GitReflow::Config.set('chucknorris.roundhouse', 'to the face') }
11
+ it { expect{ subject }.to have_run_command_silently 'git config --global --replace-all chucknorris.roundhouse "to the face"' }
12
+
13
+ context "for current project only" do
14
+ subject { GitReflow::Config.set('chucknorris.roundhouse', 'to the face', local: true) }
15
+ it { expect{ subject }.to have_run_command_silently 'git config --replace-all chucknorris.roundhouse "to the face"' }
16
+ end
17
+ end
18
+ end