prophet 1.5.4

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.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (C) 2011 Dominik Bamberger bamboo@suse.com
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
20
+
data/lib/prophet.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'octokit'
2
+ require 'logger'
3
+ require 'prophet/railtie' if defined?(Rails)
4
+ require 'prophet/pull_request'
5
+ require 'prophet/prophet'
@@ -0,0 +1,310 @@
1
+ class Prophet
2
+
3
+ attr_accessor :username,
4
+ :password,
5
+ :username_fail,
6
+ :password_fail,
7
+ :rerun_on_source_change,
8
+ :rerun_on_target_change,
9
+ :prepare_block,
10
+ :exec_block,
11
+ :logger,
12
+ :success,
13
+ :status_pending,
14
+ :status_failure,
15
+ :status_success,
16
+ :comment_failure,
17
+ :comment_success,
18
+ :reuse_comments
19
+
20
+ # Allow configuration blocks being passed to Prophet.
21
+ # See the README.md for examples on how to call this method.
22
+ def self.setup
23
+ yield main_instance
24
+ end
25
+
26
+ def preparation(&block)
27
+ self.prepare_block = block
28
+ end
29
+
30
+ def execution(&block)
31
+ self.exec_block = block
32
+ end
33
+
34
+ # The main Prophet task. Call this to run your code.
35
+ def self.run
36
+ main_instance.run
37
+ end
38
+
39
+ def run
40
+ # Populate variables and setup environment.
41
+ configure
42
+ begin
43
+ self.prepare_block.call
44
+ rescue Exception => e
45
+ @log.error "Preparation block raised an exception: #{e}"
46
+ end
47
+ # Loop through all 'open' pull requests.
48
+ selected_requests = pull_requests.select do |request|
49
+ @request = request
50
+ # Jump to next iteration if source and/or target didn't change since last run.
51
+ next unless run_necessary?
52
+ set_status_on_github
53
+ remove_comment unless self.reuse_comments
54
+ true
55
+ end
56
+ # Run code on all selected requests.
57
+ selected_requests.each do |request|
58
+ @request = request
59
+ @log.info "Running for request ##{@request.id}."
60
+ # GitHub always creates a merge commit for its 'Merge Button'.
61
+ # Prophet reuses that commit to run the code on it.
62
+ switch_branch_to_merged_state
63
+ # Run specified code (i.e. tests) for the project.
64
+ begin
65
+ self.exec_block.call
66
+ # Unless self.success has already been set manually,
67
+ # the success/failure is determined by the last command's return code.
68
+ self.success ||= ($? && $?.exitstatus == 0)
69
+ rescue Exception => e
70
+ @log.error "Execution block raised an exception: #{e}"
71
+ self.success = false
72
+ end
73
+ switch_branch_back
74
+ comment_on_github
75
+ set_status_on_github
76
+ self.success = nil
77
+ end
78
+ end
79
+
80
+
81
+ private
82
+
83
+ # Remember the one instance we setup in our application and want to run.
84
+ def self.main_instance
85
+ @main_instance ||= Prophet.new
86
+ end
87
+
88
+ def configure
89
+ # Use existing logger or fall back to a new one with standard log level.
90
+ if self.logger
91
+ @log = self.logger
92
+ else
93
+ @log = Logger.new(STDOUT)
94
+ @log.level = Logger::INFO
95
+ end
96
+ # Set default fall back values for options that aren't set.
97
+ self.username ||= git_config['github.login']
98
+ self.password ||= git_config['github.password']
99
+ self.username_fail ||= self.username
100
+ self.password_fail ||= self.password
101
+ self.rerun_on_source_change = true if self.rerun_on_source_change.nil?
102
+ self.rerun_on_target_change = true if self.rerun_on_target_change.nil?
103
+ self.reuse_comments = true if self.reuse_comments.nil?
104
+ # Allow for custom messages.
105
+ self.status_pending ||= 'Prophet is still running.'
106
+ self.status_failure ||= 'Prophet reports failure.'
107
+ self.status_success ||= 'Prophet reports success.'
108
+ self.comment_failure ||= 'Prophet reports failure.'
109
+ self.comment_success ||= 'Prophet reports success.'
110
+ # Find environment (tasks, project, ...).
111
+ self.prepare_block ||= lambda {}
112
+ self.exec_block ||= lambda { `rake` }
113
+ @github = connect_to_github
114
+ @github_fail = if self.username == self.username_fail
115
+ @github
116
+ else
117
+ connect_to_github self.username_fail, self.password_fail
118
+ end
119
+ end
120
+
121
+ def connect_to_github(user = self.username, pass = self.password)
122
+ github = Octokit::Client.new(
123
+ :login => user,
124
+ :password => pass
125
+ )
126
+ # Check user login to GitHub.
127
+ github.login
128
+ @log.info "Successfully logged into GitHub (API v#{github.api_version}) with user '#{user}'."
129
+ # Ensure the user has access to desired project.
130
+ @project ||= /:(.*)\.git/.match(git_config['remote.origin.url'])[1]
131
+ begin
132
+ github.repo @project
133
+ @log.info "Successfully accessed GitHub project '#{@project}'"
134
+ github
135
+ rescue Octokit::Unauthorized => e
136
+ @log.error "Unable to access GitHub project with user '#{user}':\n#{e.message}"
137
+ abort
138
+ end
139
+ end
140
+
141
+ def pull_requests
142
+ request_list = @github.pulls @project, 'open'
143
+ requests = request_list.collect do |request|
144
+ PullRequest.new(@github.pull_request @project, request.number)
145
+ end
146
+ @log.info "Found #{requests.size > 0 ? requests.size : 'no'} open pull requests in '#{@project}'."
147
+ requests
148
+ end
149
+
150
+ # (Re-)runs are necessary if:
151
+ # - the pull request hasn't been used for a run before.
152
+ # - the pull request has been updated since the last run.
153
+ # - the target (i.e. master) has been updated since the last run.
154
+ def run_necessary?
155
+ @log.info "Checking pull request ##{@request.id}: #{@request.content.title}"
156
+ # Compare current sha ids of target and source branch with those from the last test run.
157
+ @request.target_head_sha = @github.commits(@project).first.sha
158
+ comments = @github.issue_comments(@project, @request.id)
159
+ comments = comments.select { |c| [username, username_fail].include?(c.user.login) }.reverse
160
+ # Initialize shas to ensure it will live on after the 'each' block.
161
+ shas = nil
162
+ @request.comment = nil
163
+ comments.each do |comment|
164
+ shas = /Merged ([\w]+) into ([\w]+)/.match(comment.body)
165
+ if shas && shas[1] && shas[2]
166
+ # Remember comment to be able to update or delete it later.
167
+ @request.comment = comment
168
+ break
169
+ end
170
+ end
171
+ # If it's not mergeable, we need to delete all comments of former test runs.
172
+ unless @request.content.mergeable
173
+ # Sometimes GitHub doesn't have a proper boolean value stored.
174
+ if @request.content.mergeable.nil? && switch_branch_to_merged_state(false)
175
+ # Pull request is mergeable after all.
176
+ switch_branch_back
177
+ else
178
+ @log.info 'Pull request not auto-mergeable. Not running.'
179
+ if @request.comment
180
+ @log.info 'Deleting existing comment.'
181
+ call_github(old_comment_success?).delete_comment(@project, @request.comment.id)
182
+ end
183
+ return false
184
+ end
185
+ end
186
+ if @request.comment
187
+ @log.info "Current target sha: '#{@request.target_head_sha}', pull sha: '#{@request.head_sha}'."
188
+ @log.info "Last test run target sha: '#{shas[2]}', pull sha: '#{shas[1]}'."
189
+ if self.rerun_on_source_change && (shas[1] != @request.head_sha)
190
+ @log.info 'Re-running due to new commit in pull request.'
191
+ return true
192
+ elsif self.rerun_on_target_change && (shas[2] != @request.target_head_sha)
193
+ @log.info 'Re-running due to new commit in target branch.'
194
+ return true
195
+ end
196
+ else
197
+ # If there are no comments yet, it has to be a new request.
198
+ @log.info 'New pull request detected, run needed.'
199
+ return true
200
+ end
201
+ @log.info "Not running for request ##{@request.id}."
202
+ false
203
+ end
204
+
205
+ def switch_branch_to_merged_state(hard = true)
206
+ # Fetch the merge-commit for the pull request.
207
+ # NOTE: This commit is automatically created by 'GitHub Merge Button'.
208
+ # FIXME: Use cheetah to pipe to @log.debug instead of that /dev/null hack.
209
+ `git fetch origin refs/pull/#{@request.id}/merge: &> /dev/null`
210
+ `git checkout FETCH_HEAD &> /dev/null`
211
+ unless ($? && $?.exitstatus == 0)
212
+ @log.error 'Unable to switch to merge branch.'
213
+ hard ? abort : false
214
+ end
215
+ true
216
+ end
217
+
218
+ def switch_branch_back
219
+ # FIXME: Use cheetah to pipe to @log.debug instead of that /dev/null hack.
220
+ @log.info 'Switching back to original branch.'
221
+ # FIXME: For branches other than master, remember the original branch.
222
+ `git checkout master &> /dev/null`
223
+ # Clean up potential remains and run garbage collector.
224
+ `git gc &> /dev/null`
225
+ end
226
+
227
+ def old_comment_success?
228
+ return unless @request.comment
229
+ # Analyze old comment to see whether it was a successful or a failing one.
230
+ @request.comment.body.include? '( Success: '
231
+ end
232
+
233
+ def remove_comment
234
+ if @request.comment
235
+ # Remove old comment and reset variable.
236
+ call_github(old_comment_success?).delete_comment(@project, @request.comment.id)
237
+ @request.comment = nil
238
+ end
239
+ end
240
+
241
+ def comment_on_github
242
+ # Determine comment message.
243
+ message = if self.success
244
+ @log.info 'Successful run.'
245
+ self.comment_success + "\n( Success: "
246
+ else
247
+ @log.info 'Failing run.'
248
+ self.comment_failure + "\n( Failure: "
249
+ end
250
+ message += "Merged #{@request.head_sha} into #{@request.target_head_sha} )"
251
+ if self.reuse_comments && old_comment_success? == self.success
252
+ # Replace existing comment's body with the correct connection.
253
+ @log.info "Updating existing #{notion(self.success)} comment."
254
+ call_github(self.success).update_comment(@project, @request.comment.id, message)
255
+ else
256
+ if @request.comment
257
+ @log.info "Deleting existing #{notion(!self.success)} comment."
258
+ # Delete old comment with correct connection (if @request.comment exists).
259
+ call_github(!self.success).delete_comment(@project, @request.comment.id)
260
+ end
261
+ # Create new comment with correct connection.
262
+ @log.info "Adding new #{notion(self.success)} comment."
263
+ call_github(self.success).add_comment(@project, @request.id, message)
264
+ end
265
+ end
266
+
267
+ def set_status_on_github
268
+ @log.info 'Updating status on GitHub.'
269
+ case self.success
270
+ when true
271
+ state_symbol = :success
272
+ state_message = self.status_success
273
+ when false
274
+ state_symbol = :failure
275
+ state_message = self.status_failure
276
+ else
277
+ state_symbol = :pending
278
+ state_message = self.status_pending
279
+ end
280
+ @github.post(
281
+ "repos/#{@project}/statuses/#{@request.head_sha}", {
282
+ :state => state_symbol,
283
+ :description => state_message
284
+ }
285
+ )
286
+ end
287
+
288
+ def notion(success)
289
+ success ? 'positive' : 'negative'
290
+ end
291
+
292
+ # Determine which connection to GitHub should be used for the call.
293
+ def call_github(use_default_user = true)
294
+ use_default_user ? @github : @github_fail
295
+ end
296
+
297
+ # Collect git config information in a Hash for easy access.
298
+ # Checks '~/.gitconfig' for credentials.
299
+ def git_config
300
+ unless @git_config
301
+ @git_config = {}
302
+ `git config --list`.split("\n").each do |line|
303
+ key, value = line.split('=')
304
+ @git_config[key] = value
305
+ end
306
+ end
307
+ @git_config
308
+ end
309
+
310
+ end
@@ -0,0 +1,15 @@
1
+ class PullRequest
2
+
3
+ attr_accessor :id,
4
+ :content,
5
+ :comment,
6
+ :head_sha,
7
+ :target_head_sha
8
+
9
+ def initialize(content)
10
+ @content = content
11
+ @id = content.number
12
+ @head_sha = content.head.sha
13
+ end
14
+
15
+ end
@@ -0,0 +1,8 @@
1
+ require 'prophet'
2
+ require 'rails'
3
+
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load 'tasks/prophet.rake.rake'
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ desc 'Test open pull requests'
2
+ task :prophet => :environment do
3
+ Prophet.run
4
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prophet
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.5.4
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dominik Bamberger
9
+ - Thomas Schmidt
10
+ - Jordi Massaguer Pla
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2013-04-11 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: octokit
18
+ requirement: !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ! '>='
22
+ - !ruby/object:Gem::Version
23
+ version: '0'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ none: false
28
+ requirements:
29
+ - - ! '>='
30
+ - !ruby/object:Gem::Version
31
+ version: '0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: rspec
34
+ requirement: !ruby/object:Gem::Requirement
35
+ none: false
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ description: Prophet runs custom code (i.e. your project's test suite) on open pull
49
+ requests on GitHub. Afterwards it posts the result as a comment to the respective
50
+ request. This should give you an outlook on the future state of your repository
51
+ in case you accept the request and merge the code.
52
+ email: bamboo@suse.com
53
+ executables: []
54
+ extensions: []
55
+ extra_rdoc_files: []
56
+ files:
57
+ - LICENSE
58
+ - lib/tasks/prophet.rake
59
+ - lib/prophet.rb
60
+ - lib/prophet/railtie.rb
61
+ - lib/prophet/pull_request.rb
62
+ - lib/prophet/prophet.rb
63
+ homepage: http://github.com/b4mboo/prophet
64
+ licenses: []
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 1.8.23
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: An easy way to loop through open pull requests and run code onthe merged
87
+ branch.
88
+ test_files: []