git_reflow 0.3.5 → 0.4.0

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,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