octopolo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +21 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +3 -0
  7. data/Guardfile +5 -0
  8. data/MIT-LICENSE +20 -0
  9. data/README.markdown +55 -0
  10. data/Rakefile +38 -0
  11. data/bash_completion.sh +13 -0
  12. data/bin/octopolo +21 -0
  13. data/bin/op +21 -0
  14. data/lib/octopolo.rb +15 -0
  15. data/lib/octopolo/changelog.rb +27 -0
  16. data/lib/octopolo/cli.rb +210 -0
  17. data/lib/octopolo/commands/accept_pull.rb +8 -0
  18. data/lib/octopolo/commands/compare_release.rb +9 -0
  19. data/lib/octopolo/commands/deployable.rb +8 -0
  20. data/lib/octopolo/commands/github_auth.rb +5 -0
  21. data/lib/octopolo/commands/new_branch.rb +9 -0
  22. data/lib/octopolo/commands/new_deployable.rb +8 -0
  23. data/lib/octopolo/commands/new_staging.rb +8 -0
  24. data/lib/octopolo/commands/octopolo_setup.rb +5 -0
  25. data/lib/octopolo/commands/pivotal_auth.rb +5 -0
  26. data/lib/octopolo/commands/pull_request.rb +13 -0
  27. data/lib/octopolo/commands/signoff.rb +10 -0
  28. data/lib/octopolo/commands/stage_up.rb +8 -0
  29. data/lib/octopolo/commands/stale_branches.rb +11 -0
  30. data/lib/octopolo/commands/sync_branch.rb +11 -0
  31. data/lib/octopolo/commands/tag_release.rb +13 -0
  32. data/lib/octopolo/config.rb +146 -0
  33. data/lib/octopolo/convenience_wrappers.rb +46 -0
  34. data/lib/octopolo/dated_branch_creator.rb +81 -0
  35. data/lib/octopolo/git.rb +262 -0
  36. data/lib/octopolo/github.rb +95 -0
  37. data/lib/octopolo/github/commit.rb +45 -0
  38. data/lib/octopolo/github/pull_request.rb +126 -0
  39. data/lib/octopolo/github/pull_request_creator.rb +127 -0
  40. data/lib/octopolo/github/user.rb +40 -0
  41. data/lib/octopolo/jira/story_commenter.rb +26 -0
  42. data/lib/octopolo/pivotal.rb +44 -0
  43. data/lib/octopolo/pivotal/story_commenter.rb +19 -0
  44. data/lib/octopolo/pull_request_merger.rb +99 -0
  45. data/lib/octopolo/renderer.rb +37 -0
  46. data/lib/octopolo/reports.rb +18 -0
  47. data/lib/octopolo/scripts.rb +23 -0
  48. data/lib/octopolo/scripts/accept_pull.rb +67 -0
  49. data/lib/octopolo/scripts/compare_release.rb +52 -0
  50. data/lib/octopolo/scripts/deployable.rb +27 -0
  51. data/lib/octopolo/scripts/github_auth.rb +87 -0
  52. data/lib/octopolo/scripts/new_branch.rb +34 -0
  53. data/lib/octopolo/scripts/new_deployable.rb +14 -0
  54. data/lib/octopolo/scripts/new_staging.rb +15 -0
  55. data/lib/octopolo/scripts/octopolo_setup.rb +55 -0
  56. data/lib/octopolo/scripts/pivotal_auth.rb +44 -0
  57. data/lib/octopolo/scripts/pull_request.rb +127 -0
  58. data/lib/octopolo/scripts/signoff.rb +85 -0
  59. data/lib/octopolo/scripts/stage_up.rb +26 -0
  60. data/lib/octopolo/scripts/stale_branches.rb +54 -0
  61. data/lib/octopolo/scripts/sync_branch.rb +37 -0
  62. data/lib/octopolo/scripts/tag_release.rb +70 -0
  63. data/lib/octopolo/templates/pull_request_body.erb +24 -0
  64. data/lib/octopolo/user_config.rb +112 -0
  65. data/lib/octopolo/version.rb +3 -0
  66. data/lib/octopolo/week.rb +130 -0
  67. data/octopolo.gemspec +31 -0
  68. data/spec/.DS_Store +0 -0
  69. data/spec/octopolo/cli_spec.rb +310 -0
  70. data/spec/octopolo/config_spec.rb +344 -0
  71. data/spec/octopolo/convenience_wrappers_spec.rb +80 -0
  72. data/spec/octopolo/dated_branch_creator_spec.rb +143 -0
  73. data/spec/octopolo/git_spec.rb +419 -0
  74. data/spec/octopolo/github/commit_spec.rb +59 -0
  75. data/spec/octopolo/github/pull_request_creator_spec.rb +174 -0
  76. data/spec/octopolo/github/pull_request_spec.rb +291 -0
  77. data/spec/octopolo/github/user_spec.rb +65 -0
  78. data/spec/octopolo/github_spec.rb +169 -0
  79. data/spec/octopolo/jira/stor_commenter_spec.rb +30 -0
  80. data/spec/octopolo/pivotal/story_commenter_spec.rb +34 -0
  81. data/spec/octopolo/pivotal_spec.rb +61 -0
  82. data/spec/octopolo/pull_request_merger_spec.rb +144 -0
  83. data/spec/octopolo/renderer_spec.rb +35 -0
  84. data/spec/octopolo/scripts/accept_pull_spec.rb +76 -0
  85. data/spec/octopolo/scripts/compare_release_spec.rb +115 -0
  86. data/spec/octopolo/scripts/deployable_spec.rb +52 -0
  87. data/spec/octopolo/scripts/github_auth_spec.rb +156 -0
  88. data/spec/octopolo/scripts/new_branch_spec.rb +41 -0
  89. data/spec/octopolo/scripts/new_deployable_spec.rb +18 -0
  90. data/spec/octopolo/scripts/new_staging_spec.rb +18 -0
  91. data/spec/octopolo/scripts/octopolo_setup_spec.rb +120 -0
  92. data/spec/octopolo/scripts/pivotal_auth_spec.rb +77 -0
  93. data/spec/octopolo/scripts/pull_request_spec.rb +217 -0
  94. data/spec/octopolo/scripts/signoff_spec.rb +139 -0
  95. data/spec/octopolo/scripts/stage_up_spec.rb +52 -0
  96. data/spec/octopolo/scripts/stale_branches_spec.rb +81 -0
  97. data/spec/octopolo/scripts/sync_branch_spec.rb +57 -0
  98. data/spec/octopolo/scripts/tag_release_spec.rb +108 -0
  99. data/spec/octopolo/user_config_spec.rb +167 -0
  100. data/spec/octopolo_spec.rb +7 -0
  101. data/spec/spec_helper.rb +29 -0
  102. data/spec/support/engine_yard.cache +0 -0
  103. data/spec/support/sample_octopolo.yml +2 -0
  104. data/spec/support/sample_user.yml +2 -0
  105. data/templates/lib.erb +23 -0
  106. data/templates/script.erb +7 -0
  107. data/templates/spec.erb +29 -0
  108. metadata +344 -0
@@ -0,0 +1,174 @@
1
+ require "spec_helper"
2
+ require_relative "../../../lib/octopolo/github/pull_request_creator"
3
+
4
+ module Octopolo
5
+ module GitHub
6
+ describe PullRequestCreator do
7
+ let(:creator) { PullRequestCreator.new repo_name, options }
8
+ let(:repo_name) { "foo/bar" }
9
+ let(:options) { {} }
10
+ let(:destination_branch) { "master" }
11
+ let(:source_branch) { "cool-feature" }
12
+ let(:title) { "title" }
13
+ let(:body) { "body" }
14
+ let(:pivotal_ids) { %w(123 456) }
15
+ let(:jira_ids) { %w(123 456) }
16
+ let(:jira_url) { "https://example-jira.com" }
17
+
18
+ context ".perform repo_name, options" do
19
+ let(:creator) { stub }
20
+
21
+ it "instantiates a creator and perfoms it" do
22
+ PullRequestCreator.should_receive(:new).with(repo_name, options) { creator }
23
+ creator.should_receive(:perform)
24
+ PullRequestCreator.perform(repo_name, options).should == creator
25
+ end
26
+ end
27
+
28
+ context ".new repo_name, options" do
29
+ it "remembers the repo name and options" do
30
+ creator = PullRequestCreator.new repo_name, options
31
+ creator.repo_name.should == repo_name
32
+ creator.options.should == options
33
+ end
34
+ end
35
+
36
+ context "#perform" do
37
+ let(:pull_request_data) { stub(:mash, number: 123) }
38
+
39
+ before do
40
+ creator.stub({
41
+ destination_branch: destination_branch,
42
+ source_branch: source_branch,
43
+ title: title,
44
+ body: body,
45
+ })
46
+ end
47
+
48
+ it "generates the pull request with the given details and retains the information" do
49
+ GitHub.should_receive(:create_pull_request).with(repo_name, destination_branch, source_branch, title, body) { pull_request_data }
50
+ creator.perform.should == pull_request_data
51
+ creator.number.should == pull_request_data.number
52
+ creator.pull_request_data.should == pull_request_data
53
+ end
54
+
55
+ it "raises CannotCreate if any exception occurs" do
56
+ GitHub.should_receive(:create_pull_request).and_raise(Octokit::UnprocessableEntity)
57
+ expect { creator.perform }.to raise_error(PullRequestCreator::CannotCreate)
58
+ end
59
+ end
60
+
61
+ context "#number" do
62
+ let(:number) { 123 }
63
+
64
+ it "returns the stored pull request number" do
65
+ creator.number = number
66
+ creator.number.should == number
67
+ end
68
+
69
+ it "raises an exception if no pull request has been created yet" do
70
+ creator.number = nil
71
+ expect { creator.number }.to raise_error(PullRequestCreator::NotYetCreated)
72
+ end
73
+ end
74
+
75
+ context "#pull_request_data" do
76
+ let(:details) { stub(:pull_request_data) }
77
+
78
+ it "returns the stored pull request details" do
79
+ creator.pull_request_data = details
80
+ creator.pull_request_data.should == details
81
+ end
82
+
83
+ it "raises an exception if no information has been captured yet" do
84
+ creator.pull_request_data = nil
85
+ expect { creator.pull_request_data }.to raise_error(PullRequestCreator::NotYetCreated)
86
+ end
87
+ end
88
+
89
+ context "#destination_branch" do
90
+ it "fetches from the options" do
91
+ creator.options[:destination_branch] = destination_branch
92
+ creator.destination_branch.should == destination_branch
93
+ end
94
+
95
+ it "raises an exception if it's missing" do
96
+ creator.options[:destination_branch] = nil
97
+ expect { creator.destination_branch }.to raise_error(PullRequestCreator::MissingAttribute)
98
+ end
99
+ end
100
+
101
+ context "#source_branch" do
102
+ it "fetches from the options" do
103
+ creator.options[:source_branch] = source_branch
104
+ creator.source_branch.should == source_branch
105
+ end
106
+
107
+ it "raises an exception if it's missing" do
108
+ creator.options[:source_branch] = nil
109
+ expect { creator.source_branch }.to raise_error(PullRequestCreator::MissingAttribute)
110
+ end
111
+ end
112
+
113
+ context "#title" do
114
+ context "having the option set" do
115
+ before { creator.options[:title] = title }
116
+
117
+ it "fetches from the options" do
118
+ creator.title.should == title
119
+ end
120
+ end
121
+
122
+ it "raises an exception if it's missing" do
123
+ creator.options[:title] = nil
124
+ expect { creator.title }.to raise_error(PullRequestCreator::MissingAttribute)
125
+ end
126
+ end
127
+
128
+ context "#pivotal_ids" do
129
+ it "fetches from the options" do
130
+ creator.options[:pivotal_ids] = pivotal_ids
131
+ creator.pivotal_ids.should == pivotal_ids
132
+ end
133
+
134
+ it "defaults to an empty array if it's missing" do
135
+ creator.options[:pivotal_ids] = nil
136
+ creator.pivotal_ids.should == []
137
+ end
138
+ end
139
+
140
+ context "#body_locals" do
141
+ let(:urls) { %w(link1 link2) }
142
+
143
+ before do
144
+ creator.stub({
145
+ pivotal_ids: pivotal_ids,
146
+ jira_ids: jira_ids,
147
+ jira_url: jira_url,
148
+ })
149
+ end
150
+ it "includes the necessary keys to render the template" do
151
+ creator.body_locals[:pivotal_ids].should == creator.pivotal_ids
152
+ creator.body_locals[:jira_ids].should == creator.jira_ids
153
+ creator.body_locals[:jira_url].should == creator.jira_url
154
+ end
155
+ end
156
+
157
+ context "#body" do
158
+ let(:locals) { stub(:hash) }
159
+ let(:output) { stub(:string) }
160
+
161
+ before do
162
+ creator.stub({
163
+ body_locals: locals,
164
+ })
165
+ end
166
+
167
+ it "renders the body template with the body locals" do
168
+ Renderer.should_receive(:render).with(Renderer::PULL_REQUEST_BODY, locals) { output }
169
+ creator.body.should == output
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,291 @@
1
+ require "spec_helper"
2
+ require_relative "../../../lib/octopolo/github/pull_request"
3
+
4
+ module Octopolo
5
+ module GitHub
6
+ describe PullRequest do
7
+ let(:repo_name) { "account/repo" }
8
+ let(:pr_number) { 7 }
9
+ let(:prhash) { stub }
10
+ let(:commits) { stub }
11
+ let(:comments) { stub }
12
+ let(:octo) { stub }
13
+
14
+ context ".new" do
15
+ it "remembers the pull request identifiers" do
16
+ pr = PullRequest.new repo_name, pr_number
17
+ pr.repo_name.should == repo_name
18
+ pr.number.should == pr_number
19
+ end
20
+
21
+ it "optionally accepts the github data" do
22
+ pr = PullRequest.new repo_name, pr_number, octo
23
+ pr.pull_request_data.should == octo
24
+ end
25
+
26
+ it "fails if not given a repo name" do
27
+ expect { PullRequest.new nil, pr_number }.to raise_error(PullRequest::MissingParameter)
28
+ end
29
+
30
+ it "fails if not given a pull request number" do
31
+ expect { PullRequest.new repo_name, nil }.to raise_error(PullRequest::MissingParameter)
32
+ end
33
+ end
34
+
35
+ context "#pull_request_data" do
36
+ let(:pull) { PullRequest.new repo_name, pr_number }
37
+
38
+ it "fetches the details from GitHub" do
39
+ GitHub.should_receive(:pull_request).with(pull.repo_name, pull.number) { octo }
40
+ pull.pull_request_data.should == octo
41
+ end
42
+
43
+ it "catches the information" do
44
+ GitHub.should_receive(:pull_request).once { octo }
45
+ pull.pull_request_data
46
+ pull.pull_request_data
47
+ end
48
+
49
+ it "fails if given invalid information" do
50
+ GitHub.should_receive(:pull_request).and_raise(Octokit::NotFound)
51
+ expect { pull.pull_request_data }.to raise_error(PullRequest::NotFound)
52
+ end
53
+ end
54
+
55
+ context "fetching its attributes from Octokit" do
56
+ let(:pull) { PullRequest.new repo_name, pr_number }
57
+
58
+ before do
59
+ pull.stub(pull_request_data: octo)
60
+ end
61
+
62
+ context "#title" do
63
+ let(:octo) { stub(title: "the title") }
64
+
65
+ it "retrieves from the github data" do
66
+ pull.title.should == octo.title
67
+ end
68
+ end
69
+
70
+ context "#branch" do
71
+ let(:octo) { stub(head: stub(ref: "asdf")) }
72
+
73
+ it "retrieves from the github data" do
74
+ pull.branch.should == octo.head.ref
75
+ end
76
+ end
77
+
78
+ context "#commits" do
79
+ it "fetches through octokit" do
80
+ Commit.should_receive(:for_pull_request).with(pull) { commits }
81
+ pull.commits.should == commits
82
+ end
83
+
84
+ it "caches the result" do
85
+ Commit.should_receive(:for_pull_request).once { commits }
86
+ pull.commits
87
+ pull.commits
88
+ end
89
+ end
90
+
91
+ context "#comments" do
92
+ it "fetches through octokit" do
93
+ GitHub.should_receive(:issue_comments).with(pull.repo_name, pull.number) { comments }
94
+ pull.comments.should == comments
95
+ end
96
+
97
+ it "caches the result" do
98
+ GitHub.should_receive(:issue_comments).once { comments }
99
+ pull.comments
100
+ pull.comments
101
+ end
102
+ end
103
+
104
+ context "#author_names" do
105
+ let(:commit1) { stub(author_name: "foo") }
106
+ let(:commit2) { stub(author_name: "bar") }
107
+
108
+ before do
109
+ pull.stub(commits: [commit1, commit2])
110
+ end
111
+
112
+ it "returns the list of authors" do
113
+ names = pull.author_names
114
+ names.should_not be_empty
115
+ names.count.should == 2
116
+ names.first.should == "foo"
117
+ end
118
+
119
+ it "returns only unique values" do
120
+ # make it same commenter
121
+ commit2.stub(author_name: commit1.author_name)
122
+ names = pull.author_names
123
+ names.size.should == 1
124
+ end
125
+ end
126
+
127
+ context "#commenter_names" do
128
+ let(:comment1) { stub(user: stub(login: "pbyrne")) }
129
+ let(:comment2) { stub(user: stub(login: "anfleene")) }
130
+
131
+ before do
132
+ pull.stub(comments: [comment1, comment2], author_names: [])
133
+ end
134
+
135
+ it "returns the names of the commit authors" do
136
+ GitHub.stub(:user).with("pbyrne").and_return(Hashie::Mash.new(:name => "pbyrne"))
137
+ GitHub.stub(:user).with("anfleene").and_return(Hashie::Mash.new(:name => "anfleene"))
138
+ names = pull.commenter_names
139
+ names.should_not be_empty
140
+ names.size.should == 2
141
+ names.first.should == "pbyrne"
142
+ end
143
+
144
+ it "returns only unique values" do
145
+ # make it same commenter
146
+ comment2.user.stub(login: comment1.user.login)
147
+ names = pull.commenter_names
148
+ names.size.should == 1
149
+ end
150
+
151
+ it "does not include authors in this list" do
152
+ pull.stub(author_names: [comment1.user.login])
153
+ names = pull.commenter_names
154
+ names.size.should == 1
155
+ names.should_not include comment1.user.login
156
+ end
157
+ end
158
+
159
+ context "#without_octopolo_users" do
160
+ let(:users) { ["anfleene", "tst-octopolo"] }
161
+
162
+ it "excludes the github octopolo users" do
163
+ pull.exlude_octopolo_user(users).should_not include("tst-octopolo")
164
+ pull.exlude_octopolo_user(users).should include("anfleene")
165
+ end
166
+ end
167
+
168
+ context "#url" do
169
+ let(:octo) { stub(html_url: "http://example.com") }
170
+
171
+ it "retrieves from the github data" do
172
+ pull.url.should == octo.html_url
173
+ end
174
+ end
175
+
176
+ context "#external_urls" do
177
+ # nicked from https://github.com/tstmedia/ngin/pull/1151
178
+ let(:body) do
179
+ <<-END
180
+ http://thedesk.tstmedia.com/admin.php?pg=request&reqid=44690 - verified
181
+ http://thedesk.tstmedia.com/admin.php?pg=request&reqid=44693 - verified
182
+
183
+ development_ftp_server: ftp.tstmedia.com
184
+ development_username: startribuneftptest@ftp.tstmedia.com
185
+ development_password: JUm1kU7STYt0
186
+ http://www.ngin.com.stage.ngin-staging.com/api/volleyball/stats/summaries?id=68382&gender=girls&tst_test=1&date=8/24/2012
187
+ END
188
+ end
189
+
190
+ before do
191
+ pull.stub(body: body)
192
+ end
193
+
194
+ it "parses from the body" do
195
+ urls = pull.external_urls
196
+ urls.size.should == 3
197
+ urls.should include "http://thedesk.tstmedia.com/admin.php?pg=request&reqid=44690"
198
+ urls.should include "http://thedesk.tstmedia.com/admin.php?pg=request&reqid=44693"
199
+ urls.should include "http://www.ngin.com.stage.ngin-staging.com/api/volleyball/stats/summaries?id=68382&gender=girls&tst_test=1&date=8/24/2012"
200
+ end
201
+ end
202
+
203
+ context "#body" do
204
+ let(:octo) { stub(body: "asdf") }
205
+
206
+ it "retrieves from the github data" do
207
+ pull.body.should == octo.body
208
+ end
209
+
210
+ it "returns an empty string if the GitHub data has no body" do
211
+ octo.stub(body: nil)
212
+ pull.body.should == ""
213
+ end
214
+ end
215
+
216
+ context "#mergeable?" do
217
+ it "retrieves from the github data" do
218
+ octo.stub(mergeable: true)
219
+ pull.should be_mergeable
220
+ octo.stub(mergeable: false)
221
+ pull.should_not be_mergeable
222
+ end
223
+ end
224
+
225
+ context "#week" do
226
+ it "retrieves from the github data" do
227
+ octo.stub(closed_at: "2012-09-18T14:00:01Z")
228
+ pull.week.should == Week.parse(octo.closed_at)
229
+ end
230
+ end
231
+ end
232
+
233
+ context "#human_app_name" do
234
+ let(:repo_name) { "account_name/repo_name" }
235
+ let(:pull) { PullRequest.new repo_name, pr_number }
236
+
237
+ it "infers from the repo_name" do
238
+ pull.repo_name = "account/foo"
239
+ pull.human_app_name.should == "Foo"
240
+ pull.repo_name = "account/foo_bar"
241
+ pull.human_app_name.should == "Foo Bar"
242
+ end
243
+ end
244
+
245
+ context ".closed repo_name" do
246
+ let(:raw_pr) { stub(:octo_data, number: 123) }
247
+ let(:pr_wrapper) { stub(:pull_request) }
248
+
249
+ it "crawls the repo for pull requests and wraps them in PullRequests" do
250
+ GitHub.should_receive(:pull_requests).with(repo_name, "closed") { [raw_pr] }
251
+ PullRequest.should_receive(:new).with(repo_name, raw_pr.number, raw_pr) { pr_wrapper }
252
+
253
+ result = PullRequest.closed(repo_name)
254
+ result.should == [pr_wrapper]
255
+ end
256
+ end
257
+
258
+ context "#write_comment(message)" do
259
+ let(:pull) { PullRequest.new repo_name, pr_number }
260
+ let(:message) { "Test message" }
261
+ let(:error) { Octokit::UnprocessableEntity.new }
262
+
263
+ it "creates the message through octokit" do
264
+ GitHub.should_receive(:add_comment).with(pull.repo_name, pull.number, ":octocat: #{message}")
265
+
266
+ pull.write_comment message
267
+ end
268
+
269
+ it "raises CommentFailed if an exception occurs" do
270
+ GitHub.should_receive(:add_comment).and_raise(error)
271
+
272
+ expect { pull.write_comment message }.to raise_error(PullRequest::CommentFailed, "Unable to write the comment: '#{error.message}'")
273
+ end
274
+ end
275
+
276
+ context ".create repo_name, options" do
277
+ let(:options) { stub(:hash) }
278
+ let(:number) { stub(:integer) }
279
+ let(:pull_request_data) { stub(:pull_request_data)}
280
+ let(:creator) { stub(:pull_request_creator, number: number, pull_request_data: pull_request_data)}
281
+ let(:pull_request) { stub(:pull_request) }
282
+
283
+ it "passes on to PullRequestCreator and returns a new PullRequest" do
284
+ PullRequestCreator.should_receive(:perform).with(repo_name, options) { creator }
285
+ PullRequest.should_receive(:new).with(repo_name, number, pull_request_data) { pull_request }
286
+ PullRequest.create(repo_name, options).should == pull_request
287
+ end
288
+ end
289
+ end
290
+ end
291
+ end