party_foul 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +5 -3
- data/lib/generators/party_foul/templates/party_foul.rb +3 -0
- data/lib/party_foul/exception_handler.rb +22 -0
- data/lib/party_foul/issue_renderer.rb +50 -2
- data/lib/party_foul/issue_renderers/rack.rb +6 -0
- data/lib/party_foul/issue_renderers/rails.rb +6 -0
- data/lib/party_foul/processors/sync.rb +4 -0
- data/lib/party_foul/version.rb +1 -1
- data/lib/party_foul.rb +42 -1
- data/test/party_foul/configure_test.rb +7 -0
- data/test/party_foul/exception_handler_test.rb +6 -0
- data/test/test_helper.rb +1 -1
- data/test/tmp/config/initializers/party_foul.rb +3 -0
- metadata +4 -4
data/README.md
CHANGED
@@ -87,6 +87,9 @@ PartyFoul.configure do |config|
|
|
87
87
|
|
88
88
|
# The repository for this application
|
89
89
|
config.repo = 'repo_name'
|
90
|
+
|
91
|
+
# The branch for your deployed code
|
92
|
+
# config.branch = 'master'
|
90
93
|
end
|
91
94
|
```
|
92
95
|
|
@@ -147,9 +150,8 @@ the change in one of the [different issue renderer adapters](https://github.com/
|
|
147
150
|
|
148
151
|
## Authors ##
|
149
152
|
|
150
|
-
[Brian Cardarella](http://twitter.com/bcardarella)
|
151
|
-
|
152
|
-
[Dan McClain](http://twitter.com/_danmcclain)
|
153
|
+
* [Brian Cardarella](http://twitter.com/bcardarella)
|
154
|
+
* [Dan McClain](http://twitter.com/_danmcclain)
|
153
155
|
|
154
156
|
[We are very thankful for the many contributors](https://github.com/dockyard/party_foul/graphs/contributors)
|
155
157
|
|
@@ -1,10 +1,19 @@
|
|
1
1
|
class PartyFoul::ExceptionHandler
|
2
2
|
attr_accessor :rendered_issue
|
3
3
|
|
4
|
+
# This handler will pass the exception and env from Rack off to a processor.
|
5
|
+
# The default PartyFoul processor will work synchronously. Processor adapters can be written
|
6
|
+
# to push this logic to a background job if desired.
|
7
|
+
#
|
8
|
+
# @param [Exception, Hash]
|
4
9
|
def self.handle(exception, env)
|
5
10
|
PartyFoul.processor.handle(exception, env)
|
6
11
|
end
|
7
12
|
|
13
|
+
# Makes an attempt to determine what framework is being used and will use the proper
|
14
|
+
# IssueRenderer.
|
15
|
+
#
|
16
|
+
# @param [Exception, Hash]
|
8
17
|
def initialize(exception, env)
|
9
18
|
renderer_klass = if defined?(Rails)
|
10
19
|
PartyFoul::RailsIssueRenderer
|
@@ -15,6 +24,8 @@ class PartyFoul::ExceptionHandler
|
|
15
24
|
self.rendered_issue = renderer_klass.new(exception, env)
|
16
25
|
end
|
17
26
|
|
27
|
+
# Begins to process the exception for Github Issues. Makes an attempt
|
28
|
+
# to find the issue. If found will update the issue. If not found will create a new issue.
|
18
29
|
def run
|
19
30
|
if issue = find_issue
|
20
31
|
update_issue(issue)
|
@@ -23,6 +34,7 @@ class PartyFoul::ExceptionHandler
|
|
23
34
|
end
|
24
35
|
end
|
25
36
|
|
37
|
+
# Hits the Github API to find the matching issue using the fingerprint.
|
26
38
|
def find_issue
|
27
39
|
unless issue = PartyFoul.github.search.issues(owner: PartyFoul.owner, repo: PartyFoul.repo, state: 'open', keyword: fingerprint).issues.first
|
28
40
|
issue = PartyFoul.github.search.issues(owner: PartyFoul.owner, repo: PartyFoul.repo, state: 'closed', keyword: fingerprint).issues.first
|
@@ -31,11 +43,16 @@ class PartyFoul::ExceptionHandler
|
|
31
43
|
issue
|
32
44
|
end
|
33
45
|
|
46
|
+
# Will create a new issue and a comment with the proper details. All issues are labeled as 'bug'.
|
34
47
|
def create_issue
|
48
|
+
self.sha = PartyFoul.github.git_data.references.get(PartyFoul.owner, PartyFoul.repo, "heads/#{PartyFoul.branch}").object.sha
|
35
49
|
issue = PartyFoul.github.issues.create(PartyFoul.owner, PartyFoul.repo, title: rendered_issue.title, body: rendered_issue.body, labels: ['bug'])
|
36
50
|
PartyFoul.github.issues.comments.create(PartyFoul.owner, PartyFoul.repo, issue['number'], body: rendered_issue.comment)
|
37
51
|
end
|
38
52
|
|
53
|
+
# Updates the given issue. If the issue is labeled as 'wontfix' nothing is done. If the issue is closed the issue is reopened and labeled as 'regression'.
|
54
|
+
#
|
55
|
+
# @param [Hashie::Mash]
|
39
56
|
def update_issue(issue)
|
40
57
|
unless issue.key?('labels') && issue['labels'].include?('wontfix')
|
41
58
|
params = {body: rendered_issue.update_body(issue['body']), state: 'open'}
|
@@ -44,6 +61,7 @@ class PartyFoul::ExceptionHandler
|
|
44
61
|
params[:labels] = ['bug', 'regression']
|
45
62
|
end
|
46
63
|
|
64
|
+
self.sha = PartyFoul.github.git_data.references.get(PartyFoul.owner, PartyFoul.repo, "heads/#{PartyFoul.branch}").object.sha
|
47
65
|
PartyFoul.github.issues.edit(PartyFoul.owner, PartyFoul.repo, issue['number'], params)
|
48
66
|
PartyFoul.github.issues.comments.create(PartyFoul.owner, PartyFoul.repo, issue['number'], body: rendered_issue.comment)
|
49
67
|
end
|
@@ -54,4 +72,8 @@ class PartyFoul::ExceptionHandler
|
|
54
72
|
def fingerprint
|
55
73
|
rendered_issue.fingerprint
|
56
74
|
end
|
75
|
+
|
76
|
+
def sha=(sha)
|
77
|
+
rendered_issue.sha = sha
|
78
|
+
end
|
57
79
|
end
|
@@ -1,37 +1,68 @@
|
|
1
1
|
class PartyFoul::IssueRenderer
|
2
|
-
attr_accessor :exception, :env
|
2
|
+
attr_accessor :exception, :env, :sha
|
3
3
|
|
4
|
+
# A new renderer instance for Githug issues
|
5
|
+
#
|
6
|
+
# @param [Exception, Hash]
|
4
7
|
def initialize(exception, env)
|
5
8
|
self.exception = exception
|
6
9
|
self.env = env
|
7
10
|
end
|
8
11
|
|
12
|
+
# Derived title of the issue. Must be implemented by the adapter class
|
13
|
+
#
|
14
|
+
# @return [NotImplementedError]
|
9
15
|
def title
|
10
16
|
raise NotImplementedError
|
11
17
|
end
|
12
18
|
|
19
|
+
# Will compile the template for an issue body as defined in
|
20
|
+
# {PartyFoul.issue_template}
|
21
|
+
#
|
22
|
+
# @return [String]
|
13
23
|
def body
|
14
24
|
compile_template(PartyFoul.issue_template)
|
15
25
|
end
|
16
26
|
|
27
|
+
# Will compile the template for a comment body as defined in
|
28
|
+
# {PartyFoul.comment_template}
|
17
29
|
def comment
|
18
30
|
compile_template(PartyFoul.comment_template)
|
19
31
|
end
|
20
32
|
|
33
|
+
# Compiles the stack trace for use in the issue body. Lines in the
|
34
|
+
# stack trace that are part of the application will be rendered as
|
35
|
+
# links to the relative file and line on Github based upon
|
36
|
+
# {PartyFoul.web_url}, {PartyFoul.owner}, {PartyFoul.repo}, and
|
37
|
+
# {PartyFoul.branch}. The branch will be used at the time the
|
38
|
+
# exception happens to grab the SHA for that branch at that time for
|
39
|
+
# the purpose of linking.
|
40
|
+
#
|
41
|
+
# @return [String]
|
21
42
|
def stack_trace
|
22
43
|
exception.backtrace.map do |line|
|
23
44
|
if matches = extract_file_name_and_line_number(line)
|
24
|
-
"<a href='#{PartyFoul.repo_url}/
|
45
|
+
"<a href='#{PartyFoul.repo_url}/blob/#{sha}/#{matches[2]}#L#{matches[3]}'>#{line}</a>"
|
25
46
|
else
|
26
47
|
line
|
27
48
|
end
|
28
49
|
end.join("\n")
|
29
50
|
end
|
30
51
|
|
52
|
+
# A SHA1 hex digested representation of the title. The fingerprint is
|
53
|
+
# used to create a unique value in the issue body. This value is used
|
54
|
+
# for seraching when matching issues happen again in the future.
|
55
|
+
#
|
56
|
+
# @return [String]
|
31
57
|
def fingerprint
|
32
58
|
Digest::SHA1.hexdigest(title)
|
33
59
|
end
|
34
60
|
|
61
|
+
# Will update the issue body. The count and the time stamp will both
|
62
|
+
# be updated. If the format of the issue body fails to match for
|
63
|
+
# whatever reason the issue body will be reset.
|
64
|
+
#
|
65
|
+
# @return [String]
|
35
66
|
def update_body(old_body)
|
36
67
|
begin
|
37
68
|
current_count = old_body.match(/<th>Count<\/th><td>(\d+)<\/td>/)[1].to_i
|
@@ -43,22 +74,39 @@ class PartyFoul::IssueRenderer
|
|
43
74
|
end
|
44
75
|
end
|
45
76
|
|
77
|
+
# The params hash at the time the exception occurred. This method is
|
78
|
+
# overriden for each framework adapter. It should return a hash.
|
79
|
+
#
|
80
|
+
# @return [NotImplementedError]
|
46
81
|
def params
|
47
82
|
raise NotImplementedError
|
48
83
|
end
|
49
84
|
|
85
|
+
# The timestamp when the exception occurred.
|
86
|
+
#
|
87
|
+
# @return [String]
|
50
88
|
def occurred_at
|
51
89
|
Time.now.strftime('%B %d, %Y %H:%M:%S %z')
|
52
90
|
end
|
53
91
|
|
92
|
+
# IP address of the client who triggered the exception
|
93
|
+
#
|
94
|
+
# @return [String]
|
54
95
|
def ip_address
|
55
96
|
env['REMOTE_ADDR']
|
56
97
|
end
|
57
98
|
|
99
|
+
# The session hash for the client at the time of the exception
|
100
|
+
#
|
101
|
+
# @return [Hash]
|
58
102
|
def session
|
59
103
|
env['rack.session']
|
60
104
|
end
|
61
105
|
|
106
|
+
# HTTP Headers hash from the request. Headers can be filtered out by
|
107
|
+
# adding matching key names to {PartyFoul.filtered_http_headers}
|
108
|
+
#
|
109
|
+
# @return [Hash]
|
62
110
|
def http_headers
|
63
111
|
env.keys.select { |key| key =~ /^HTTP_(\w+)/ && !(PartyFoul.filtered_http_headers || []).include?($1.split('_').map(&:capitalize).join('-')) }.sort.inject({}) do |hash, key|
|
64
112
|
hash[key.split('HTTP_').last.split('_').map(&:capitalize).join('-')] = env[key]
|
@@ -2,10 +2,16 @@ require 'party_foul/issue_renderer'
|
|
2
2
|
|
3
3
|
module PartyFoul
|
4
4
|
class RackIssueRenderer < IssueRenderer
|
5
|
+
# Rack params
|
6
|
+
#
|
7
|
+
# @return [Hash]
|
5
8
|
def params
|
6
9
|
env['QUERY_STRING']
|
7
10
|
end
|
8
11
|
|
12
|
+
# Title for the issue comprised of (exception) "message"
|
13
|
+
#
|
14
|
+
# @return [String]
|
9
15
|
def title
|
10
16
|
%{(#{exception.class}) "#{exception.message}"}
|
11
17
|
end
|
@@ -2,11 +2,17 @@ require 'party_foul/issue_renderer'
|
|
2
2
|
|
3
3
|
module PartyFoul
|
4
4
|
class RailsIssueRenderer < IssueRenderer
|
5
|
+
# Rails params hash. Filtered parms are respected.
|
6
|
+
#
|
7
|
+
# @return [Hash]
|
5
8
|
def params
|
6
9
|
parameter_filter = ActionDispatch::Http::ParameterFilter.new(env["action_dispatch.parameter_filter"])
|
7
10
|
parameter_filter.filter(env['action_dispatch.request.path_parameters'])
|
8
11
|
end
|
9
12
|
|
13
|
+
# Title for the issue comprised of Controller#action (exception) "message"
|
14
|
+
#
|
15
|
+
# @return [String]
|
10
16
|
def title
|
11
17
|
%{#{env['action_controller.instance'].class}##{env['action_dispatch.request.path_parameters']['action']} (#{exception.class}) "#{exception.message}"}
|
12
18
|
end
|
@@ -1,4 +1,8 @@
|
|
1
1
|
class PartyFoul::Processors::Sync
|
2
|
+
# Passes the exception and rack env data to the ExceptionHandler and
|
3
|
+
# runs everything synchronously.
|
4
|
+
#
|
5
|
+
# @param [Exception, Hash]
|
2
6
|
def self.handle(exception, env)
|
3
7
|
PartyFoul::ExceptionHandler.new(exception, env).run
|
4
8
|
end
|
data/lib/party_foul/version.rb
CHANGED
data/lib/party_foul.rb
CHANGED
@@ -4,13 +4,27 @@ module PartyFoul
|
|
4
4
|
class << self
|
5
5
|
attr_accessor :github, :oauth_token, :endpoint, :owner, :repo,
|
6
6
|
:ignored_exceptions, :processor, :issue_template,
|
7
|
-
:comment_template, :filtered_http_headers, :web_url
|
7
|
+
:comment_template, :filtered_http_headers, :web_url, :branch
|
8
8
|
end
|
9
9
|
|
10
|
+
# The git branch that is used for linking in the stack trace
|
11
|
+
#
|
12
|
+
# @return [String] Defaults to 'master' if not set
|
13
|
+
def self.branch
|
14
|
+
@branch ||= 'master'
|
15
|
+
end
|
16
|
+
|
17
|
+
# The web url for Github. This is only interesting for Enterprise
|
18
|
+
# users
|
19
|
+
#
|
20
|
+
# @return [String] Defaults to 'https://github.com' if not set
|
10
21
|
def self.web_url
|
11
22
|
@web_url ||= 'https://github.com'
|
12
23
|
end
|
13
24
|
|
25
|
+
# The template used for rendering the body of a new issue
|
26
|
+
#
|
27
|
+
# @return [String]
|
14
28
|
def self.issue_template
|
15
29
|
@issue_template ||=
|
16
30
|
<<-TEMPLATE
|
@@ -26,6 +40,9 @@ Fingerprint: `:fingerprint`
|
|
26
40
|
TEMPLATE
|
27
41
|
end
|
28
42
|
|
43
|
+
# The template used for rendering the body of a new comment
|
44
|
+
#
|
45
|
+
# @return [String]
|
29
46
|
def self.comment_template
|
30
47
|
@comment_template ||=
|
31
48
|
<<-TEMPLATE
|
@@ -39,14 +56,38 @@ Fingerprint: `:fingerprint`
|
|
39
56
|
TEMPLATE
|
40
57
|
end
|
41
58
|
|
59
|
+
# The collection of exceptions that should not be captured. Members of
|
60
|
+
# the collection must be string representations of the exception. For
|
61
|
+
# example:
|
62
|
+
#
|
63
|
+
# # This is good
|
64
|
+
# ['ActiveRecord::RecordNotFound']
|
65
|
+
#
|
66
|
+
# # This is not
|
67
|
+
# [ActiveRecord::RecordNotFound]
|
68
|
+
#
|
69
|
+
# @return [Array]
|
42
70
|
def self.ignored_exceptions
|
43
71
|
@ignored_exceptions || []
|
44
72
|
end
|
45
73
|
|
74
|
+
# The url of the repository. Built using the {.web_url}, {.owner}, and {.repo}
|
75
|
+
# values
|
76
|
+
#
|
77
|
+
# @return [String]
|
46
78
|
def self.repo_url
|
47
79
|
"#{web_url}/#{owner}/#{repo}"
|
48
80
|
end
|
49
81
|
|
82
|
+
# The configure block for PartyFoul. Use to initialize settings
|
83
|
+
#
|
84
|
+
# PartyFoul.configure do |config|
|
85
|
+
# config.owner 'dockyard'
|
86
|
+
# config.repo 'test_app'
|
87
|
+
# config.oauth_token = ENV['oauth_token']
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# @param [Block]
|
50
91
|
def self.configure(&block)
|
51
92
|
yield self
|
52
93
|
self.processor ||= PartyFoul::Processors::Sync
|
@@ -14,6 +14,7 @@ describe 'Party Foul Confg' do
|
|
14
14
|
config.endpoint = 'http://api.example.com'
|
15
15
|
config.owner = 'test_owner'
|
16
16
|
config.repo = 'test_repo'
|
17
|
+
config.branch = 'master'
|
17
18
|
end
|
18
19
|
|
19
20
|
PartyFoul.ignored_exceptions.must_equal ['StandardError']
|
@@ -23,5 +24,11 @@ describe 'Party Foul Confg' do
|
|
23
24
|
PartyFoul.owner.must_equal 'test_owner'
|
24
25
|
PartyFoul.repo.must_equal 'test_repo'
|
25
26
|
PartyFoul.repo_url.must_equal 'http://example.com/test_owner/test_repo'
|
27
|
+
PartyFoul.branch.must_equal 'master'
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'has default values' do
|
31
|
+
PartyFoul.web_url.must_equal 'https://github.com'
|
32
|
+
PartyFoul.branch.must_equal 'master'
|
26
33
|
end
|
27
34
|
end
|
@@ -8,8 +8,10 @@ describe 'Party Foul Exception Handler' do
|
|
8
8
|
config.repo = 'test_repo'
|
9
9
|
end
|
10
10
|
|
11
|
+
PartyFoul.stubs(:branch).returns('deploy')
|
11
12
|
PartyFoul.github.stubs(:issues).returns(mock('Issues'))
|
12
13
|
PartyFoul.github.stubs(:search).returns(mock('Search'))
|
14
|
+
PartyFoul.github.git_data.references.stubs(:get)
|
13
15
|
PartyFoul.github.issues.stubs(:create)
|
14
16
|
PartyFoul.github.issues.stubs(:edit)
|
15
17
|
PartyFoul.github.issues.stubs(:comments).returns(mock('Comments'))
|
@@ -25,6 +27,7 @@ describe 'Party Foul Exception Handler' do
|
|
25
27
|
PartyFoul.github.search.stubs(:issues).with(owner: 'test_owner', repo: 'test_repo', keyword: 'test_fingerprint', state: 'open').returns(Hashie::Mash.new(issues: []))
|
26
28
|
PartyFoul.github.search.stubs(:issues).with(owner: 'test_owner', repo: 'test_repo', keyword: 'test_fingerprint', state: 'closed').returns(Hashie::Mash.new(issues: []))
|
27
29
|
PartyFoul.github.issues.expects(:create).with('test_owner', 'test_repo', title: 'Test Title', body: 'Test Body', :labels => ['bug']).returns(Hashie::Mash.new('number' => 1))
|
30
|
+
PartyFoul.github.git_data.references.expects(:get).with('test_owner', 'test_repo', 'heads/deploy').returns(Hashie::Mash.new(object: Hashie::Mash.new(sha: 'abcdefg1234567890')))
|
28
31
|
PartyFoul.github.issues.comments.expects(:create).with('test_owner', 'test_repo', 1, body: 'Test Comment')
|
29
32
|
PartyFoul::ExceptionHandler.new(nil, {}).run
|
30
33
|
end
|
@@ -41,6 +44,7 @@ describe 'Party Foul Exception Handler' do
|
|
41
44
|
PartyFoul.github.search.stubs(:issues).with(owner: 'test_owner', repo: 'test_repo', keyword: 'test_fingerprint', state: 'open').returns(Hashie::Mash.new(issues: [{title: 'Test Title', body: 'Test Body', state: 'open', number: 1}]))
|
42
45
|
PartyFoul.github.issues.expects(:edit).with('test_owner', 'test_repo', 1, body: 'New Body', state: 'open')
|
43
46
|
PartyFoul.github.issues.comments.expects(:create).with('test_owner', 'test_repo', 1, body: 'Test Comment')
|
47
|
+
PartyFoul.github.git_data.references.expects(:get).with('test_owner', 'test_repo', 'heads/deploy').returns(Hashie::Mash.new(object: Hashie::Mash.new(sha: 'abcdefg1234567890')))
|
44
48
|
PartyFoul::ExceptionHandler.new(nil, {}).run
|
45
49
|
end
|
46
50
|
end
|
@@ -51,6 +55,7 @@ describe 'Party Foul Exception Handler' do
|
|
51
55
|
PartyFoul.github.search.stubs(:issues).with(owner: 'test_owner', repo: 'test_repo', keyword: 'test_fingerprint', state: 'closed').returns(Hashie::Mash.new(issues: [{title: 'Test Title', body: 'Test Body', state: 'closed', number: 1}]))
|
52
56
|
PartyFoul.github.issues.expects(:edit).with('test_owner', 'test_repo', 1, body: 'New Body', state: 'open', labels: ['bug', 'regression'])
|
53
57
|
PartyFoul.github.issues.comments.expects(:create).with('test_owner', 'test_repo', 1, body: 'Test Comment')
|
58
|
+
PartyFoul.github.git_data.references.expects(:get).with('test_owner', 'test_repo', 'heads/deploy').returns(Hashie::Mash.new(object: Hashie::Mash.new(sha: 'abcdefg1234567890')))
|
54
59
|
PartyFoul::ExceptionHandler.new(nil, {}).run
|
55
60
|
end
|
56
61
|
end
|
@@ -64,6 +69,7 @@ describe 'Party Foul Exception Handler' do
|
|
64
69
|
PartyFoul.github.issues.expects(:create).never
|
65
70
|
PartyFoul.github.issues.expects(:edit).never
|
66
71
|
PartyFoul.github.issues.comments.expects(:create).never
|
72
|
+
PartyFoul.github.git_data.references.expects(:get).never
|
67
73
|
PartyFoul::ExceptionHandler.new(nil, {}).run
|
68
74
|
end
|
69
75
|
end
|
data/test/test_helper.rb
CHANGED
@@ -21,7 +21,7 @@ module MiniTest::Expectations
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def clean_up_party
|
24
|
-
%w{github oauth_token endpoint owner repo ignored_exceptions processor issue_template comment_template filtered_http_headers web_url}.each do |attr|
|
24
|
+
%w{github oauth_token endpoint owner repo ignored_exceptions processor issue_template comment_template filtered_http_headers web_url branch}.each do |attr|
|
25
25
|
PartyFoul.send("#{attr}=", nil)
|
26
26
|
end
|
27
27
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: party_foul
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2013-01-
|
13
|
+
date: 2013-01-28 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: github_api
|
@@ -200,7 +200,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
200
200
|
version: '0'
|
201
201
|
segments:
|
202
202
|
- 0
|
203
|
-
hash: -
|
203
|
+
hash: -549281873562555121
|
204
204
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
205
205
|
none: false
|
206
206
|
requirements:
|
@@ -209,7 +209,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
209
209
|
version: '0'
|
210
210
|
segments:
|
211
211
|
- 0
|
212
|
-
hash: -
|
212
|
+
hash: -549281873562555121
|
213
213
|
requirements: []
|
214
214
|
rubyforge_project:
|
215
215
|
rubygems_version: 1.8.23
|